From d351aa2760e81ce67fe8f00b9d79dd4872e6ef5a Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 8 Nov 2026 23:23:09 +0700 Subject: [PATCH] Read Thai font programs and CIDToGIDMaps with DecodeParms given by reference A predictor whose parameters are an indirect object left its row tags in the decoded font program and glyph map, so the Thai map repair, the glyph codes and the shaper read the wrong bytes. Resolve the parameters as the other font readers do, keeping the decode budgets. --- crates/fonts/src/pdf.rs | 2 +- crates/fonts/src/pdf/thai.rs | 50 ++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index d093c22e69..2693c19701 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -108,7 +108,7 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg let Some(object) = descriptor.get(key) else { continue }; let object = doc.resolve(object); let Object::Stream(stream) = object.as_ref() else { continue }; - let Ok(mut bytes) = budget.decode_within(stream, MAX_SFNT) else { continue }; + let Ok(mut bytes) = budget.decode_within(&doc.with_direct_parms(stream), MAX_SFNT) else { continue }; let Ok(face) = skrifa::FontRef::from_index(&bytes, 0) else { continue }; if face.metrics(skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default()).units_per_em == 0 { continue; diff --git a/crates/fonts/src/pdf/thai.rs b/crates/fonts/src/pdf/thai.rs index 950cd9edb0..33932a6ca7 100644 --- a/crates/fonts/src/pdf/thai.rs +++ b/crates/fonts/src/pdf/thai.rs @@ -170,7 +170,7 @@ fn embedded_truetype(doc: &Document, descendant: &Dict) -> Option> { let descriptor = dict(doc, descendant.get(b"FontDescriptor"))?; let program = doc.resolve(descriptor.get(b"FontFile2")?); let Object::Stream(program) = program.as_ref() else { return None }; - DecodeBudget::new(super::MAX_SFNT, 0).decode_within(program, super::MAX_SFNT).ok() + DecodeBudget::new(super::MAX_SFNT, 0).decode_within(&doc.with_direct_parms(program), super::MAX_SFNT).ok() } /// Source glyph positions and the original PDF character codes that draw them. Empty codes @@ -197,7 +197,7 @@ pub(super) fn composite_glyph_maps( let cid_map = match descendant.get(b"CIDToGIDMap").map(|m| doc.resolve(m)).as_deref() { None => None, Some(Object::Name(n)) if n == b"Identity" => None, - Some(Object::Stream(s)) => Some(budget.decode_within(s, 1 << 17).ok()?), + Some(Object::Stream(s)) => Some(budget.decode_within(&doc.with_direct_parms(s), 1 << 17).ok()?), _ => return None, }; let face = skrifa::FontRef::new(bytes).ok()?; @@ -473,12 +473,17 @@ mod tests { /// A THSarabunPSK Identity-H font over `program`, with `to_unicode`. fn thai_font(doc: &mut Document, program: &[u8], to_unicode: &[u8]) -> Dict { + let file = Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), program)))); + thai_font_over(doc, file, Object::name("Identity"), to_unicode) + } + + /// [`thai_font`] over the `/FontFile2` stream `file` and the `/CIDToGIDMap` `cid_to_gid`. + fn thai_font_over(doc: &mut Document, file: Object, cid_to_gid: Object, to_unicode: &[u8]) -> Dict { let tu = doc.add(Object::Stream(Stream::flate(Dict::new(), to_unicode))); - let file = doc.add(Object::Stream(Stream::flate(Dict::new(), program))); let mut descriptor = Dict::new(); - descriptor.set(b"FontFile2".to_vec(), Object::Ref(file)); + descriptor.set(b"FontFile2".to_vec(), file); let mut desc = Dict::new(); - desc.set(b"CIDToGIDMap".to_vec(), Object::name("Identity")); + desc.set(b"CIDToGIDMap".to_vec(), cid_to_gid); desc.set(b"FontDescriptor".to_vec(), Object::Dict(descriptor)); let mut font = Dict::new(); font.set(b"Subtype".to_vec(), Object::name("Type0")); @@ -489,6 +494,22 @@ mod tests { font } + /// `data` as a Flate stream under a PNG predictor whose `/DecodeParms` is given by reference: + /// read without that dictionary, each row's tag byte stays in the decoded bytes. + fn predicted(doc: &mut Document, data: &[u8]) -> Object { + let rows: Vec = data.iter().flat_map(|byte| [0, *byte]).collect(); + let mut parms = Dict::new(); + parms.set(b"Predictor".to_vec(), Object::Int(12)); + parms.set(b"Columns".to_vec(), Object::Int(1)); + let parms = doc.add(Object::Dict(parms)); + let mut stream = Stream::flate(Dict::new(), &rows); + stream.dict.set(b"DecodeParms".to_vec(), Object::Ref(parms)); + Object::Ref(doc.add(Object::Stream(stream))) + } + + /// An intact legacy map: nominal Thai and two tone alternates. + const INTACT: &[u8] = b"2 beginbfrange <00d6> <010f> <0e01> <0110> <012a> <0e3f> endbfrange 2 beginbfchar <0160> <0e48> <0161> <0e49> endbfchar"; + /// The exporter's damaged map: intact anchors, Latin/control destinations for the other Thai /// codes, and the oversized range first. const DAMAGED: &[u8] = b"begincmap 1 begincodespacerange <0000> endcodespacerange \ @@ -543,6 +564,25 @@ mod tests { program } + /// The font program and `/CIDToGIDMap` are read with `/DecodeParms` given by reference, as a + /// viewer reads them: the map repair, the glyph codes and the shaper see the real bytes. + #[test] + fn thai_font_streams_are_read_with_decode_parms_given_by_reference() { + let stripped = without_tables(&thai_ordered_program(), &[b"cmap", b"GSUB", b"GPOS"]); + let mut doc = Document::new_empty(); + let program = predicted(&mut doc, &stripped); + let damaged = thai_font_over(&mut doc, program.clone(), Object::name("Identity"), DAMAGED); + assert!(recovered_thai_map(&doc, &damaged).is_some(), "the program is read with its predictor"); + let gids: Vec = (0u16..=368).flat_map(u16::to_be_bytes).collect(); + let map = predicted(&mut doc, &gids); + let font = thai_font_over(&mut doc, program, map, INTACT); + let metrics = Metrics::from_dict(&doc, &font); + let (map, _) = composite_glyph_maps(&doc, &font, &stripped, &metrics, &mut DecodeBudget::new(1 << 20, 0)).unwrap(); + assert_eq!(map.get(&0x0e01), Some(&214), "the CIDToGIDMap is read with its predictor"); + let shaped = LegacyThaiFont::from_pdf(&doc, &font, 0.25).unwrap().expect("a legacy subset").shape("ก่").unwrap(); + assert_eq!(shaped.shaped.glyphs.iter().map(|g| g.id.to_u32()).collect::>(), [214, 352]); + } + /// A THSarabunPSK program with its own Thai mark positioning is shaped by that layout; one /// without it only if every Thai glyph it keeps is where the legacy order has that character /// (the legacy shaper skips the positioning check). A subset needs no particular glyph.