Skip to content
Open
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
17 changes: 1 addition & 16 deletions crates/pdf-font/src/cid_system_info.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<Option<HashMap<u16, char>>, 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<Self> {
match name {
"Japan1" => Some(Self::Japan1),
Expand Down
116 changes: 48 additions & 68 deletions crates/pdf-font/src/fallback.rs
Original file line number Diff line number Diff line change
@@ -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,
};

Expand All @@ -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.
Expand All @@ -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)),
]));

Expand All @@ -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);

Expand Down
55 changes: 29 additions & 26 deletions crates/pdf-font/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Font, FontError> {
// Determine the font subtype from the dictionary.
let subtype = dictionary.required_str("Subtype", objects)?;
Expand All @@ -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))
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
Loading