From 509cf9f9fd96aa22153e2e28419f1cf9f960308c Mon Sep 17 00:00:00 2001 From: Grzegorz Martens Date: Mon, 9 Nov 2026 13:36:44 +0100 Subject: [PATCH] M7: the editor reads no metrics for a composite font it can't adapt The Resources review of 8eead8ecd: - `embedded_sfnt_within` read a composite font's metrics within the editor's allowance before `composite_glyph_maps` refused any `/Encoding` but Identity-H, so an Identity-V or UniJIS font spent its ToUnicode and `/CIDToGIDMap` for nothing. Its `/Encoding` is now checked first; a program with a cmap of its own is kept as it is, as when the rebuild finds nothing. - The Thai edit's allowance, `MAX_THAI_PROGRAMS`, now also pays for the CMaps and glyph maps its subsets' metrics and codes are read from: its doc and its refusal say "font data", not "font programs". - `a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names` also reads its font, whose `post` table names its glyphs, within an allowance: the program is paid for by the glyph-name reading, by the Mac OS Roman one and for the widths, and a byte short of the first the names go unread. Tests: `a_composite_font_that_cant_be_adapted_spends_only_its_program`, and `adapting_a_programs_cmap_is_paid_for` again expects a composite font whose codes aren't Identity-H to pay for its program only. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY --- crates/edit/src/text_regions/styled.rs | 7 ++--- crates/fonts/src/pdf.rs | 9 +++++++ crates/fonts/src/pdf/tests.rs | 37 +++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/crates/edit/src/text_regions/styled.rs b/crates/edit/src/text_regions/styled.rs index 487bb0e20e..d45049db02 100644 --- a/crates/edit/src/text_regions/styled.rs +++ b/crates/edit/src/text_regions/styled.rs @@ -116,8 +116,9 @@ struct LegacyFace { space: f64, } -/// What one edit may decode, and so keep, of the font programs of its THSarabunPSK subsets -/// (each at most 16 MiB): a page may name many of them. +/// What one edit may decode of its THSarabunPSK subsets' font data: their programs (each at most +/// 16 MiB), which it keeps, and the CMaps and glyph maps their metrics and codes are read from. A +/// page may name many of them. const MAX_THAI_PROGRAMS: usize = 64 << 20; struct PreparedCharacters { @@ -926,7 +927,7 @@ fn legacy_thai_font( // A subset left unread would count as unusable, silently changing which subset draws the text. if budget.remaining() == 0 { return Err(EditError::Invalid(format!( - "the page's {} subsets need more than {} MiB of font programs for one edit; choose a replacement font for this text", + "the page's {} subsets need more than {} MiB of font data for one edit; choose a replacement font for this text", source.base_font.rsplit('+').next().unwrap_or(&source.base_font), MAX_THAI_PROGRAMS >> 20 ))); diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index c8f34bd1c4..43670fb4db 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -135,6 +135,15 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg if own_cmap && !base_font.rsplit('+').next().is_some_and(|f| f.starts_with("THSarabunPSK")) { return Some(bytes); } + // A composite font's cmap is rebuilt only through Identity-H codes (`composite_glyph_maps`): + // with any other `/Encoding` nothing is, so its metrics aren't read, and a program with a + // cmap of its own is kept as it is, as when the rebuild finds nothing. + if font.name(b"Subtype") == Some(b"Type0") && font.name(b"Encoding") != Some(b"Identity-H") { + if own_cmap { + return Some(bytes); + } + continue; + } // PDF subsets may remove the Unicode cmap entirely. Rebuild an editor-only // cmap from ToUnicode and the actual CID-to-glyph map; source bytes stay unchanged. // Metrics read past the allowance, its widths' read of the program again included, diff --git a/crates/fonts/src/pdf/tests.rs b/crates/fonts/src/pdf/tests.rs index 53d43f0c72..54b38c6e16 100644 --- a/crates/fonts/src/pdf/tests.rs +++ b/crates/fonts/src/pdf/tests.rs @@ -243,13 +243,13 @@ fn adapting_a_programs_cmap_is_paid_for() { assert_eq!(skrifa::FontRef::new(&editor).unwrap().charmap().map('Ω'), Some(expected)); assert_eq!(budget.remaining(), 0); assert!(embedded_sfnt_within(&doc, &source, &mut DecodeBudget::new(cost - 1, 0)).is_none()); - // A composite font's metrics are read too, and paid for: the program and its ToUnicode, which - // give no glyph here (its codes aren't Identity-H). + // A composite font whose codes aren't Identity-H can't be adapted, so its metrics aren't + // read: the program is all it pays for. let mut budget = DecodeBudget::new(cost, 0); let mut composite = source.clone(); composite.set(b"Subtype".to_vec(), Object::name("Type0")); assert!(embedded_sfnt_within(&doc, &composite, &mut budget).is_none()); - assert_eq!(budget.remaining(), cost - subset.len() - cmap.len()); + assert_eq!(budget.remaining(), cost - subset.len()); } /// A Type 0 subset font whose `/Encoding`, program, `/CIDToGIDMap` and ToUnicode are streams, and @@ -721,6 +721,21 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() { ], ); assert_eq!(Metrics::from_dict(&doc, &f).decode(&codes), ""); + // The names are read within the allowance the metrics are read within: the program is paid + // for by the glyph-name reading, by the Mac OS Roman one (which finds the glyphs named and + // reads nothing) and for the widths. A byte short of the first, the names go unread. + let mut doc = Document::new_empty(); + let program = doc.add(Object::Stream(Stream::flate(Dict::new(), &subset))); + let mut descriptor = Dict::new(); + descriptor.set(b"Flags".to_vec(), Object::Int(4)); + descriptor.set(b"FontFile2".to_vec(), Object::Ref(program)); + let f = font(&mut doc, vec![("Subtype", Object::name("TrueType")), ("FontDescriptor", Object::Dict(descriptor))]); + let mut budget = DecodeBudget::new(3 * subset.len(), 0); + assert_eq!(Metrics::from_dict_within(&doc, &f, &mut budget), symbolic); + assert_eq!(budget.remaining(), 0); + let short = Metrics::from_dict_within(&doc, &f, &mut DecodeBudget::new(subset.len() - 1, 0)); + assert!(short.past_allowance); + assert_ne!(short.decode(&codes), TEXT, "the names unread"); } /// The glyph list specification's `uni` and `u` forms spell their hex digits in uppercase @@ -1984,6 +1999,22 @@ fn adapting_a_composite_subsets_cmap_is_paid_for() { assert!(embedded_sfnt_within(&doc, &source, &mut DecodeBudget::new(subset.len() + cmap - 1, 0)).is_none()); } +/// A composite font whose `/Encoding` isn't Identity-H can't have its cmap rebuilt +/// (`composite_glyph_maps` reads only Identity-H codes), so the editor reads its program and no +/// more: its ToUnicode and `/CIDToGIDMap` are left for what is left of the allowance. +#[test] +fn a_composite_font_that_cant_be_adapted_spends_only_its_program() { + let mut doc = Document::new_empty(); + let (source, _, subset, _, [cmap, map]) = cmapless_composite(&mut doc); + for encoding in ["Identity-V", "UniJIS-UCS2-H"] { + let mut f = source.clone(); + f.set(b"Encoding".to_vec(), Object::name(encoding)); + let mut budget = DecodeBudget::new(subset.len() + cmap + map, 0); + assert!(embedded_sfnt_within(&doc, &f, &mut budget).is_none(), "{encoding}"); + assert_eq!(budget.remaining(), cmap + map, "{encoding}"); + } +} + /// The `/CIDToGIDMap` a composite subset's cmap is rebuilt from reads as the font's other streams /// do: a full map, which compresses well, reads behind ASCIIHex and Flate, though the hex stage's /// output comes on top of the map.