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([(