From cc9b94b632e78e59c03571b894a34383b2b20da9 Mon Sep 17 00:00:00 2001 From: Velli20 Date: Tue, 13 Oct 2026 21:30:27 +0300 Subject: [PATCH] Draw correct glyphs for non-embedded Identity TrueType CID fonts A non-embedded CIDFontType2 font with Identity encoding and no CIDToGIDMap selects glyphs of the named system font by index. The substitute is a name-keyed CFF with a different glyph order, so using the CID as its glyph index drew the wrong glyphs. TrueType core fonts such as Arial keep the standard Macintosh glyph order for their first 258 glyphs. For such fonts, map CIDs to Unicode through a compile-time table of that order, so the substitute draws glyphs by character. Type0 fonts also now pick the substitute's weight and style from their base font name instead of always regular Helvetica. Co-Authored-By: Claude Opus 5.5 --- crates/pdf-font/src/cid_to_unicode.rs | 292 ++++++++++++++++++ crates/pdf-font/src/lib.rs | 1 + crates/pdf-font/src/pdf_font_descriptor.rs | 7 +- crates/pdf-font/src/pdf_font_spec.rs | 5 +- crates/pdf-font/src/standard14.rs | 7 + crates/pdf-font/src/type0_font_spec.rs | 19 +- .../pdf-text-engine/src/pdf_text_decoder.rs | 3 +- 7 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 crates/pdf-font/src/cid_to_unicode.rs diff --git a/crates/pdf-font/src/cid_to_unicode.rs b/crates/pdf-font/src/cid_to_unicode.rs new file mode 100644 index 00000000..f590677f --- /dev/null +++ b/crates/pdf-font/src/cid_to_unicode.rs @@ -0,0 +1,292 @@ +//! Best-effort CID-to-Unicode mappings for substituted CID fonts. + +use std::{collections::HashMap, sync::Arc}; + +/// Best-effort CID-to-Unicode mapping used when a CID font has no `/ToUnicode` entry. +#[derive(Clone)] +pub enum CidToUnicode { + /// Inverted predefined Unicode CMap of a CJK character collection. + Collection(Arc>), + /// CIDs are glyph indices in the standard Macintosh glyph order. + /// + /// TrueType core fonts such as Arial, Times New Roman, and Courier New keep this order for + /// their first 258 glyphs. A non-embedded Identity CIDFontType2 font naming one of them + /// selects glyphs of that font by index, so its CIDs follow this order. + StandardGlyphOrder, +} + +impl CidToUnicode { + /// Returns the Unicode value of `cid`, if known. + #[must_use] + pub fn get(&self, cid: u16) -> Option { + match self { + Self::Collection(map) => map.get(&cid).copied(), + Self::StandardGlyphOrder => STANDARD_GLYPH_ORDER_UNICODE + .get(usize::from(cid)) + .copied() + .flatten(), + } + } +} + +/// Unicode value of each glyph in the standard Macintosh glyph order, by glyph index. +const STANDARD_GLYPH_ORDER_UNICODE: [Option; 258] = [ + None, // .notdef + None, // .null + None, // nonmarkingreturn + Some(' '), // space + Some('!'), // exclam + Some('"'), // quotedbl + Some('#'), // numbersign + Some('$'), // dollar + Some('%'), // percent + Some('&'), // ampersand + Some('\''), // quotesingle + Some('('), // parenleft + Some(')'), // parenright + Some('*'), // asterisk + Some('+'), // plus + Some(','), // comma + Some('-'), // hyphen + Some('.'), // period + Some('/'), // slash + Some('0'), // zero + Some('1'), // one + Some('2'), // two + Some('3'), // three + Some('4'), // four + Some('5'), // five + Some('6'), // six + Some('7'), // seven + Some('8'), // eight + Some('9'), // nine + Some(':'), // colon + Some(';'), // semicolon + Some('<'), // less + Some('='), // equal + Some('>'), // greater + Some('?'), // question + Some('@'), // at + Some('A'), // A + Some('B'), // B + Some('C'), // C + Some('D'), // D + Some('E'), // E + Some('F'), // F + Some('G'), // G + Some('H'), // H + Some('I'), // I + Some('J'), // J + Some('K'), // K + Some('L'), // L + Some('M'), // M + Some('N'), // N + Some('O'), // O + Some('P'), // P + Some('Q'), // Q + Some('R'), // R + Some('S'), // S + Some('T'), // T + Some('U'), // U + Some('V'), // V + Some('W'), // W + Some('X'), // X + Some('Y'), // Y + Some('Z'), // Z + Some('['), // bracketleft + Some('\\'), // backslash + Some(']'), // bracketright + Some('^'), // asciicircum + Some('_'), // underscore + Some('`'), // grave + Some('a'), // a + Some('b'), // b + Some('c'), // c + Some('d'), // d + Some('e'), // e + Some('f'), // f + Some('g'), // g + Some('h'), // h + Some('i'), // i + Some('j'), // j + Some('k'), // k + Some('l'), // l + Some('m'), // m + Some('n'), // n + Some('o'), // o + Some('p'), // p + Some('q'), // q + Some('r'), // r + Some('s'), // s + Some('t'), // t + Some('u'), // u + Some('v'), // v + Some('w'), // w + Some('x'), // x + Some('y'), // y + Some('z'), // z + Some('{'), // braceleft + Some('|'), // bar + Some('}'), // braceright + Some('~'), // asciitilde + Some('\u{00C4}'), // Adieresis + Some('\u{00C5}'), // Aring + Some('\u{00C7}'), // Ccedilla + Some('\u{00C9}'), // Eacute + Some('\u{00D1}'), // Ntilde + Some('\u{00D6}'), // Odieresis + Some('\u{00DC}'), // Udieresis + Some('\u{00E1}'), // aacute + Some('\u{00E0}'), // agrave + Some('\u{00E2}'), // acircumflex + Some('\u{00E4}'), // adieresis + Some('\u{00E3}'), // atilde + Some('\u{00E5}'), // aring + Some('\u{00E7}'), // ccedilla + Some('\u{00E9}'), // eacute + Some('\u{00E8}'), // egrave + Some('\u{00EA}'), // ecircumflex + Some('\u{00EB}'), // edieresis + Some('\u{00ED}'), // iacute + Some('\u{00EC}'), // igrave + Some('\u{00EE}'), // icircumflex + Some('\u{00EF}'), // idieresis + Some('\u{00F1}'), // ntilde + Some('\u{00F3}'), // oacute + Some('\u{00F2}'), // ograve + Some('\u{00F4}'), // ocircumflex + Some('\u{00F6}'), // odieresis + Some('\u{00F5}'), // otilde + Some('\u{00FA}'), // uacute + Some('\u{00F9}'), // ugrave + Some('\u{00FB}'), // ucircumflex + Some('\u{00FC}'), // udieresis + Some('\u{2020}'), // dagger + Some('\u{00B0}'), // degree + Some('\u{00A2}'), // cent + Some('\u{00A3}'), // sterling + Some('\u{00A7}'), // section + Some('\u{2022}'), // bullet + Some('\u{00B6}'), // paragraph + Some('\u{00DF}'), // germandbls + Some('\u{00AE}'), // registered + Some('\u{00A9}'), // copyright + Some('\u{2122}'), // trademark + Some('\u{00B4}'), // acute + Some('\u{00A8}'), // dieresis + Some('\u{2260}'), // notequal + Some('\u{00C6}'), // AE + Some('\u{00D8}'), // Oslash + Some('\u{221E}'), // infinity + Some('\u{00B1}'), // plusminus + Some('\u{2264}'), // lessequal + Some('\u{2265}'), // greaterequal + Some('\u{00A5}'), // yen + Some('\u{00B5}'), // mu + Some('\u{2202}'), // partialdiff + Some('\u{2211}'), // summation + Some('\u{220F}'), // product + Some('\u{03C0}'), // pi + Some('\u{222B}'), // integral + Some('\u{00AA}'), // ordfeminine + Some('\u{00BA}'), // ordmasculine + Some('\u{2126}'), // Omega + Some('\u{00E6}'), // ae + Some('\u{00F8}'), // oslash + Some('\u{00BF}'), // questiondown + Some('\u{00A1}'), // exclamdown + Some('\u{00AC}'), // logicalnot + Some('\u{221A}'), // radical + Some('\u{0192}'), // florin + Some('\u{2248}'), // approxequal + Some('\u{2206}'), // Delta + Some('\u{00AB}'), // guillemotleft + Some('\u{00BB}'), // guillemotright + Some('\u{2026}'), // ellipsis + Some('\u{00A0}'), // nonbreakingspace + Some('\u{00C0}'), // Agrave + Some('\u{00C3}'), // Atilde + Some('\u{00D5}'), // Otilde + Some('\u{0152}'), // OE + Some('\u{0153}'), // oe + Some('\u{2013}'), // endash + Some('\u{2014}'), // emdash + Some('\u{201C}'), // quotedblleft + Some('\u{201D}'), // quotedblright + Some('\u{2018}'), // quoteleft + Some('\u{2019}'), // quoteright + Some('\u{00F7}'), // divide + Some('\u{25CA}'), // lozenge + Some('\u{00FF}'), // ydieresis + Some('\u{0178}'), // Ydieresis + Some('\u{2044}'), // fraction + Some('\u{00A4}'), // currency + Some('\u{2039}'), // guilsinglleft + Some('\u{203A}'), // guilsinglright + Some('\u{FB01}'), // fi + Some('\u{FB02}'), // fl + Some('\u{2021}'), // daggerdbl + Some('\u{00B7}'), // periodcentered + Some('\u{201A}'), // quotesinglbase + Some('\u{201E}'), // quotedblbase + Some('\u{2030}'), // perthousand + Some('\u{00C2}'), // Acircumflex + Some('\u{00CA}'), // Ecircumflex + Some('\u{00C1}'), // Aacute + Some('\u{00CB}'), // Edieresis + Some('\u{00C8}'), // Egrave + Some('\u{00CD}'), // Iacute + Some('\u{00CE}'), // Icircumflex + Some('\u{00CF}'), // Idieresis + Some('\u{00CC}'), // Igrave + Some('\u{00D3}'), // Oacute + Some('\u{00D4}'), // Ocircumflex + Some('\u{F8FF}'), // apple + Some('\u{00D2}'), // Ograve + Some('\u{00DA}'), // Uacute + Some('\u{00DB}'), // Ucircumflex + Some('\u{00D9}'), // Ugrave + Some('\u{0131}'), // dotlessi + Some('\u{02C6}'), // circumflex + Some('\u{02DC}'), // tilde + Some('\u{00AF}'), // macron + Some('\u{02D8}'), // breve + Some('\u{02D9}'), // dotaccent + Some('\u{02DA}'), // ring + Some('\u{00B8}'), // cedilla + Some('\u{02DD}'), // hungarumlaut + Some('\u{02DB}'), // ogonek + Some('\u{02C7}'), // caron + Some('\u{0141}'), // Lslash + Some('\u{0142}'), // lslash + Some('\u{0160}'), // Scaron + Some('\u{0161}'), // scaron + Some('\u{017D}'), // Zcaron + Some('\u{017E}'), // zcaron + Some('\u{00A6}'), // brokenbar + Some('\u{00D0}'), // Eth + Some('\u{00F0}'), // eth + Some('\u{00DD}'), // Yacute + Some('\u{00FD}'), // yacute + Some('\u{00DE}'), // Thorn + Some('\u{00FE}'), // thorn + Some('\u{2212}'), // minus + Some('\u{00D7}'), // multiply + Some('\u{00B9}'), // onesuperior + Some('\u{00B2}'), // twosuperior + Some('\u{00B3}'), // threesuperior + Some('\u{00BD}'), // onehalf + Some('\u{00BC}'), // onequarter + Some('\u{00BE}'), // threequarters + Some('\u{20A3}'), // franc + Some('\u{011E}'), // Gbreve + Some('\u{011F}'), // gbreve + Some('\u{0130}'), // Idotaccent + Some('\u{015E}'), // Scedilla + Some('\u{015F}'), // scedilla + Some('\u{0106}'), // Cacute + Some('\u{0107}'), // cacute + Some('\u{010C}'), // Ccaron + Some('\u{010D}'), // ccaron + Some('\u{0111}'), // dcroat +]; diff --git a/crates/pdf-font/src/lib.rs b/crates/pdf-font/src/lib.rs index 5d20ed64..f917b897 100644 --- a/crates/pdf-font/src/lib.rs +++ b/crates/pdf-font/src/lib.rs @@ -1,5 +1,6 @@ pub mod base_encoding; pub mod cff; +pub mod cid_to_unicode; pub mod encoding; pub mod error; pub mod fallback; diff --git a/crates/pdf-font/src/pdf_font_descriptor.rs b/crates/pdf-font/src/pdf_font_descriptor.rs index c8781a75..45297827 100644 --- a/crates/pdf-font/src/pdf_font_descriptor.rs +++ b/crates/pdf-font/src/pdf_font_descriptor.rs @@ -1,6 +1,7 @@ //! Best-effort font descriptor metadata and CID collection information. use crate::{ + cid_to_unicode::CidToUnicode, flags::FontFlags, font::{FontSlant, FontSource, FontWeight}, pdf::{CidSystemInfo, PdfFontDescriptor}, @@ -11,7 +12,7 @@ use pdf_graphics::rect::Rect; use pdf_object_reader::{ DictionaryContext, FromPdfObject, ObjectAccess, ObjectContext, ReadResult, }; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; /// Metadata and the program selected for a specific font subtype. struct Descriptor

{ @@ -119,7 +120,7 @@ impl FromPdfObject for CidSystemInfo { } /// Finds the existing collection-based Unicode fallback when the ordering is known. -pub(crate) fn cid_to_unicode_map(system_info: &CidSystemInfo) -> Option>> { +pub(crate) fn cid_to_unicode_map(system_info: &CidSystemInfo) -> Option { let cmap_name: &[u8] = match system_info.ordering.as_ref() { b"Japan1" => b"UniJIS-UCS2-HW-H", b"GB1" => b"UniGB-UCS2-H", @@ -130,5 +131,5 @@ pub(crate) fn cid_to_unicode_map(system_info: &CidSystemInfo) -> Option font.standard14.unwrap_or_default(), + PdfFontSpec::Type0(font) => { + from_base_font_name(&font.base_font).unwrap_or(Standard14Font::Helvetica) + } _ => Standard14Font::Helvetica, } } diff --git a/crates/pdf-font/src/standard14.rs b/crates/pdf-font/src/standard14.rs index e12fee9e..6e34d9fc 100644 --- a/crates/pdf-font/src/standard14.rs +++ b/crates/pdf-font/src/standard14.rs @@ -196,6 +196,13 @@ pub(crate) fn from_flags(flags: FontFlags) -> Standard14Font { } impl Standard14Font { + /// Returns whether the TrueType core font behind this identity, such as Arial for Helvetica, + /// keeps the standard Macintosh glyph order. The symbol fonts do not. + #[must_use] + pub fn has_standard_glyph_order(self) -> bool { + !matches!(self, Self::Symbol | Self::ZapfDingbats) + } + /// Returns the advance width, in 1/1000 em, of a named glyph in this font's AFM metrics. /// /// Courier is monospaced, so every glyph name is 600 units wide. Other fonts return `None` diff --git a/crates/pdf-font/src/type0_font_spec.rs b/crates/pdf-font/src/type0_font_spec.rs index 7103458e..e0463cb4 100644 --- a/crates/pdf-font/src/type0_font_spec.rs +++ b/crates/pdf-font/src/type0_font_spec.rs @@ -1,19 +1,21 @@ //! Composite Type 0 specifications and their CID descendant fonts. use crate::{ + cid_to_unicode::CidToUnicode, error::FontError, font::FontSource, pdf::{CidSystemInfo, PdfFontDescriptor, PdfMetrics, ToUnicodeMap}, pdf_font_descriptor::{cid_to_unicode_map, descriptor}, pdf_font_parser::{base_font, to_unicode}, pdf_font_program::{CidCffProgram, TrueTypeProgram}, + standard14::{Standard14Font, from_base_font_name}, }; use pdf_cmap::{PdfCMap, Type0EncodingCMap}; use pdf_object_reader::{ DictionaryContext, FromPdfObject, ObjectAccess, ObjectContext, ObjectReadError, ReadResult, object_variant::ObjectVariant, }; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; /// Descendant subtype used by a Type 0 composite font. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -39,8 +41,8 @@ pub struct CidFontSpec { pub metrics: PdfMetrics, /// Optional CID-to-glyph identifier mapping for CIDFontType2. pub cid_to_gid: Option>, - /// Best-effort CID-to-Unicode mapping for collection-backed font substitution. - pub cid_to_unicode: Option>>, + /// Best-effort CID-to-Unicode mapping for font substitution. + pub cid_to_unicode: Option, } /// Normalized Type 0 composite font. @@ -132,8 +134,17 @@ impl FromPdfObject for CidFontSpec { ordering: Arc::default(), supplement: 0, }); - let cid_to_unicode = cid_to_unicode_map(&system_info); let cid_to_gid = CidToGidMap::read(&mut context, kind); + // A substituted TrueType core font without its own glyph map is indexed by CID. + let indexes_core_font = kind == CidFontKind::Type2 + && program.is_none() + && cid_to_gid.is_none() + && base_font + .as_deref() + .and_then(from_base_font_name) + .is_some_and(Standard14Font::has_standard_glyph_order); + let cid_to_unicode = cid_to_unicode_map(&system_info) + .or_else(|| indexes_core_font.then_some(CidToUnicode::StandardGlyphOrder)); Ok(Self { kind, descriptor, diff --git a/crates/pdf-text-engine/src/pdf_text_decoder.rs b/crates/pdf-text-engine/src/pdf_text_decoder.rs index 3b7da9ef..0dbdbcc8 100644 --- a/crates/pdf-text-engine/src/pdf_text_decoder.rs +++ b/crates/pdf-text-engine/src/pdf_text_decoder.rs @@ -153,8 +153,7 @@ fn type0_unicode(font: &Type0FontSpec, mapping: CidMapping) -> UnicodeSequence { font.descendant .cid_to_unicode .as_ref()? - .get(&cid) - .copied() + .get(cid) .map(Into::into) }) .unwrap_or_default()