From cc9b94b632e78e59c03571b894a34383b2b20da9 Mon Sep 17 00:00:00 2001 From: Velli20 Date: Tue, 13 Oct 2026 21:30:27 +0300 Subject: [PATCH 1/2] 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() From b2b2348b18b2cd443b4276434446f18f9a8a494c Mon Sep 17 00:00:00 2001 From: Velli20 Date: Sun, 25 Oct 2026 17:18:10 +0200 Subject: [PATCH 2/2] Map standard glyph order through read-fonts tables Replace the hand-written 258-entry Unicode table with read-fonts' DEFAULT_GLYPH_NAMES and its Adobe Glyph List lookup, and move CidToUnicode next to CidToGidMap in type0_font_spec.rs. Co-Authored-By: Claude Opus 5.5 --- crates/pdf-font/src/cid_to_unicode.rs | 292 ------------------ crates/pdf-font/src/lib.rs | 3 +- crates/pdf-font/src/pdf.rs | 4 +- crates/pdf-font/src/pdf_font_descriptor.rs | 8 +- crates/pdf-font/src/type0_font_spec.rs | 31 +- .../pdf-text-engine/src/pdf_text_decoder.rs | 2 +- 6 files changed, 39 insertions(+), 301 deletions(-) delete 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 deleted file mode 100644 index f590677f..00000000 --- a/crates/pdf-font/src/cid_to_unicode.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! 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 c420812e..2997d757 100644 --- a/crates/pdf-font/src/lib.rs +++ b/crates/pdf-font/src/lib.rs @@ -1,6 +1,5 @@ pub mod base_encoding; pub mod cff; -pub mod cid_to_unicode; pub mod encoding; pub mod error; pub mod fallback; @@ -39,7 +38,7 @@ pub use font::{ }; pub use font_registry::FontRegistry; pub use pdf::{ - CharProcedures, CidFontKind, CidFontSpec, CidSystemInfo, CidToGidMap, + CharProcedures, CidFontKind, CidFontSpec, CidSystemInfo, CidToGidMap, CidToUnicode, PDF_GLYPH_SPACE_UNITS_PER_EM, PdfCode, PdfFontDescriptor, PdfMetrics, SimpleEncoding, SimpleFontSpec, ToUnicodeMap, Type0FontSpec, Type3FontSpec, }; diff --git a/crates/pdf-font/src/pdf.rs b/crates/pdf-font/src/pdf.rs index 8e80b141..d1522ffc 100644 --- a/crates/pdf-font/src/pdf.rs +++ b/crates/pdf-font/src/pdf.rs @@ -11,7 +11,9 @@ use crate::glyph_widths_map::GlyphWidthsMap; pub use crate::pdf_font_spec::PdfFontSpec; pub use crate::simple_font_spec::SimpleFontSpec; pub use crate::standard14::Standard14Font; -pub use crate::type0_font_spec::{CidFontKind, CidFontSpec, CidToGidMap, Type0FontSpec}; +pub use crate::type0_font_spec::{ + CidFontKind, CidFontSpec, CidToGidMap, CidToUnicode, Type0FontSpec, +}; pub use crate::type3_font_spec::{CharProcedures, Type3FontSpec}; pub use pdf_cmap::{PdfCode, ToUnicodeMap}; diff --git a/crates/pdf-font/src/pdf_font_descriptor.rs b/crates/pdf-font/src/pdf_font_descriptor.rs index db88027d..049be1e7 100644 --- a/crates/pdf-font/src/pdf_font_descriptor.rs +++ b/crates/pdf-font/src/pdf_font_descriptor.rs @@ -1,8 +1,8 @@ //! Best-effort font descriptor metadata and CID collection information. use crate::{ - cid_to_unicode::CidToUnicode, pdf::{CidSystemInfo, PdfFontDescriptor}, + type0_font_spec::CidToUnicode, }; use pdf_cmap::predefined::PredefinedCMap; use pdf_object_reader::{FromPdfObject, ObjectAccess, ObjectContext, ReadResult}; @@ -47,7 +47,7 @@ impl FromPdfObject for CidSystemInfo { } impl CidSystemInfo { - /// Finds the existing collection-based Unicode fallback when the ordering is known. + /// Finds the collection-based Unicode fallback when the ordering is known. pub(crate) fn cid_to_unicode_map(&self) -> Option { let cmap_name: &[u8] = match self.ordering.as_ref() { b"Japan1" => b"UniJIS-UCS2-HW-H", @@ -57,6 +57,8 @@ impl CidSystemInfo { _ => return None, }; let cmap = PredefinedCMap::from_name(cmap_name).ok()??; - Some(CidToUnicode::Collection(Arc::new(cmap.cid_to_unicode_map()))) + Some(CidToUnicode::Collection(Arc::new( + cmap.cid_to_unicode_map(), + ))) } } diff --git a/crates/pdf-font/src/type0_font_spec.rs b/crates/pdf-font/src/type0_font_spec.rs index d71e6d49..6e6156ec 100644 --- a/crates/pdf-font/src/type0_font_spec.rs +++ b/crates/pdf-font/src/type0_font_spec.rs @@ -1,7 +1,6 @@ //! Composite Type 0 specifications and their CID descendant fonts. use crate::{ - cid_to_unicode::CidToUnicode, error::FontError, font::{FontSource, GlyphId}, pdf::{CidSystemInfo, PdfFontDescriptor, PdfMetrics, ToUnicodeMap}, @@ -12,7 +11,8 @@ use pdf_cmap::{Cid, PdfCMap, Type0EncodingCMap}; use pdf_object_reader::{ FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_variant::ObjectVariant, }; -use std::sync::Arc; +use read_fonts::{ps::agl, tables::post::DEFAULT_GLYPH_NAMES}; +use std::{collections::HashMap, sync::Arc}; /// Descendant subtype used by a Type 0 composite font. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -77,6 +77,33 @@ impl CidToGidMap { } } +/// 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 => DEFAULT_GLYPH_NAMES + .get(usize::from(cid)) + .copied() + .and_then(agl::name_to_char), + } + } +} + impl FromPdfObject for CidToGidMap { /// Decodes a stream into its `u16` table, ignoring a trailing odd byte. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { diff --git a/crates/pdf-text-engine/src/pdf_text_decoder.rs b/crates/pdf-text-engine/src/pdf_text_decoder.rs index 8ba9b013..08f4be75 100644 --- a/crates/pdf-text-engine/src/pdf_text_decoder.rs +++ b/crates/pdf-text-engine/src/pdf_text_decoder.rs @@ -135,7 +135,7 @@ fn type0_selector(descendant: &CidFontSpec, cid: Cid) -> GlyphSelector<'static> /// Resolves extraction Unicode for one composite-font mapping. /// -/// The source-code `/ToUnicode` map is authoritative. A best-effort collection CID map is used only +/// The source-code `/ToUnicode` map is authoritative. The descendant's best-effort CID map is used only /// when `/ToUnicode` has no entry and the CID fits that map's 16-bit key space. fn type0_unicode(font: &Type0FontSpec, mapping: CidMapping) -> UnicodeSequence { font.to_unicode