diff --git a/crates/fonts/README.md b/crates/fonts/README.md index c462c428d4..963ee889b9 100644 --- a/crates/fonts/README.md +++ b/crates/fonts/README.md @@ -88,7 +88,8 @@ exact program preservation, Unicode/CID/GID mapping and full-face subset complet never exported. See [existing-text editing](../../docs/editing-existing-text.md). `pdf::embedded_sfnt_within` reconstructs a missing Unicode cmap for Identity-H TrueType -subsets from the PDF's ToUnicode and CID-to-glyph maps. Only the in-memory editor copy changes; +subsets from the PDF's ToUnicode and CID-to-glyph maps, and completes a THSarabunPSK subset's +Latin-only one with them. Only the in-memory editor copy changes; the PDF's font program stays intact. `pdf::LegacyThaiFont` shapes verified THSarabunPSK positional glyphs into their original PDF codes, retaining the source face and weight. Missing contextual glyphs are refused. `pdf::recovered_thai_map` recognizes narrowly fingerprinted diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 2693c19701..0ef49c81a5 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -113,7 +113,11 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg if face.metrics(skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default()).units_per_em == 0 { continue; } - if face.charmap().mappings().next().is_some() { + let own_cmap = face.charmap().mappings().next().is_some(); + // A legacy THSarabunPSK subset may keep a Latin-only cmap beside its Thai glyphs, which + // only the PDF's verified maps reach (`composite_glyph_maps`); other programs keep theirs. + let base_font = font.name(b"BaseFont").map(String::from_utf8_lossy).unwrap_or_default(); + if own_cmap && !base_font.rsplit('+').next().is_some_and(|f| f.starts_with("THSarabunPSK")) { return Some(bytes); } // PDF subsets may remove the Unicode cmap entirely. Rebuild an editor-only @@ -124,6 +128,20 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg if let Some((map, _)) = composite_glyph_maps(doc, font, &bytes, &metrics, budget) { mappings = map; } + if own_cmap { + let charmap = face.charmap(); + if mappings.keys().all(|&u| char::from_u32(u).is_some_and(|c| charmap.map(c).is_some())) { + return Some(bytes); + } + // The program's own mappings stay; the PDF's fill only what they lack. + mappings.extend(charmap.mappings().filter(|(_, g)| g.to_u32() != 0).take(65_537).map(|(u, g)| (u, g.to_u32()))); + let mut merged = bytes.clone(); + let usable = append_editor_cmap(&mut merged, &mappings).is_some() + && skrifa::FontRef::new(&merged).is_ok_and(|font| font.charmap().mappings().next().is_some()); + return Some(if usable { merged } else { bytes }); + } + } else if own_cmap { + return Some(bytes); } else if let Ok(cmap) = face.cmap() { for code in 0..=255 { let Some(text) = metrics.unicode.text(code) else { continue }; diff --git a/crates/fonts/src/pdf/thai.rs b/crates/fonts/src/pdf/thai.rs index 33932a6ca7..9ec2ff738b 100644 --- a/crates/fonts/src/pdf/thai.rs +++ b/crates/fonts/src/pdf/thai.rs @@ -583,6 +583,21 @@ mod tests { assert_eq!(shaped.shaped.glyphs.iter().map(|g| g.id.to_u32()).collect::>(), [214, 352]); } + /// A legacy subset that kept a Latin-only cmap of its own still reaches its Thai glyphs through + /// the PDF's verified maps, and keeps its own mappings. + #[test] + fn a_latin_only_cmap_does_not_hide_a_legacy_subsets_thai_glyphs() { + use skrifa::MetadataProvider as _; + let program = without_tables(&thai_ordered_program(), &[b"GSUB", b"GPOS"]); + let latin = skrifa::FontRef::new(&program).unwrap().charmap().map('A').unwrap(); + let mut doc = Document::new_empty(); + let font = thai_font(&mut doc, &program, INTACT); + 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]); + let editor = crate::pdf::embedded_sfnt(&doc, &font).unwrap(); + assert_eq!(skrifa::FontRef::new(&editor).unwrap().charmap().map('A'), Some(latin), "the program's own mappings stay"); + } + /// 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.