From 47aed15fba1f43e3db2c4984e1d95ba05ddfb278 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 2 Aug 2026 22:41:06 +0300 Subject: [PATCH] pdf-font: centralize font map and fallback parsing Move CID width parsing onto GlyphWidthsMap and use the shared ToUnicodeCMap dictionary parser across simple and fallback fonts. Inline fallback program selection into FallbackFontProgram and name the bundled CJK font data as an associated constant. Co-authored-by: Codex --- crates/pdf-cmap/src/to_unicode.rs | 9 ++++ crates/pdf-font/src/fallback.rs | 63 ++++++++----------------- crates/pdf-font/src/glyph_widths_map.rs | 19 +++++++- crates/pdf-font/src/true_type_font.rs | 7 +-- crates/pdf-font/src/type0_font.rs | 25 +--------- crates/pdf-font/src/type1_font.rs | 7 +-- crates/pdf-font/src/type3_font.rs | 7 +-- 7 files changed, 50 insertions(+), 87 deletions(-) diff --git a/crates/pdf-cmap/src/to_unicode.rs b/crates/pdf-cmap/src/to_unicode.rs index e0694766..cbca2b2d 100644 --- a/crates/pdf-cmap/src/to_unicode.rs +++ b/crates/pdf-cmap/src/to_unicode.rs @@ -10,6 +10,15 @@ pub struct ToUnicodeCMap(HashMap>); impl ToUnicodeCMap { /// Parse the optional `/ToUnicode` CMap from a font dictionary. + /// + /// # Paramaters + /// + /// - `dictionary`: The PDF font dictionary that may contain `/ToUnicode`. + /// - `objects`: The resolver used to dereference indirect PDF objects. + /// + /// # Returns + /// + /// The parsed ToUnicode CMap when a readable `/ToUnicode` stream is present. pub fn from_dictionary( dictionary: &Dictionary, objects: &dyn ObjectResolver, diff --git a/crates/pdf-font/src/fallback.rs b/crates/pdf-font/src/fallback.rs index 16514711..fa47cfd7 100644 --- a/crates/pdf-font/src/fallback.rs +++ b/crates/pdf-font/src/fallback.rs @@ -14,6 +14,8 @@ pub(crate) struct FallbackFontProgram { } impl FallbackFontProgram { + const NOTO_SANS_CJK_JP_REGULAR: &[u8] = include_bytes!("../assets/NotoSansCJKjp-Regular.otf"); + /// Select fallback font bytes and metadata for a font dictionary. pub(crate) fn from_dictionary( dictionary: &Dictionary, @@ -22,8 +24,17 @@ impl FallbackFontProgram { let flags = FontFlags::from_dictionary(dictionary, objects)?; let standard14 = Standard14Font::from_dictionary(dictionary, objects, flags); let is_cjk = is_cjk_cid_font(dictionary, objects)?; + let font_file = if is_cjk { + Self::NOTO_SANS_CJK_JP_REGULAR + } else { + standard14.fallback_font_bytes() + }; - Ok(fallback_program(flags, standard14, is_cjk)) + Ok(Self { + font_file, + standard14, + flags, + }) } } @@ -47,7 +58,7 @@ pub(crate) fn fallback_true_type_from_dictionary( let encoding = Encoding::from_dictionary(dictionary, objects) .ok() .flatten(); - let to_unicode = to_unicode_cmap(dictionary, objects)?; + let to_unicode = ToUnicodeCMap::from_dictionary(dictionary, objects)?; Ok(TrueTypeFont { font_file: fallback.font_file.into(), @@ -71,56 +82,20 @@ pub(crate) fn fallback_true_type_from_dictionary_best_effort( let flags = FontFlags::from_dictionary(dictionary, objects).unwrap_or_default(); let is_cjk = is_cjk_cid_font(dictionary, objects).unwrap_or(false); let standard14 = Standard14Font::from_dictionary(dictionary, objects, flags); - let fallback = fallback_program(flags, standard14, is_cjk); - let mut font = TrueTypeFont::from_bytes(fallback.font_file, Some(fallback.standard14)); - font.flags = fallback.flags; - - font -} - -/// Build the fallback font program descriptor from already-decided inputs. -/// -/// `standard14` selects the Standard 14 identity for simple-font fallback, -/// while `is_cjk` switches the program bytes to the bundled CJK fallback for -/// CID fonts that declare a supported CJK ordering. -fn fallback_program( - flags: FontFlags, - standard14: Standard14Font, - is_cjk: bool, -) -> FallbackFontProgram { let font_file = if is_cjk { - include_bytes!("../assets/NotoSansCJKjp-Regular.otf").as_slice() + FallbackFontProgram::NOTO_SANS_CJK_JP_REGULAR } else { standard14.fallback_font_bytes() }; - - FallbackFontProgram { + let fallback = FallbackFontProgram { font_file, standard14, flags, - } -} + }; + let mut font = TrueTypeFont::from_bytes(fallback.font_file, Some(fallback.standard14)); + font.flags = fallback.flags; -/// Parse an optional ToUnicode CMap from a font dictionary. -/// -/// # Paramaters -/// -/// - `dictionary`: The PDF font dictionary that may contain `/ToUnicode`. -/// - `objects`: The resolver used to dereference indirect PDF objects. -/// -/// # Returns -/// -/// The parsed ToUnicode CMap when a readable `/ToUnicode` stream is present. -fn to_unicode_cmap( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, -) -> Result, FontError> { - dictionary - .get("ToUnicode") - .and_then(|e| e.try_stream(objects).ok()) - .map(|s| ToUnicodeCMap::try_from(s.raw_data())) - .transpose() - .map_err(FontError::from) + font } /// Detect whether a CID font dictionary uses a known CJK CID ordering. diff --git a/crates/pdf-font/src/glyph_widths_map.rs b/crates/pdf-font/src/glyph_widths_map.rs index 6f883216..05e903a6 100644 --- a/crates/pdf-font/src/glyph_widths_map.rs +++ b/crates/pdf-font/src/glyph_widths_map.rs @@ -1,9 +1,12 @@ use pdf_object::{ - error::ObjectError, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, error::ObjectError, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use std::collections::BTreeMap; use thiserror::Error; +use crate::error::FontError; + /// Errors that can occur during GlyphWidthsMap parsing from a /W array. #[derive(Debug, Error, Clone, PartialEq)] pub enum GlyphWidthsMapError { @@ -58,6 +61,20 @@ pub struct GlyphWidthsMap { } impl GlyphWidthsMap { + /// Parse the optional `/W` width map from a descendant CIDFont dictionary. + pub fn from_dictionary( + dictionary: &Dictionary, + objects: &dyn ObjectResolver, + ) -> Result, FontError> { + dictionary + .get("W") + .map(|value| { + let widths = value.try_array(objects)?; + Self::from_array(widths, objects).map_err(FontError::from) + }) + .transpose() + } + /// Parses a PDF /W array into a `GlyphWidthsMap`. /// /// The /W array can contain entries of the form: diff --git a/crates/pdf-font/src/true_type_font.rs b/crates/pdf-font/src/true_type_font.rs index 5d7fe9d4..55c820b1 100644 --- a/crates/pdf-font/src/true_type_font.rs +++ b/crates/pdf-font/src/true_type_font.rs @@ -74,12 +74,7 @@ impl TrueTypeFont { .flatten() .or_else(|| Self::default_simple_encoding(program.flags, program.standard14)); - // Parse optional ToUnicode CMap stream. - let to_unicode = dictionary - .get("ToUnicode") - .and_then(|e| e.try_stream(objects).ok()) - .map(|s| ToUnicodeCMap::try_from(s.raw_data())) - .transpose()?; + let to_unicode = ToUnicodeCMap::from_dictionary(dictionary, objects)?; Ok(Self { font_file: program.font_file, diff --git a/crates/pdf-font/src/type0_font.rs b/crates/pdf-font/src/type0_font.rs index a4838d50..2408850a 100644 --- a/crates/pdf-font/src/type0_font.rs +++ b/crates/pdf-font/src/type0_font.rs @@ -170,7 +170,7 @@ impl<'a> Type0DescendantFont<'a> { dictionary, subtype: CidFontSubType::from_dictionary(dictionary, objects)?, default_width, - widths: widths_map(dictionary, objects)?, + widths: GlyphWidthsMap::from_dictionary(dictionary, objects)?, }) } } @@ -210,29 +210,6 @@ fn descendant_font_dictionary<'a>( .map_err(FontError::from) } -/// Parse explicit CID width overrides from a descendant font dictionary. -/// -/// # Paramaters -/// -/// - `dictionary`: The descendant CIDFont dictionary. -/// - `objects`: The resolver used to dereference indirect PDF objects. -/// -/// # Returns -/// -/// The parsed `/W` width map when the dictionary contains width overrides. -fn widths_map( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, -) -> Result, FontError> { - dictionary - .get("W") - .map(|obj| { - let widths = obj.try_array(objects)?; - GlyphWidthsMap::from_array(widths, objects).map_err(FontError::from) - }) - .transpose() -} - /// Read or synthesize the font program for a Type0 descendant font. /// /// # Paramaters diff --git a/crates/pdf-font/src/type1_font.rs b/crates/pdf-font/src/type1_font.rs index 364778fe..658fb66b 100644 --- a/crates/pdf-font/src/type1_font.rs +++ b/crates/pdf-font/src/type1_font.rs @@ -46,12 +46,7 @@ impl Type1Font { let encoding = Encoding::from_dictionary(dictionary, objects)?.unwrap_or_default(); - // Parse optional ToUnicode CMap stream. - let to_unicode = dictionary - .get("ToUnicode") - .and_then(|e| e.try_stream(objects).ok()) - .map(|s| ToUnicodeCMap::try_from(s.raw_data())) - .transpose()?; + let to_unicode = ToUnicodeCMap::from_dictionary(dictionary, objects)?; Ok(Self { font_file, diff --git a/crates/pdf-font/src/type3_font.rs b/crates/pdf-font/src/type3_font.rs index 82f082cd..90aa7f1d 100644 --- a/crates/pdf-font/src/type3_font.rs +++ b/crates/pdf-font/src/type3_font.rs @@ -58,12 +58,7 @@ impl Type3Font { char_procs.insert(name.to_owned(), content_stream); } - // Parse optional ToUnicode CMap stream. - let to_unicode = dictionary - .get("ToUnicode") - .and_then(|e| e.try_stream(objects).ok()) - .map(|s| ToUnicodeCMap::try_from(s.raw_data())) - .transpose()?; + let to_unicode = ToUnicodeCMap::from_dictionary(dictionary, objects)?; Ok(Type3Font { font_matrix,