From e9142b03b91489c411e2ea342b73df012a38f1f8 Mon Sep 17 00:00:00 2001 From: Grzegorz Martens Date: Mon, 9 Nov 2026 16:27:49 +0100 Subject: [PATCH] M7: a composite font's /Encoding is read through a reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Legal forms review of 8fb534ad4. A Type 0 font's /Encoding may be given by reference (a name or a CMap stream, either indirect), and the metrics read resolve it: an indirect /Identity-H reads two-byte codes. The editor's check before reading a composite font's metrics, and `main`'s `composite_glyph_maps` and `recovered_thai_map`, read it with `Dict::name`, which doesn't resolve: such a font counted as another encoding, so the editor adapted no copy of it and Repair Thai maps passed it over. All three now read it through `identity_h`, which resolves it as the metrics do. Tests: `a_composite_fonts_encoding_given_by_reference_is_read` (the editor adapts a copy of a composite subset with `/Encoding 5 0 R`, object 5 `/Identity-H`, that maps 'Ω', and its metrics read two-byte codes), and `recovered_thai_map_checks_every_kept_glyph_against_the_font_program` recovers the same map with the font's /Encoding by reference. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY --- crates/fonts/src/pdf.rs | 8 +++++++- crates/fonts/src/pdf/tests.rs | 14 ++++++++++++++ crates/fonts/src/pdf/thai.rs | 10 +++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 43670fb4db..1573204192 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -84,6 +84,12 @@ fn dict(doc: &Document, o: Option<&Object>) -> Option { o.and_then(|o| doc.resolve(o).as_dict().cloned()) } +/// Whether `font`'s `/Encoding` is the name Identity-H, given directly or through a reference, +/// as [`Metrics::from_dict`] reads it (a Type 0 font's `/Encoding` may be indirect). +fn identity_h(doc: &Document, font: &Dict) -> bool { + font.get(b"Encoding").is_some_and(|e| doc.resolve(e).as_name() == Some(b"Identity-H".as_slice())) +} + /// Whether `base_font` (subset tag allowed) names a legacy Indic font family that is laid out in /// visual order (Kruti Dev, DevLys, Chanakya, Shusha, Shivaji, Walkman, Akruti, APS-DV, DV-TT…). fn legacy_indic_font(base_font: &str) -> bool { @@ -138,7 +144,7 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg // 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 font.name(b"Subtype") == Some(b"Type0") && !identity_h(doc, font) { if own_cmap { return Some(bytes); } diff --git a/crates/fonts/src/pdf/tests.rs b/crates/fonts/src/pdf/tests.rs index 54b38c6e16..975b71289e 100644 --- a/crates/fonts/src/pdf/tests.rs +++ b/crates/fonts/src/pdf/tests.rs @@ -2015,6 +2015,20 @@ fn a_composite_font_that_cant_be_adapted_spends_only_its_program() { } } +/// A composite font's `/Encoding` given through a reference is the name it refers to, as the +/// metrics read it: Identity-H by reference is adapted as Identity-H written directly is. +#[test] +fn a_composite_fonts_encoding_given_by_reference_is_read() { + use skrifa::MetadataProvider as _; + let mut doc = Document::new_empty(); + let (mut source, _, _, expected, _) = cmapless_composite(&mut doc); + let identity = doc.add(Object::name("Identity-H")); + source.set(b"Encoding".to_vec(), Object::Ref(identity)); + assert_eq!(Metrics::from_dict(&doc, &source).code_len, 2); + let editor = embedded_sfnt(&doc, &source).expect("Identity-H by reference is adapted"); + assert_eq!(skrifa::FontRef::new(&editor).unwrap().charmap().map('Ω'), Some(expected)); +} + /// 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. diff --git a/crates/fonts/src/pdf/thai.rs b/crates/fonts/src/pdf/thai.rs index a6da5f1d77..c6e1941fbd 100644 --- a/crates/fonts/src/pdf/thai.rs +++ b/crates/fonts/src/pdf/thai.rs @@ -7,7 +7,7 @@ //! does (a mark takes none); correct maps and unrelated fonts are never changed. No font //! artwork is bundled. -use super::{Dict, Document, Metrics, Reads, dict, only_char, unicode_map}; +use super::{Dict, Document, Metrics, Reads, dict, identity_h, only_char, unicode_map}; use pdfcraft_cos::{DecodeBudget, Object}; use std::collections::BTreeMap; @@ -19,7 +19,7 @@ pub fn recovered_thai_map(doc: &Document, font: &Dict) -> Option Option<(BTreeMap, BTreeMap)> { use skrifa::{MetadataProvider as _, raw::TableProvider as _}; - if font.name(b"Encoding") != Some(b"Identity-H") { + if !identity_h(doc, font) { return None; } let descendant = dict(doc, font.get(b"DescendantFonts").and_then(|a| doc.resolve(a).as_array().and_then(|a| a.first()).cloned()).as_ref())?; @@ -548,6 +548,10 @@ mod tests { let recovered: HashMap<_, _> = recovered_thai_map(&doc, &font).unwrap().into_iter().collect(); assert_eq!(recovered[&262], 'ั'); assert_eq!(recovered[&297], '๘'); + // `/Encoding` given through a reference is the name it refers to. + let mut indirect = font.clone(); + indirect.set(b"Encoding".to_vec(), Object::Ref(doc.add(Object::name("Identity-H")))); + assert_eq!(recovered_thai_map(&doc, &indirect), recovered_thai_map(&doc, &font)); // 259–261 are ฮ, ฯ and ะ, between the last anchor (อ, 258) and the first mark: a font that // put Mai Han-akat one glyph earlier passes every anchor and only this check sees it. let mut shifted = thai_ordered_program();