Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 126 additions & 73 deletions crates/pdf-font/src/fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, FontError> {
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.
///
Expand All @@ -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<TrueTypeFont, FontError> {
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
}
Expand All @@ -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<bool, FontError> {
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);
}
}
31 changes: 10 additions & 21 deletions crates/pdf-font/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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),
Expand All @@ -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
Expand All @@ -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))
}
}

Expand Down
10 changes: 5 additions & 5 deletions crates/pdf-font/src/true_type_font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
})
}
Expand Down
Loading
Loading