use crate::model::Color;
use crate::util::{u16le, u32le};
const FKP_SIZE: usize = 512;
const MAX_FKP_ENTRIES: usize = 1 << 20;
#[cfg(test)]
thread_local! {
static TEST_MAX_FKP: std::cell::Cell<usize> = const { std::cell::Cell::new(MAX_FKP_ENTRIES) };
}
#[cfg(test)]
fn set_test_max_fkp(n: usize) {
TEST_MAX_FKP.with(|c| c.set(n));
}
fn max_fkp_entries() -> usize {
#[cfg(test)]
{
TEST_MAX_FKP.with(|c| c.get())
}
#[cfg(not(test))]
{
MAX_FKP_ENTRIES
}
}
const SPRM_C_F_BOLD: u16 = 0x0835;
const SPRM_C_F_ITALIC: u16 = 0x0836;
const SPRM_C_F_STRIKE: u16 = 0x0837;
const SPRM_C_F_VANISH: u16 = 0x083C; const SPRM_C_F_SPEC: u16 = 0x0855; const SPRM_C_KUL: u16 = 0x2A3E; const SPRM_C_PIC_LOCATION: u16 = 0x6A03; const SPRM_C_HPS: u16 = 0x4A43; const SPRM_C_RG_FTC0: u16 = 0x4A4F; const SPRM_C_CV: u16 = 0x6870; const SPRM_C_ICO: u16 = 0x2A42;
fn ico_color(i: u8) -> Option<Color> {
let (r, g, b) = match i {
0 | 1 => (0, 0, 0), 2 => (0, 0, 0xFF), 3 => (0, 0xFF, 0xFF), 4 => (0, 0xFF, 0), 5 => (0xFF, 0, 0xFF), 6 => (0xFF, 0, 0), 7 => (0xFF, 0xFF, 0), 8 => (0xFF, 0xFF, 0xFF), 9 => (0, 0, 0x80), 10 => (0, 0x80, 0x80), 11 => (0, 0x80, 0), 12 => (0x80, 0, 0x80), 13 => (0x80, 0, 0), 14 => (0x80, 0x80, 0), 15 => (0x80, 0x80, 0x80), 16 => (0xC0, 0xC0, 0xC0), _ => return None,
};
Some(Color { r, g, b })
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct Chp {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strike: bool,
pub hidden: bool,
pub size_half_pt: Option<u16>,
pub ftc: Option<u16>,
pub color: Option<Color>,
pub pic: Option<u32>,
}
#[derive(Debug, Clone, Copy)]
struct ChpEntry {
fc_lim: u32,
chp: Chp,
}
#[derive(Debug, Default)]
pub(crate) struct ChpxTable {
entries: Vec<ChpEntry>,
}
impl ChpxTable {
pub(crate) fn chp_at(&self, fc: u32) -> Chp {
let i = self.entries.partition_point(|e| e.fc_lim <= fc);
self.entries.get(i).map(|e| e.chp).unwrap_or_default()
}
pub(crate) fn pic_at(&self, fc: u32) -> Option<u32> {
self.chp_at(fc).pic
}
}
pub(crate) fn parse(word: &[u8], table: &[u8], fc_plcf: usize, lcb_plcf: usize) -> ChpxTable {
let mut entries = Vec::new();
if lcb_plcf < 4 {
return ChpxTable { entries };
}
let Some(plc) = table.get(fc_plcf..fc_plcf.saturating_add(lcb_plcf)) else {
return ChpxTable { entries };
};
let cap = max_fkp_entries();
let n = (plc.len().saturating_sub(4) / 8).min(cap);
let pn_base = 4 * (n + 1);
for i in 0..n {
if entries.len() >= cap {
break;
}
let Some(pn_raw) = u32le(plc, pn_base + i * 4) else {
break;
};
let page = (pn_raw & 0x003F_FFFF) as usize; let off = page.saturating_mul(FKP_SIZE);
parse_fkp(word, off, &mut entries);
}
entries.sort_by_key(|e| e.fc_lim);
ChpxTable { entries }
}
fn parse_fkp(word: &[u8], page_off: usize, out: &mut Vec<ChpEntry>) {
let Some(page) = word.get(page_off..page_off + FKP_SIZE) else {
return;
};
let crun = page[FKP_SIZE - 1] as usize;
if crun == 0 || 4 * (crun + 1) + crun >= FKP_SIZE {
return;
}
for i in 0..crun {
let Some(fc_lim) = u32le(page, 4 * (i + 1)) else {
break;
};
let b = page.get(4 * (crun + 1) + i).copied().unwrap_or(0) as usize;
let chp = if b == 0 {
Chp::default()
} else {
parse_chpx(page, b * 2)
};
out.push(ChpEntry { fc_lim, chp });
}
}
fn parse_chpx(page: &[u8], off: usize) -> Chp {
let Some(&cb) = page.get(off) else {
return Chp::default();
};
match page.get(off + 1..off + 1 + cb as usize) {
Some(gp) => scan_grpprl(gp),
None => Chp::default(),
}
}
fn scan_grpprl(gp: &[u8]) -> Chp {
let mut chp = Chp::default();
let mut fspec = false;
let mut picloc = None;
let mut pos = 0;
while pos + 2 <= gp.len() {
let Some(sprm) = u16le(gp, pos) else { break };
let op = pos + 2;
let Some(len) = operand_len(sprm, gp, op) else {
break;
};
let toggle = || matches!(gp.get(op).copied().unwrap_or(0), 0x01 | 0x81);
match sprm {
SPRM_C_F_BOLD => chp.bold = toggle(),
SPRM_C_F_ITALIC => chp.italic = toggle(),
SPRM_C_F_STRIKE => chp.strike = toggle(),
SPRM_C_F_VANISH => chp.hidden = toggle(),
SPRM_C_F_SPEC => fspec = gp.get(op).copied().unwrap_or(0) != 0,
SPRM_C_KUL => chp.underline = gp.get(op).copied().unwrap_or(0) != 0,
SPRM_C_PIC_LOCATION => picloc = u32le(gp, op),
SPRM_C_HPS => chp.size_half_pt = u16le(gp, op),
SPRM_C_RG_FTC0 => chp.ftc = u16le(gp, op),
SPRM_C_CV => {
if let (Some(&r), Some(&g), Some(&b)) = (gp.get(op), gp.get(op + 1), gp.get(op + 2))
{
chp.color = Some(Color { r, g, b });
}
}
SPRM_C_ICO if chp.color.is_none() => {
chp.color = ico_color(gp.get(op).copied().unwrap_or(0));
}
_ => {}
}
pos = op + len;
}
chp.pic = if fspec { picloc } else { None };
chp
}
fn operand_len(sprm: u16, data: &[u8], op: usize) -> Option<usize> {
match (sprm >> 13) & 0x7 {
0 | 1 => Some(1),
2 | 4 | 5 => Some(2),
3 => Some(4),
7 => Some(3),
6 => Some(1 + *data.get(op)? as usize),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scans_bold_italic_hidden() {
let chp = scan_grpprl(&[0x35, 0x08, 0x01, 0x36, 0x08, 0x01, 0x3C, 0x08, 0x01]);
assert!(chp.bold && chp.italic && chp.hidden);
assert!(!chp.strike && !chp.underline);
}
#[test]
fn bold_off_and_inherit() {
assert!(!scan_grpprl(&[0x35, 0x08, 0x00]).bold); assert!(!scan_grpprl(&[0x35, 0x08, 0x80]).bold); assert!(scan_grpprl(&[0x35, 0x08, 0x81]).bold); }
#[test]
fn underline_and_strike() {
let chp = scan_grpprl(&[0x3E, 0x2A, 0x01, 0x37, 0x08, 0x01]);
assert!(chp.underline && chp.strike);
}
#[test]
fn skips_unknown_sprm_by_spra() {
let chp = scan_grpprl(&[0x43, 0x4A, 0xAA, 0xBB, 0x35, 0x08, 0x01]);
assert!(chp.bold);
}
#[test]
fn lookup_by_fc() {
let t = ChpxTable {
entries: vec![
ChpEntry {
fc_lim: 100,
chp: Chp {
bold: true,
..Chp::default()
},
},
ChpEntry {
fc_lim: 200,
chp: Chp {
italic: true,
..Chp::default()
},
},
],
};
assert!(t.chp_at(50).bold);
assert!(t.chp_at(150).italic);
assert!(!t.chp_at(250).bold && !t.chp_at(250).italic); }
#[test]
fn bin_table_entry_count_is_capped() {
let mut word = vec![0u8; FKP_SIZE];
word[FKP_SIZE - 1] = 101;
let n_pages = 10usize;
let plc = vec![0u8; 4 * (n_pages + 1) + 4 * n_pages];
set_test_max_fkp(64);
let t = parse(&word, &plc, 0, plc.len());
set_test_max_fkp(MAX_FKP_ENTRIES);
assert!(
t.entries.len() >= 64 && t.entries.len() < n_pages * 101,
"entry cap did not bound the repeated-page bin table: {}",
t.entries.len()
);
}
}