M7: a composite font's /Encoding is read through a reference

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz MartensandClaude Opus 5.5 committed 2026-11-09 16:27:49 +01:00
1 parent 8fb534ad4c
commit e9142b03b9
3 files changed
+28 -4

No files matched your search

+7 -1
View File
@@ -84,6 +84,12 @@ fn dict(doc: &Document, o: Option<&Object>) -> Option<Dict> {
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);
}
+14
View File
@@ -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.
+7 -3
View File
@@ -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<Vec<(u16, char)
let family = base.rsplit('+').next()?;
if !matches!(family, "THSarabunPSK" | "THSarabunPSK-Bold" | "THSarabunPSK-Italic" | "THSarabunPSK-BoldItalic")
|| font.name(b"Subtype") != Some(b"Type0")
|| font.name(b"Encoding") != Some(b"Identity-H")
|| !identity_h(doc, font)
{
return None;
}
@@ -190,7 +190,7 @@ pub(super) fn composite_glyph_maps(
budget: &mut DecodeBudget,
) -> Option<(BTreeMap<u32, u32>, BTreeMap<u32, u16>)> {
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();