diff --git a/crates/pdf-font/src/fallback.rs b/crates/pdf-font/src/fallback.rs index fa47cfd7..ae6e1247 100644 --- a/crates/pdf-font/src/fallback.rs +++ b/crates/pdf-font/src/fallback.rs @@ -2,41 +2,12 @@ use pdf_cmap::ToUnicodeCMap; use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; use crate::{ - cid_system_info::CidOrdering, encoding::Encoding, error::FontError, flags::FontFlags, + cid_system_info::CidOrdering, encoding::Encoding, flags::FontFlags, simple_font_glyph_map::SimpleFontGlyphWidthsMap, standard14::Standard14Font, true_type_font::TrueTypeFont, }; -pub(crate) struct FallbackFontProgram { - pub(crate) font_file: &'static [u8], - pub(crate) standard14: Standard14Font, - pub(crate) flags: FontFlags, -} - -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, - objects: &dyn ObjectResolver, - ) -> Result { - 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(Self { - font_file, - standard14, - flags, - }) - } -} +const NOTO_SANS_CJK_JP_REGULAR: &[u8] = include_bytes!("../assets/NotoSansCJKjp-Regular.otf"); /// Build a synthetic TrueType font from fallback font data. /// @@ -48,52 +19,35 @@ impl FallbackFontProgram { /// # Returns /// /// A [`TrueTypeFont`] backed by fallback font bytes, simple font widths, -/// optional encoding, optional ToUnicode data, and descriptor flags. +/// optional encoding, optional ToUnicode data, and descriptor flags. Each +/// metadata field is parsed independently and ignored when malformed. pub(crate) fn fallback_true_type_from_dictionary( dictionary: &Dictionary, objects: &dyn ObjectResolver, -) -> Result { - let fallback = FallbackFontProgram::from_dictionary(dictionary, objects)?; - let widths = SimpleFontGlyphWidthsMap::from_dictionary(dictionary, objects)?; - let encoding = Encoding::from_dictionary(dictionary, objects) - .ok() - .flatten(); - let to_unicode = ToUnicodeCMap::from_dictionary(dictionary, objects)?; - - Ok(TrueTypeFont { - font_file: fallback.font_file.into(), - widths, - encoding, - to_unicode, - standard14: Some(fallback.standard14), - flags: fallback.flags, - }) -} - -/// Build a synthetic TrueType font from fallback font data without failing on -/// malformed optional metadata. -/// -/// This is used by higher-level resource loading when a font resource is -/// otherwise unreadable and the parser should preserve best-effort rendering. -pub(crate) fn fallback_true_type_from_dictionary_best_effort( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, ) -> TrueTypeFont { 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 font_file = if is_cjk { - FallbackFontProgram::NOTO_SANS_CJK_JP_REGULAR + let font_file = if is_cjk_cid_font(dictionary, objects) { + NOTO_SANS_CJK_JP_REGULAR } else { standard14.fallback_font_bytes() }; - let fallback = FallbackFontProgram { - font_file, - standard14, - flags, - }; - let mut font = TrueTypeFont::from_bytes(fallback.font_file, Some(fallback.standard14)); - font.flags = fallback.flags; + 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 } @@ -109,9 +63,108 @@ pub(crate) fn fallback_true_type_from_dictionary_best_effort( /// /// `true` when the dictionary declares a supported CJK CID ordering; otherwise /// `false`. -fn is_cjk_cid_font( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, -) -> Result { - Ok(CidOrdering::from_dictionary(dictionary, objects)?.is_some()) +fn is_cjk_cid_font(dictionary: &Dictionary, objects: &dyn ObjectResolver) -> bool { + CidOrdering::from_dictionary(dictionary, objects) + .ok() + .flatten() + .is_some() +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use std::collections::BTreeMap; + + use pdf_object::{ + object_resolver::PassthroughResolver, object_variant::ObjectVariant, stream::StreamObject, + }; + + 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(), + )); + let dictionary = Dictionary::new(BTreeMap::from([ + ( + "BaseFont".to_string(), + ObjectVariant::Name(b"Helvetica-Bold".to_vec()), + ), + ("FontDescriptor".to_string(), ObjectVariant::Integer(1)), + ("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), + ])); + + 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()) + ); + } + + #[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(), + )); + let dictionary = Dictionary::new(BTreeMap::from([ + ("Widths".to_string(), ObjectVariant::Integer(1)), + ("ToUnicode".to_string(), malformed_to_unicode), + ("CIDSystemInfo".to_string(), ObjectVariant::Integer(1)), + ])); + + let font = fallback_true_type_from_dictionary(&dictionary, &PassthroughResolver); + + assert!(font.widths.is_none()); + assert!(font.to_unicode.is_none()); + assert_eq!(font.standard14, Some(Standard14Font::Helvetica)); + assert_eq!( + font.font_file.as_ref(), + Standard14Font::Helvetica.fallback_font_bytes() + ); + } + + #[test] + fn fallback_uses_cjk_program_for_known_cid_ordering() { + let dictionary = 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 fallback = fallback_true_type_from_dictionary(&dictionary, &PassthroughResolver); + + assert_eq!(fallback.font_file.as_ref(), NOTO_SANS_CJK_JP_REGULAR); + } } diff --git a/crates/pdf-font/src/font.rs b/crates/pdf-font/src/font.rs index 3d42330d..4645a8d1 100644 --- a/crates/pdf-font/src/font.rs +++ b/crates/pdf-font/src/font.rs @@ -9,16 +9,9 @@ use read_fonts::TableProvider; use skrifa::{FontRef, MetadataProvider}; use crate::{ - char_vec::CharVec, - error::FontError, - fallback::{ - fallback_true_type_from_dictionary, fallback_true_type_from_dictionary_best_effort, - }, - glyph_name_to_unicode::glyph_name_to_unicode, - standard14::Standard14Font, - true_type_font::TrueTypeFont, - type0_font::Type0Font, - type1_font::Type1Font, + char_vec::CharVec, error::FontError, fallback::fallback_true_type_from_dictionary, + glyph_name_to_unicode::glyph_name_to_unicode, standard14::Standard14Font, + true_type_font::TrueTypeFont, type0_font::Type0Font, type1_font::Type1Font, type3_font::Type3Font, }; @@ -51,7 +44,7 @@ impl Font { } "Type1" => match Type1Font::from_dictionary(dictionary, objects) { Err(FontError::MissingFontFile) => Ok(Font::TrueType( - fallback_true_type_from_dictionary(dictionary, objects)?, + fallback_true_type_from_dictionary(dictionary, objects), )), Ok(type1_font) => Ok(Font::Type1(type1_font)), Err(e) => Err(e), @@ -70,14 +63,12 @@ impl Font { } } - /// Build a minimal Standard 14-backed fallback font for best-effort - /// resource recovery. + /// Build a Standard 14-backed fallback font for best-effort resource + /// recovery. /// - /// This is intentionally narrower than the normal font parsing path: - /// once the original font dictionary has already failed to parse, this - /// fallback does not attempt to preserve `/Widths`, `/Encoding`, or - /// `/ToUnicode` data from that failed font. The synthetic font keeps only - /// the bundled fallback program and the selected Standard 14 identity. + /// 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 @@ -86,9 +77,7 @@ impl Font { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Self { - Self::TrueType(fallback_true_type_from_dictionary_best_effort( - dictionary, objects, - )) + Self::TrueType(fallback_true_type_from_dictionary(dictionary, objects)) } } diff --git a/crates/pdf-font/src/true_type_font.rs b/crates/pdf-font/src/true_type_font.rs index 55c820b1..2ac5057b 100644 --- a/crates/pdf-font/src/true_type_font.rs +++ b/crates/pdf-font/src/true_type_font.rs @@ -6,8 +6,8 @@ use pdf_object::{ }; use crate::{ - encoding::Encoding, error::FontError, fallback::FallbackFontProgram, flags::FontFlags, - font_data::FontData, simple_font_glyph_map::SimpleFontGlyphWidthsMap, + encoding::Encoding, error::FontError, fallback::fallback_true_type_from_dictionary, + flags::FontFlags, font_data::FontData, simple_font_glyph_map::SimpleFontGlyphWidthsMap, standard14::Standard14Font, }; @@ -155,10 +155,10 @@ impl TrueTypeFont { } } - let fallback = FallbackFontProgram::from_dictionary(dictionary, objects)?; + let fallback = fallback_true_type_from_dictionary(dictionary, objects); Ok(TrueTypeFontProgram { - font_file: fallback.font_file.into(), - standard14: Some(fallback.standard14), + font_file: fallback.font_file, + standard14: fallback.standard14, flags: fallback.flags, }) } diff --git a/crates/pdf-font/src/type0_font.rs b/crates/pdf-font/src/type0_font.rs index 2408850a..2bbf8caa 100644 --- a/crates/pdf-font/src/type0_font.rs +++ b/crates/pdf-font/src/type0_font.rs @@ -11,7 +11,7 @@ pub use crate::cid_font_subtype::CidFontSubType; use crate::{ cid_system_info::CidOrdering, error::FontError, - fallback::FallbackFontProgram, + fallback::fallback_true_type_from_dictionary, font_data::FontData, glyph_widths_map::GlyphWidthsMap, true_type_font::TrueTypeFont, @@ -263,7 +263,7 @@ fn read_cid_font_type0_program( Ok((_, Type1FontProgramFormat::ClassicType1)) => Err(FontError::UnsupportedFontSubtype { subtype: "FontFile".to_string(), }), - Err(FontError::MissingFontFile) => fallback_type0_program(dictionary, objects), + Err(FontError::MissingFontFile) => Ok(fallback_type0_program(dictionary, objects)), Err(err) => Err(err), } } @@ -282,17 +282,17 @@ fn read_cid_font_type0_program( fn fallback_type0_program( dictionary: &Dictionary, objects: &dyn ObjectResolver, -) -> Result { - let fallback = FallbackFontProgram::from_dictionary(dictionary, objects)?; - let fallback_cid_to_unicode = cid_to_unicode_map(dictionary, objects)?; +) -> Type0FontProgram { + let fallback = fallback_true_type_from_dictionary(dictionary, objects); + let fallback_cid_to_unicode = cid_to_unicode_map(dictionary, objects).ok().flatten(); - Ok(Type0FontProgram { - font_file: fallback.font_file.into(), + 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. @@ -623,6 +623,42 @@ mod tests { ); } + #[test] + fn cid_font_type0_fallback_ignores_malformed_cid_system_info() { + let descendant = Dictionary::new(BTreeMap::from([ + ( + "Subtype".to_string(), + ObjectVariant::Name(b"CIDFontType0".to_vec()), + ), + ( + "FontDescriptor".to_string(), + ObjectVariant::Dictionary(Box::new(Dictionary::new(BTreeMap::new()))), + ), + ("CIDSystemInfo".to_string(), ObjectVariant::Integer(1)), + ])); + let dictionary = Dictionary::new(BTreeMap::from([ + ( + "Subtype".to_string(), + ObjectVariant::Name(b"Type0".to_vec()), + ), + ( + "DescendantFonts".to_string(), + ObjectVariant::Array(vec![ObjectVariant::Dictionary(Box::new(descendant))]), + ), + ])); + + 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();