From 83627de3ceab28dae08169a16c2262fb32d9181e Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 4 Aug 2026 22:04:48 +0300 Subject: [PATCH] pdf-font: centralize whole-font fallback Make Font::from_dictionary infallible and route every subtype parsing failure through one bundled TrueType fallback. Remove partial fallback program construction from TrueType and Type0 readers. Discard PDF widths, encoding, ToUnicode, and flags for substituted fonts so glyph lookup and advance widths come from the bundled cmap and hmtx tables. Limit nested font resources to parsed Type3 fonts. Co-authored-by: Codex --- crates/pdf-font/src/cid_system_info.rs | 17 +- crates/pdf-font/src/fallback.rs | 116 ++++++------- crates/pdf-font/src/font.rs | 55 ++++--- crates/pdf-font/src/true_type_font.rs | 67 +++----- crates/pdf-font/src/type0_font.rs | 210 ++---------------------- crates/pdf-font/src/type1_font.rs | 2 +- crates/pdf-resources/src/resources.rs | 23 +-- crates/pdf-resources/tests/resources.rs | 38 +++++ 8 files changed, 163 insertions(+), 365 deletions(-) diff --git a/crates/pdf-font/src/cid_system_info.rs b/crates/pdf-font/src/cid_system_info.rs index 022b8f75..59a0e287 100644 --- a/crates/pdf-font/src/cid_system_info.rs +++ b/crates/pdf-font/src/cid_system_info.rs @@ -1,13 +1,10 @@ -use std::collections::HashMap; - -use pdf_cmap::{error::CMapError, predefined::PredefinedCMap}; use pdf_object::{ dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, }; use crate::error::FontError; -/// Known Adobe CIDSystemInfo ordering values with bundled Unicode CMap support. +/// Known Adobe CIDSystemInfo ordering values with bundled CJK font support. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CidOrdering { /// Adobe-Japan1 character collection. @@ -48,18 +45,6 @@ impl CidOrdering { Ok(Self::from_name(ordering)) } - /// Build a best-effort CID to Unicode map for this ordering. - pub(crate) fn cid_to_unicode_map(self) -> Result>, CMapError> { - let unicode_cmap_name = match self { - Self::Japan1 => "UniJIS-UCS2-HW-H", - Self::GB1 => "UniGB-UCS2-H", - Self::CNS1 => "UniCNS-UCS2-H", - Self::Korea1 => "UniKS-UCS2-H", - }; - - Ok(PredefinedCMap::from_name(unicode_cmap_name)?.map(|cmap| cmap.cid_to_unicode_map())) - } - fn from_name(name: &str) -> Option { match name { "Japan1" => Some(Self::Japan1), diff --git a/crates/pdf-font/src/fallback.rs b/crates/pdf-font/src/fallback.rs index ae6e1247..79341fe2 100644 --- a/crates/pdf-font/src/fallback.rs +++ b/crates/pdf-font/src/fallback.rs @@ -1,9 +1,7 @@ -use pdf_cmap::ToUnicodeCMap; use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; use crate::{ - cid_system_info::CidOrdering, encoding::Encoding, flags::FontFlags, - simple_font_glyph_map::SimpleFontGlyphWidthsMap, standard14::Standard14Font, + cid_system_info::CidOrdering, flags::FontFlags, standard14::Standard14Font, true_type_font::TrueTypeFont, }; @@ -13,43 +11,40 @@ const NOTO_SANS_CJK_JP_REGULAR: &[u8] = include_bytes!("../assets/NotoSansCJKjp- /// /// # Paramaters /// -/// - `dictionary`: The PDF font dictionary used to derive fallback metrics and metadata. +/// - `dictionary`: The PDF font dictionary used to select a bundled font program. /// - `objects`: The resolver used to dereference indirect PDF objects. /// /// # Returns /// -/// A [`TrueTypeFont`] backed by fallback font bytes, simple font widths, -/// optional encoding, optional ToUnicode data, and descriptor flags. Each -/// metadata field is parsed independently and ignored when malformed. +/// A [`TrueTypeFont`] backed only by bundled fallback font data. PDF widths, +/// encoding, ToUnicode data, and descriptor flags are intentionally discarded. pub(crate) fn fallback_true_type_from_dictionary( dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> TrueTypeFont { - let flags = FontFlags::from_dictionary(dictionary, objects).unwrap_or_default(); - let standard14 = Standard14Font::from_dictionary(dictionary, objects, flags); - let font_file = if is_cjk_cid_font(dictionary, objects) { + let metadata = fallback_metadata_dictionary(dictionary, objects); + let flags = FontFlags::from_dictionary(metadata, objects).unwrap_or_default(); + let standard14 = Standard14Font::from_dictionary(metadata, objects, flags); + let font_file = if is_cjk_cid_font(metadata, objects) { NOTO_SANS_CJK_JP_REGULAR } else { standard14.fallback_font_bytes() }; - let widths = SimpleFontGlyphWidthsMap::from_dictionary(dictionary, objects) - .ok() - .flatten(); - let encoding = Encoding::from_dictionary(dictionary, objects) - .ok() - .flatten(); - let to_unicode = ToUnicodeCMap::from_dictionary(dictionary, objects) - .ok() - .flatten(); - let mut font = TrueTypeFont::from_bytes(font_file, Some(standard14)); - font.widths = widths; - if encoding.is_some() { - font.encoding = encoding; - } - font.to_unicode = to_unicode; - font.flags = flags; - font + TrueTypeFont::from_bytes(font_file, Some(standard14)) +} + +/// Select metadata from a Type0 descendant when one is readable. +fn fallback_metadata_dictionary<'a>( + dictionary: &'a Dictionary, + objects: &'a dyn ObjectResolver, +) -> &'a Dictionary { + dictionary + .get("DescendantFonts") + .and_then(|value| value.try_array(objects).ok()) + .and_then(|descendants| descendants.first()) + .and_then(|descendant| descendant.try_dictionary(objects).ok()) + .unwrap_or(dictionary) } /// Detect whether a CID font dictionary uses a known CJK CID ordering. @@ -75,70 +70,51 @@ fn is_cjk_cid_font(dictionary: &Dictionary, objects: &dyn ObjectResolver) -> boo mod tests { use std::collections::BTreeMap; - use pdf_object::{ - object_resolver::PassthroughResolver, object_variant::ObjectVariant, stream::StreamObject, - }; + use pdf_object::{object_resolver::PassthroughResolver, object_variant::ObjectVariant}; use super::*; #[test] - fn fallback_salvages_valid_metadata_independently() { - let to_unicode = ObjectVariant::Stream(StreamObject::new( - 1, - 0, - Box::new(Dictionary::new(BTreeMap::new())), - b"beginbfchar\n<41> <0042>\nendbfchar\n".to_vec(), - )); + fn fallback_discards_pdf_font_metadata() { + let descriptor = Dictionary::new(BTreeMap::from([( + "Flags".to_string(), + ObjectVariant::Integer(i64::from(FontFlags::SYMBOLIC.bits())), + )])); let dictionary = Dictionary::new(BTreeMap::from([ ( "BaseFont".to_string(), ObjectVariant::Name(b"Helvetica-Bold".to_vec()), ), - ("FontDescriptor".to_string(), ObjectVariant::Integer(1)), + ( + "FontDescriptor".to_string(), + ObjectVariant::Dictionary(Box::new(descriptor)), + ), ("FirstChar".to_string(), ObjectVariant::Integer(65)), ("LastChar".to_string(), ObjectVariant::Integer(65)), ( "Widths".to_string(), ObjectVariant::Array(vec![ObjectVariant::Integer(625)]), ), - ("Encoding".to_string(), ObjectVariant::Integer(1)), - ("ToUnicode".to_string(), to_unicode), + ( + "Encoding".to_string(), + ObjectVariant::Name(b"WinAnsiEncoding".to_vec()), + ), + ("ToUnicode".to_string(), ObjectVariant::Integer(1)), ])); let font = fallback_true_type_from_dictionary(&dictionary, &PassthroughResolver); assert_eq!(font.standard14, Some(Standard14Font::HelveticaBold)); assert!(font.flags.is_empty()); - assert_eq!( - font.widths.as_ref().and_then(|widths| widths.get(&65)), - Some(&625.0) - ); - assert_eq!( - font.encoding - .as_ref() - .and_then(|encoding| encoding.names.get(65)) - .map(std::borrow::Cow::as_ref), - Some("A") - ); - assert_eq!( - font.to_unicode - .as_ref() - .and_then(|cmap| cmap.map_char_code(0x41)), - Some(['B'].as_slice()) - ); + assert!(font.widths.is_none()); + assert!(font.encoding.is_none()); + assert!(font.to_unicode.is_none()); } #[test] - fn fallback_ignores_malformed_widths_to_unicode_and_cid_info() { - let malformed_to_unicode = ObjectVariant::Stream(StreamObject::new( - 1, - 0, - Box::new(Dictionary::new(BTreeMap::new())), - b">".to_vec(), - )); + fn fallback_tolerates_malformed_selection_metadata() { let dictionary = Dictionary::new(BTreeMap::from([ - ("Widths".to_string(), ObjectVariant::Integer(1)), - ("ToUnicode".to_string(), malformed_to_unicode), + ("FontDescriptor".to_string(), ObjectVariant::Integer(1)), ("CIDSystemInfo".to_string(), ObjectVariant::Integer(1)), ])); @@ -154,14 +130,18 @@ mod tests { } #[test] - fn fallback_uses_cjk_program_for_known_cid_ordering() { - let dictionary = Dictionary::new(BTreeMap::from([( + fn fallback_uses_cjk_program_from_type0_descendant() { + let descendant = Dictionary::new(BTreeMap::from([( "CIDSystemInfo".to_string(), ObjectVariant::Dictionary(Box::new(Dictionary::new(BTreeMap::from([( "Ordering".to_string(), ObjectVariant::LiteralString(b"Japan1".to_vec()), )])))), )])); + let dictionary = Dictionary::new(BTreeMap::from([( + "DescendantFonts".to_string(), + ObjectVariant::Array(vec![ObjectVariant::Dictionary(Box::new(descendant))]), + )])); let fallback = fallback_true_type_from_dictionary(&dictionary, &PassthroughResolver); diff --git a/crates/pdf-font/src/font.rs b/crates/pdf-font/src/font.rs index 4645a8d1..32674681 100644 --- a/crates/pdf-font/src/font.rs +++ b/crates/pdf-font/src/font.rs @@ -30,10 +30,23 @@ pub enum Font { impl Font { pub const KEY: &'static str = "Font"; + /// Parse a font dictionary, replacing any unreadable font with a bundled + /// whole-font TrueType fallback. pub fn from_dictionary( dictionary: &Dictionary, objects: &dyn ObjectResolver, id_allocator: &mut ContentStreamIdAllocator, + ) -> Font { + match Self::try_from_dictionary(dictionary, objects, id_allocator) { + Ok(font) => font, + Err(_) => Font::TrueType(fallback_true_type_from_dictionary(dictionary, objects)), + } + } + + fn try_from_dictionary( + dictionary: &Dictionary, + objects: &dyn ObjectResolver, + id_allocator: &mut ContentStreamIdAllocator, ) -> Result { // Determine the font subtype from the dictionary. let subtype = dictionary.required_str("Subtype", objects)?; @@ -42,13 +55,7 @@ impl Font { let type0_font = Type0Font::from_dictionary(dictionary, objects)?; Ok(Font::Type0(type0_font)) } - "Type1" => match Type1Font::from_dictionary(dictionary, objects) { - Err(FontError::MissingFontFile) => Ok(Font::TrueType( - fallback_true_type_from_dictionary(dictionary, objects), - )), - Ok(type1_font) => Ok(Font::Type1(type1_font)), - Err(e) => Err(e), - }, + "Type1" => Type1Font::from_dictionary(dictionary, objects).map(Font::Type1), "Type3" => { let type3_font = Type3Font::from_dictionary(dictionary, objects, id_allocator)?; Ok(Font::Type3(type3_font)) @@ -62,23 +69,6 @@ impl Font { }), } } - - /// Build a Standard 14-backed fallback font for best-effort resource - /// recovery. - /// - /// Valid `/Widths`, `/Encoding`, and `/ToUnicode` entries are retained - /// independently. Malformed metadata is treated as absent so an unreadable - /// font cannot prevent the rest of the resource dictionary from loading. - /// - /// Callers should use this only at higher-level recovery boundaries, such - /// as page resource loading, where replacing an unreadable font is better - /// than aborting the entire resource dictionary. - pub fn fallback_from_dictionary_best_effort( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, - ) -> Self { - Self::TrueType(fallback_true_type_from_dictionary(dictionary, objects)) - } } impl Font { @@ -183,7 +173,8 @@ impl Font { /// 1. ToUnicode CMap — returns the full slice (handles ligatures such as "fi" /// mapped to `['f','i']`). /// 2. Glyph name → Adobe Glyph List (Type1 / Type3 / TrueType with encodings). - /// 3. Type0/CID reverse-cmap fallback (Identity-H/V fonts without ToUnicode). + /// 3. Bundled fallback font cmap. + /// 4. Type0/CID reverse-cmap fallback (Identity-H/V fonts without ToUnicode). /// /// Returns an empty [`CharVec`] when no mapping is found. pub fn chars_to_unicode(&self, char_code: u16) -> CharVec { @@ -207,7 +198,19 @@ impl Font { return CharVec::from(c); } - // Priority 3: Type0 reverse-cmap (Identity-H/V without ToUnicode) + // Priority 3: bundled fallback font cmap. + if let Font::TrueType(font) = self + && font.standard14.is_some() + && let Some(c) = char::from_u32(u32::from(char_code)) + && FontRef::new(font.font_file.as_ref()) + .ok() + .and_then(|font_ref| font_ref.charmap().map(c)) + .is_some() + { + return CharVec::from(c); + } + + // Priority 4: Type0 reverse-cmap (Identity-H/V without ToUnicode) if let Font::Type0(f) = self && let Some(map) = &f.glyph_to_unicode && let Some(&c) = map.get(&char_code) diff --git a/crates/pdf-font/src/true_type_font.rs b/crates/pdf-font/src/true_type_font.rs index 2ac5057b..f9caf6da 100644 --- a/crates/pdf-font/src/true_type_font.rs +++ b/crates/pdf-font/src/true_type_font.rs @@ -6,9 +6,8 @@ use pdf_object::{ }; use crate::{ - encoding::Encoding, error::FontError, fallback::fallback_true_type_from_dictionary, - flags::FontFlags, font_data::FontData, simple_font_glyph_map::SimpleFontGlyphWidthsMap, - standard14::Standard14Font, + encoding::Encoding, error::FontError, flags::FontFlags, font_data::FontData, + simple_font_glyph_map::SimpleFontGlyphWidthsMap, standard14::Standard14Font, }; /// A TrueType font parsed from a PDF font dictionary. @@ -34,7 +33,6 @@ pub struct TrueTypeFont { pub(crate) struct TrueTypeFontProgram { pub(crate) font_file: FontData, - standard14: Option, flags: FontFlags, } @@ -55,8 +53,9 @@ impl TrueTypeFont { /// Parses a TrueType font from a PDF font dictionary. /// - /// Reads the embedded font program (or falls back to a bundled substitute), - /// optional `/Widths`, `/Encoding`, and `/ToUnicode` entries. + /// Reads the embedded font program and optional `/Widths`, `/Encoding`, and + /// `/ToUnicode` entries. Missing or unreadable programs remain errors so + /// [`Font`](crate::font::Font) can apply whole-font fallback consistently. pub fn from_dictionary( dictionary: &Dictionary, objects: &dyn ObjectResolver, @@ -72,7 +71,7 @@ impl TrueTypeFont { let encoding = Encoding::from_dictionary(dictionary, objects) .ok() .flatten() - .or_else(|| Self::default_simple_encoding(program.flags, program.standard14)); + .or_else(|| Self::default_simple_encoding(program.flags, None)); let to_unicode = ToUnicodeCMap::from_dictionary(dictionary, objects)?; @@ -81,23 +80,18 @@ impl TrueTypeFont { widths, encoding, to_unicode, - standard14: program.standard14, + standard14: None, flags: program.flags, }) } - /// Creates a minimal `TrueTypeFont` from raw font bytes with no - /// widths or ToUnicode map. - /// - /// Used for Standard 14 fallback fonts where the bundled bytes have static - /// storage duration. Those fallback fonts behave like simple Type 1 fonts, - /// so they default to StandardEncoding when the PDF omitted an explicit - /// `/Encoding`. + /// Creates a minimal `TrueTypeFont` from raw font bytes without PDF width, + /// encoding, ToUnicode, or descriptor metadata. pub fn from_bytes(font_file: &'static [u8], standard14: Option) -> Self { Self { font_file: font_file.into(), widths: None, - encoding: Self::default_simple_encoding(FontFlags::empty(), standard14), + encoding: None, to_unicode: None, standard14, flags: FontFlags::empty(), @@ -127,8 +121,7 @@ impl TrueTypeFont { /// /// This method expects the full font dictionary, looks up its `/FontDescriptor` /// entry, and then attempts to read the font data from the descriptor's - /// `FontFile2` stream entry. If no embedded font is present, it falls back to a - /// bundled built-in TrueType font. + /// `FontFile2` stream entry. /// /// # Parameters /// @@ -137,29 +130,24 @@ impl TrueTypeFont { /// /// # Returns /// - /// Returns the resolved font program and its fallback metadata, or a - /// [`FontError`] if reading the font dictionary or stream fails. + /// Returns the embedded font program or a [`FontError`] if the descriptor + /// or `FontFile2` stream is missing or unreadable. pub(crate) fn read_font_file( dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { let flags = FontFlags::from_dictionary(dictionary, objects)?; - if let Some(descriptor) = dictionary.optional_dictionary("FontDescriptor", objects)? { - if let Some(stream) = descriptor.optional_stream("FontFile2", objects)? { - return Ok(TrueTypeFontProgram { - font_file: FontData::shared(stream.shared_data()), - standard14: None, - flags, - }); - } - } + let descriptor = dictionary + .optional_dictionary("FontDescriptor", objects)? + .ok_or(FontError::MissingFontFile)?; + let stream = descriptor + .optional_stream("FontFile2", objects)? + .ok_or(FontError::MissingFontFile)?; - let fallback = fallback_true_type_from_dictionary(dictionary, objects); Ok(TrueTypeFontProgram { - font_file: fallback.font_file, - standard14: fallback.standard14, - flags: fallback.flags, + font_file: FontData::shared(stream.shared_data()), + flags, }) } } @@ -210,19 +198,16 @@ mod tests { } #[test] - fn standard14_fallback_fonts_default_to_standard_encoding() { + fn raw_font_bytes_do_not_install_pdf_encoding_metadata() { let font = TrueTypeFont::from_bytes( Standard14Font::Helvetica.fallback_font_bytes(), Some(Standard14Font::Helvetica), ); - assert_eq!( - font.encoding - .as_ref() - .and_then(|encoding| encoding.names.get(65)) - .map(std::borrow::Cow::as_ref), - Some("A"), - ); + assert!(font.widths.is_none()); + assert!(font.encoding.is_none()); + assert!(font.to_unicode.is_none()); + assert!(font.flags.is_empty()); } #[test] diff --git a/crates/pdf-font/src/type0_font.rs b/crates/pdf-font/src/type0_font.rs index 2bbf8caa..eeaa7bc6 100644 --- a/crates/pdf-font/src/type0_font.rs +++ b/crates/pdf-font/src/type0_font.rs @@ -9,9 +9,7 @@ use read_fonts::{FontRef, TableProvider}; pub use crate::cid_font_subtype::CidFontSubType; use crate::{ - cid_system_info::CidOrdering, error::FontError, - fallback::fallback_true_type_from_dictionary, font_data::FontData, glyph_widths_map::GlyphWidthsMap, true_type_font::TrueTypeFont, @@ -86,10 +84,8 @@ impl Type0Font { let Type0FontProgram { font_file, program_format, - fallback_cid_to_unicode, } = read_type0_font_program(descendant.dictionary, descendant.subtype, objects)?; let glyph_to_unicode = glyph_to_unicode_map( - fallback_cid_to_unicode, font_file.as_ref(), descendant.subtype, encoding.as_ref(), @@ -178,7 +174,6 @@ impl<'a> Type0DescendantFont<'a> { struct Type0FontProgram { font_file: FontData, program_format: Type0FontProgramFormat, - fallback_cid_to_unicode: Option>, } /// Resolve the sole descendant CIDFont dictionary from `/DescendantFonts`. @@ -210,7 +205,7 @@ fn descendant_font_dictionary<'a>( .map_err(FontError::from) } -/// Read or synthesize the font program for a Type0 descendant font. +/// Read the embedded font program for a Type0 descendant font. /// /// # Paramaters /// @@ -220,8 +215,7 @@ fn descendant_font_dictionary<'a>( /// /// # Returns /// -/// The resolved font bytes, rendering program format, and any synthetic -/// CID-to-Unicode fallback map. +/// The resolved font bytes and rendering program format. fn read_type0_font_program( dictionary: &Dictionary, subtype: CidFontSubType, @@ -234,7 +228,6 @@ fn read_type0_font_program( program_format: Type0FontProgramFormat::TrueType { cid_to_unicode: false, }, - fallback_cid_to_unicode: None, }), } } @@ -248,8 +241,8 @@ fn read_type0_font_program( /// /// # Returns /// -/// An OpenType/CFF program when embedded, or a synthesized TrueType fallback -/// program when the embedded font file is missing. +/// An embedded OpenType/CFF program. Missing programs remain errors so the +/// owning [`Font`](crate::font::Font) can apply whole-font fallback. fn read_cid_font_type0_program( dictionary: &Dictionary, objects: &dyn ObjectResolver, @@ -258,49 +251,19 @@ fn read_cid_font_type0_program( Ok((font_file, Type1FontProgramFormat::OpenTypeCff)) => Ok(Type0FontProgram { font_file, program_format: Type0FontProgramFormat::OpenTypeCff, - fallback_cid_to_unicode: None, }), Ok((_, Type1FontProgramFormat::ClassicType1)) => Err(FontError::UnsupportedFontSubtype { subtype: "FontFile".to_string(), }), - Err(FontError::MissingFontFile) => Ok(fallback_type0_program(dictionary, objects)), Err(err) => Err(err), } } -/// Build the fallback program used when a CIDFontType0 has no embedded font. +/// Select the Unicode map for decoded Type0 CIDs. /// /// # Paramaters /// -/// - `dictionary`: The descendant CIDFont dictionary. -/// - `objects`: The resolver used to dereference indirect PDF objects. -/// -/// # Returns -/// -/// A synthetic TrueType-backed Type0 program plus an optional CID-to-Unicode -/// map derived from the CIDSystemInfo ordering. -fn fallback_type0_program( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, -) -> Type0FontProgram { - let fallback = fallback_true_type_from_dictionary(dictionary, objects); - let fallback_cid_to_unicode = cid_to_unicode_map(dictionary, objects).ok().flatten(); - - Type0FontProgram { - font_file: fallback.font_file, - program_format: Type0FontProgramFormat::TrueType { - cid_to_unicode: fallback_cid_to_unicode.is_some(), - }, - fallback_cid_to_unicode, - } -} - -/// Select the Unicode fallback map for decoded Type0 CIDs. -/// -/// # Paramaters -/// -/// - `fallback_cid_to_unicode`: A synthetic fallback map from CJK CID ordering. -/// - `font_file`: The resolved embedded or fallback font bytes. +/// - `font_file`: The resolved embedded font bytes. /// - `subtype`: The parsed CIDFont subtype. /// - `encoding`: The parsed Type0 encoding CMap, when present. /// - `to_unicode`: The parsed ToUnicode CMap, when present. @@ -310,16 +273,11 @@ fn fallback_type0_program( /// A CID/glyph-to-Unicode map when one can be built without overriding an /// explicit ToUnicode CMap. fn glyph_to_unicode_map( - fallback_cid_to_unicode: Option>, font_file: &[u8], subtype: CidFontSubType, encoding: Option<&Type0EncodingCMap>, to_unicode: Option<&ToUnicodeCMap>, ) -> Option> { - if fallback_cid_to_unicode.is_some() { - return fallback_cid_to_unicode; - } - let is_identity_encoding = encoding .map(Type0EncodingCMap::is_identity) .unwrap_or(false); @@ -363,40 +321,16 @@ fn build_glyph_to_unicode(font_data: &[u8]) -> Option> { if map.is_empty() { None } else { Some(map) } } -/// Build a fallback CID-to-Unicode map from a descendant font's CID ordering. -/// -/// # Paramaters -/// -/// - `descendant_font`: The descendant CIDFont dictionary. -/// - `objects`: The resolver used to dereference indirect PDF objects. -/// -/// # Returns -/// -/// A CID-to-Unicode map for known CJK orderings, or `None` when the ordering is -/// absent or unsupported. -fn cid_to_unicode_map( - descendant_font: &Dictionary, - objects: &dyn ObjectResolver, -) -> Result>, FontError> { - let Some(ordering) = CidOrdering::from_dictionary(descendant_font, objects)? else { - return Ok(None); - }; - - Ok(ordering.cid_to_unicode_map()?) -} - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use std::collections::BTreeMap; + use super::*; use pdf_object::{ dictionary::Dictionary, object_resolver::PassthroughResolver, object_variant::ObjectVariant, stream::StreamObject, }; - use read_fonts::TableProvider; - - use super::*; fn make_stream_object( object_number: usize, @@ -565,7 +499,7 @@ mod tests { } #[test] - fn cid_font_type0_missing_font_file_uses_truetype_fallback() { + fn cid_font_type0_missing_font_file_is_an_error() { let mut descriptor_dict = BTreeMap::new(); descriptor_dict.insert("Flags".to_string(), ObjectVariant::Integer(0)); @@ -606,25 +540,13 @@ mod tests { )))]), ); - let font = - Type0Font::from_dictionary(&Dictionary::new(font_dict), &PassthroughResolver).unwrap(); + let result = Type0Font::from_dictionary(&Dictionary::new(font_dict), &PassthroughResolver); - assert_eq!(font.subtype, CidFontSubType::Type0); - assert_eq!( - font.program_format, - Type0FontProgramFormat::TrueType { - cid_to_unicode: true - } - ); - assert!(!font.font_file.is_empty()); - assert_eq!( - font.decode_bytes_to_cids(&[0x00, 0x41, 0x12, 0x34, 0xFF]), - vec![65, 0x1234, 0] - ); + assert!(matches!(result, Err(FontError::MissingFontFile))); } #[test] - fn cid_font_type0_fallback_ignores_malformed_cid_system_info() { + fn cid_font_type0_missing_program_does_not_parse_cid_fallback_metadata() { let descendant = Dictionary::new(BTreeMap::from([ ( "Subtype".to_string(), @@ -647,114 +569,8 @@ mod tests { ), ])); - let font = Type0Font::from_dictionary(&dictionary, &PassthroughResolver).unwrap(); - - assert_eq!( - font.program_format, - Type0FontProgramFormat::TrueType { - cid_to_unicode: false - } - ); - assert!(font.glyph_to_unicode.is_none()); - assert!(!font.font_file.is_empty()); - } - - #[test] - fn cid_font_type0_cjk_fallback_decodes_issue_13343_text() { - let font = issue_13343_font(); - - assert_eq!( - issue_13343_text_to_unicode( - &font, - &[ - 0x28, 0x35, 0x37, 0x29, 0x81, 0x79, 0x97, 0x76, 0x96, 0xF1, 0x81, 0x7A, - ], - ), - "(57)\u{3010}\u{8981}\u{7d04}\u{3011}" - ); - assert_eq!( - issue_13343_text_to_unicode( - &font, - &[ - 0x28, 0x38, 0x31, 0x29, 0x8E, 0x77, 0x92, 0xE8, 0x8D, 0x91, 0x81, 0x45, 0x92, - 0x6E, 0x88, 0xE6, 0x81, 0x40, 0x20, 0x20, 0x41, 0x50, - ], - ), - "(81)\u{6307}\u{5b9a}\u{56fd}\u{30fb}\u{5730}\u{57df}\u{2003} AP" - ); - } - - #[test] - fn cid_font_type0_cjk_fallback_font_covers_issue_13343_glyphs() { - let font = issue_13343_font(); - let font_ref = FontRef::new(&font.font_file).unwrap(); - let cmap = font_ref.cmap().unwrap(); - let (_, _, subtable) = cmap.best_subtable().unwrap(); - - for c in [ - '\u{3010}', '\u{8981}', '\u{7d04}', '\u{3011}', '\u{6307}', '\u{5b9a}', '\u{56fd}', - '\u{5730}', '\u{57df}', '\u{2003}', - ] { - assert!( - subtable.map_codepoint(u32::from(c)).is_some(), - "missing fallback glyph for U+{:04X}", - u32::from(c) - ); - } - } - - fn issue_13343_font() -> Type0Font { - let mut descriptor_dict = BTreeMap::new(); - descriptor_dict.insert("Flags".to_string(), ObjectVariant::Integer(6)); - - let mut descendant_dict = BTreeMap::new(); - descendant_dict.insert( - "Subtype".to_string(), - ObjectVariant::Name(b"CIDFontType0".to_vec()), - ); - descendant_dict.insert( - "BaseFont".to_string(), - ObjectVariant::Name(b"Ryumin-Light".to_vec()), - ); - descendant_dict.insert( - "FontDescriptor".to_string(), - ObjectVariant::Dictionary(Box::new(Dictionary::new(descriptor_dict))), - ); - descendant_dict.insert( - "CIDSystemInfo".to_string(), - ObjectVariant::Dictionary(Box::new(Dictionary::new(BTreeMap::from([( - "Ordering".to_string(), - ObjectVariant::LiteralString(b"Japan1".to_vec()), - )])))), - ); - - let mut font_dict = BTreeMap::new(); - font_dict.insert( - "Subtype".to_string(), - ObjectVariant::Name(b"Type0".to_vec()), - ); - font_dict.insert( - "BaseFont".to_string(), - ObjectVariant::Name(b"Ryumin-Light-90ms-RKSJ-H".to_vec()), - ); - font_dict.insert( - "Encoding".to_string(), - ObjectVariant::Name(b"90ms-RKSJ-H".to_vec()), - ); - font_dict.insert( - "DescendantFonts".to_string(), - ObjectVariant::Array(vec![ObjectVariant::Dictionary(Box::new(Dictionary::new( - descendant_dict, - )))]), - ); - - Type0Font::from_dictionary(&Dictionary::new(font_dict), &PassthroughResolver).unwrap() - } + let result = Type0Font::from_dictionary(&dictionary, &PassthroughResolver); - fn issue_13343_text_to_unicode(font: &Type0Font, text: &[u8]) -> String { - font.decode_bytes_to_cids(text) - .iter() - .filter_map(|cid| font.glyph_to_unicode.as_ref().and_then(|map| map.get(cid))) - .collect() + assert!(matches!(result, Err(FontError::MissingFontFile))); } } diff --git a/crates/pdf-font/src/type1_font.rs b/crates/pdf-font/src/type1_font.rs index 658fb66b..183e6f29 100644 --- a/crates/pdf-font/src/type1_font.rs +++ b/crates/pdf-font/src/type1_font.rs @@ -349,7 +349,7 @@ currentfile eexec ); let mut id_allocator = ContentStreamIdAllocator::new(); - let font = Font::from_dictionary(&dict, &PassthroughResolver, &mut id_allocator).unwrap(); + let font = Font::from_dictionary(&dict, &PassthroughResolver, &mut id_allocator); let Font::TrueType(font) = font else { panic!("empty Type1C font should use a TrueType fallback"); diff --git a/crates/pdf-resources/src/resources.rs b/crates/pdf-resources/src/resources.rs index 83ef2043..568c1c38 100644 --- a/crates/pdf-resources/src/resources.rs +++ b/crates/pdf-resources/src/resources.rs @@ -82,22 +82,13 @@ pub(crate) fn read_font_resource( cycle_tracker: &mut ReadCycleTracker, id_allocator: &mut ContentStreamIdAllocator, ) -> Result { - // Font resources are loaded best-effort: if parsing the original font - // fails, keep the resource name resolvable by substituting a minimal - // Standard 14-backed fallback instead of aborting the whole /Resources - // dictionary. Nested font resources are only preserved for successfully - // parsed fonts. - let (font, resources) = match Font::from_dictionary(dictionary, objects, id_allocator) { - Ok(font) => { - let resources = - Resources::read(dictionary, objects, cache, cycle_tracker, id_allocator)? - .map(Rc::new); - (font, resources) - } - Err(_) => ( - Font::fallback_from_dictionary_best_effort(dictionary, objects), - None, - ), + // Font construction is best-effort and infallible. Only successfully + // parsed Type3 fonts consume nested resources; whole-font fallbacks do not. + let font = Font::from_dictionary(dictionary, objects, id_allocator); + let resources = if matches!(&font, Font::Type3(_)) { + Resources::read(dictionary, objects, cache, cycle_tracker, id_allocator)?.map(Rc::new) + } else { + None }; Ok(Resource::Font { diff --git a/crates/pdf-resources/tests/resources.rs b/crates/pdf-resources/tests/resources.rs index e1806900..015f99fc 100644 --- a/crates/pdf-resources/tests/resources.rs +++ b/crates/pdf-resources/tests/resources.rs @@ -519,6 +519,44 @@ fn self_referential_font_resources_resolve_lazily() { ); } +#[test] +fn fallback_fonts_do_not_read_nested_type3_resources() { + let malformed_type3 = ObjectVariant::Dictionary(Box::new(Dictionary::new(BTreeMap::from([ + ( + "Subtype".to_string(), + ObjectVariant::Name(b"Type3".to_vec()), + ), + ("Resources".to_string(), ObjectVariant::Integer(1)), + ])))); + let page_dict = Dictionary::new(BTreeMap::from([( + "Resources".to_string(), + ObjectVariant::Dictionary(Box::new(Dictionary::new(BTreeMap::from([( + "Font".to_string(), + ObjectVariant::Dictionary(Box::new(Dictionary::new(BTreeMap::from([( + "F1".to_string(), + malformed_type3, + )])))), + )])))), + )])); + let mut cache = DefaultResourceCache::default(); + let mut cycle_tracker = ReadCycleTracker::default(); + let mut ids = ContentStreamIdAllocator::new(); + + let resources = Resources::read( + &page_dict, + &PassthroughResolver, + &mut cache, + &mut cycle_tracker, + &mut ids, + ) + .expect("fallback font resources should parse") + .expect("page resources should exist"); + let (font, nested_resources) = resources.font("F1").expect("font should resolve"); + + assert!(matches!(font, Font::TrueType(_))); + assert!(nested_resources.is_none()); +} + #[test] fn self_referential_pattern_resources_resolve_lazily() { let page_dict = Dictionary::new(BTreeMap::from([(