diff --git a/crates/pdf/src/import.rs b/crates/pdf/src/import.rs index 7a5b3ab8..a8a39eba 100644 --- a/crates/pdf/src/import.rs +++ b/crates/pdf/src/import.rs @@ -21,7 +21,7 @@ use vectorcraft_geom::{FillRule, PathData}; use crate::import_color::{Colors, Native}; use crate::import_mask::{MaskSpec, contains, is_rectangle, luminance, mask_spec, white_cover}; -use crate::import_scan::{MAX_NESTING, Ocgs, Scan, all_on, hides_forms, scan_page, tag_key}; +use crate::import_scan::{FontNames, MAX_NESTING, Ocgs, Scan, all_on, hides_forms, scan_page, tag_key}; use crate::import_shading::{clipped, extend_clip, fold_stop_opacity, mesh_shading, shading_gradient}; use crate::import_text::{Families, LineFacts, Look, Placement, TextLine, Upright}; use crate::{CropTo, ImportOptions, ImportReport, PdfError, TextAs}; @@ -692,8 +692,9 @@ struct Builder<'p> { /// Mask keys of CMYK images whose mask turned out to hide nothing. opaque: HashSet, warnings: Vec, - /// Fonts by cache key → base font name (from [`scan_page`]). - fonts: HashMap, + /// Fonts by cache key → base font name and whether the file carries its glyphs (from + /// [`scan_page`]). + fonts: FontNames, font_names: HashMap, families: Option, missing_fonts: Vec, @@ -1346,7 +1347,7 @@ impl<'p> Builder<'p> { return n.clone(); } let data = o.font_data(); - let name = self.fonts.get(&key).cloned().or_else(|| data.as_ref().and_then(|d| d.postscript_name.clone())); + let name = self.fonts.get(&key).map(|(n, _)| n.clone()).or_else(|| data.as_ref().and_then(|d| d.postscript_name.clone())); let (weight, italic) = data.as_ref().map_or((None, false), |d| (d.weight, d.is_italic)); // A subset's six-letter tag. let ps = name.as_deref().map(|n| match n.split_once('+') { @@ -1429,7 +1430,12 @@ impl<'p> Builder<'p> { let upright = width.is_some_and(|w| w.abs() <= 1.0); let top = m * kurbo::Point::new(f64::from(width.filter(|w| *w > 1.0).unwrap_or(1000.0)) * 0.5, 880.0); let mut info = self.font_name(key, o); - if let Some(face) = &info.face { + // A font the file doesn't carry is drawn with a stand-in: its glyphs can't tell which + // style of the installed family it is, so the name decides. + let embedded = self.fonts.get(&key).is_none_or(|(_, e)| *e); + if let Some(face) = &info.face + && embedded + { let decided = match self.matched.get(&key) { Some(m) => m.clone(), // A glyph that can't be compared (several characters, a vertical form) decides diff --git a/crates/pdf/src/import_scan.rs b/crates/pdf/src/import_scan.rs index d2ec2e81..667274e8 100644 --- a/crates/pdf/src/import_scan.rs +++ b/crates/pdf/src/import_scan.rs @@ -248,7 +248,7 @@ struct Walker<'w, 'p> { /// The groups that are off for the interpreter (none once [`all_on`] turned them on). off: HashSet, xref: &'p hayro_syntax::xref::XRef, - fonts: &'w mut HashMap, + fonts: &'w mut FontNames, visible: Vec, ops: usize, max_ops: usize, @@ -296,7 +296,7 @@ impl Walker<'_, '_> { if let Some(f) = res.fonts.get::>(key.as_ref()) && let Some(name) = f.get::>(b"BaseFont") { - self.fonts.entry(f.cache_key()).or_insert_with(|| String::from_utf8_lossy(name.as_ref()).into_owned()); + self.fonts.entry(f.cache_key()).or_insert_with(|| (String::from_utf8_lossy(name.as_ref()).into_owned(), embeds_glyphs(&f))); } } } @@ -372,13 +372,32 @@ impl Walker<'_, '_> { } } +/// The fonts of a document by cache key: base font name, and whether the file carries the glyphs +/// ([`embeds_glyphs`]). +pub(crate) type FontNames = HashMap; + +/// Whether font `f` carries its own glyphs: a font program in its descriptor (a composite font's in +/// its descendant's), or a Type 3 font's glyph procedures. A font that doesn't is drawn with a +/// stand-in, whose glyphs say nothing about which installed face the document means. +fn embeds_glyphs(f: &Dict<'_>) -> bool { + if f.get::>(b"Subtype").is_some_and(|s| s.as_ref() == b"Type3") { + return true; + } + // A font program is a stream, so always an indirect object. + let program = |d: &Dict<'_>| { + d.get::>(b"FontDescriptor").is_some_and(|fd| [&b"FontFile"[..], b"FontFile2", b"FontFile3"].iter().any(|k| fd.get_ref(*k).is_some())) + }; + program(f) || f.get::>(b"DescendantFonts").and_then(|a| a.iter::>().next()).is_some_and(|d| program(&d)) +} + /// Walk `page` as the interpreter will: `all_on` when [`all_on`] turned every group on. Fonts -/// found are noted in `fonts` (their cache key → base font name). -pub(crate) fn scan_page(page: &Page<'_>, ocgs: &mut Ocgs, all_on: bool, fonts: &mut HashMap) -> Scan { +/// found are noted in `fonts` (their cache key → base font name, and whether the file carries its +/// glyphs). +pub(crate) fn scan_page(page: &Page<'_>, ocgs: &mut Ocgs, all_on: bool, fonts: &mut FontNames) -> Scan { walk_page(page, ocgs, all_on, fonts).out } -fn walk_page<'w, 'p>(page: &Page<'p>, ocgs: &'w mut Ocgs, all_on: bool, fonts: &'w mut HashMap) -> Walker<'w, 'p> { +fn walk_page<'w, 'p>(page: &Page<'p>, ocgs: &'w mut Ocgs, all_on: bool, fonts: &'w mut FontNames) -> Walker<'w, 'p> { let off = if all_on { HashSet::new() } else { ocgs.off.clone() }; let mut w = Walker { ocgs, off, xref: page.xref(), fonts, visible: vec![], ops: 0, max_ops: max_ops(), out: Scan::default(), hidden_form: false }; w.walk(page.typed_operations(), page.resources(), 0);