From d439d734e6ccf286d0eae477d3bcc3776ead40be Mon Sep 17 00:00:00 2001 From: JonRC Date: Mon, 9 Nov 2026 14:01:27 -0300 Subject: [PATCH] EPS import names the missing fonts of the document's type (#6326 follow-up) The warning #6326 added listed the families `set_text` met while the page ran. An Illustrator EPS can be rebuilt from its editing copy, whose type may be in other fonts than the page's, and `stringwidth` and `charpath` reach `set_text` without adding any type. The families are now taken from the text objects of the document the import gives (its layers and its symbols), once each, in the order the type comes, whichever path built it. --- crates/eps/src/import.rs | 47 ++++++++++++++++++++++--- crates/eps/src/import/graphics/state.rs | 14 -------- crates/eps/src/import/tests.rs | 9 +++++ crates/eps/src/import/tests_native.rs | 21 ++++++++++- crates/eps/src/import/text.rs | 9 +---- 5 files changed, 72 insertions(+), 28 deletions(-) diff --git a/crates/eps/src/import.rs b/crates/eps/src/import.rs index da75cfa84..0a5799ddf 100644 --- a/crates/eps/src/import.rs +++ b/crates/eps/src/import.rs @@ -74,7 +74,7 @@ use std::sync::Arc; use vectorcraft_color::Paint; use vectorcraft_color::swatch::{REGISTRATION, Swatch}; -use vectorcraft_doc::{ColorMode, Document, LayerColor, Node}; +use vectorcraft_doc::{ColorMode, Document, LayerColor, Node, NodeKind}; use vectorcraft_geom::{Affine, Rect}; use graphics::{GState, Out}; @@ -101,6 +101,8 @@ const MAX_SIDE: f64 = 1e5; /// The start of the warning that type was set in fonts that aren't available, before ": " and /// their families: PDF import's words, as type in those fonts comes in the same way. pub(crate) const MISSING_FONTS_NOTE: &str = "fonts that aren't available show in the fallback font until they are"; +/// Most families named in that warning. +const MAX_MISSING_FONTS: usize = 64; /// A read file. #[derive(Debug)] @@ -269,8 +271,46 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result< /// [`import_trusting`], with the names in the editing copy that aren't UTF-8 (layers, objects, /// artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables -/// and data sets) read in `names`. +/// and data sets) read in `names`. The type of the document it gives that is set in families that +/// aren't available shows in the fallback font, and a warning names them. pub fn import_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result { + let mut r = read_names(bytes, editing_data, trust, names)?; + let missing = missing_fonts(&r.document); + if !missing.is_empty() { + r.warnings.push(format!("{MISSING_FONTS_NOTE}: {}", missing.join(", "))); + } + Ok(r) +} + +/// The families of the type in `d` (its layers and its symbols) that aren't available, each once +/// in the order the type comes, at most [`MAX_MISSING_FONTS`]: the document's, whether its type +/// came from the page or from an editing copy, and none for type only measured or outlined. +fn missing_fonts(d: &Document) -> Vec { + let db = vectorcraft_text::FontDb::global(); + let mut checked: HashSet = HashSet::new(); + let mut missing: Vec = Vec::new(); + let mut add = |n: &Node| { + let NodeKind::Text(t) = &n.kind else { return }; + for run in &t.runs { + let family = run.style.font_family.as_str(); + if missing.len() >= MAX_MISSING_FONTS || family.is_empty() || checked.contains(family) { + continue; + } + checked.insert(family.to_string()); + if db.find_family(family).is_none() { + missing.push(family.to_string()); + } + } + }; + d.walk(&mut add); + for s in &d.symbols { + s.art.walk(&mut add); + } + missing +} + +/// [`import_names`] before the type's fonts are checked. +fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result { let ps = program(bytes)?; let dsc = Dsc::read(ps); // A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no @@ -368,9 +408,6 @@ fn finish_with(out: Out, layers: Vec>) -> Imported { } doc.layers = layers; let mut warnings = out.warnings; - if !out.missing_fonts.is_empty() { - warnings.push(format!("{MISSING_FONTS_NOTE}: {}", out.missing_fonts.join(", "))); - } doc.limit_depth_noting(&mut warnings); Imported { document: doc, warnings, preview: false } } diff --git a/crates/eps/src/import/graphics/state.rs b/crates/eps/src/import/graphics/state.rs index 692741c13..489f675bf 100644 --- a/crates/eps/src/import/graphics/state.rs +++ b/crates/eps/src/import/graphics/state.rs @@ -115,9 +115,6 @@ impl Default for GState { } } -/// Most families listed in the warning about fonts that aren't available. -const MAX_MISSING_FONTS: usize = 64; - /// What the program draws. pub(crate) struct Out { /// The document being made: ids, images and swatches. @@ -155,9 +152,6 @@ pub(crate) struct Out { pub(super) tilings: HashMap>, /// How deep art drawn apart (a pattern's cell, a Type 3 glyph) is nested. pub apart: u32, - /// The families type was set in that aren't available (it shows in the fallback font), each - /// once in the order first set, for the import's warning. - pub missing_fonts: Vec, } fn sane(r: Rect) -> bool { @@ -207,14 +201,6 @@ impl Out { shadings: HashSet::new(), tilings: HashMap::new(), apart: 0, - missing_fonts: vec![], - } - } - - /// Note that type was set in `family`, which isn't available (at most [`MAX_MISSING_FONTS`]). - pub fn missing_font(&mut self, family: &str) { - if !family.is_empty() && self.missing_fonts.len() < MAX_MISSING_FONTS && !self.missing_fonts.iter().any(|f| f == family) { - self.missing_fonts.push(family.to_string()); } } diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 81ee59e8c..f4edc0ac8 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1270,6 +1270,15 @@ fn shading_functions_in_shared_arrays_stop_at_the_limits() { assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{:?}", r.warnings); } +/// Type only measured (`stringwidth`) or drawn as outlines (`charpath`) adds no type to the +/// document, so its font isn't named as one that shows in the fallback font. +#[test] +fn fonts_only_measured_or_outlined_are_not_named_as_missing() { + let r = read("/Nowhere-Bold 12 selectfont (Hi) stringwidth pop pop newpath 10 10 moveto (Hi) true charpath fill 10 10 80 80 rectfill"); + assert!(!objects(&r.document).iter().any(|n| matches!(n.kind, NodeKind::Text(_)))); + assert!(!r.warnings.iter().any(|w| w.starts_with(super::MISSING_FONTS_NOTE)), "{:?}", r.warnings); +} + #[test] fn cshow_runs_its_procedure_for_each_character() { check("/n 0 def { pop pop pop /n n 1 add def } (abc) cshow n 3 eq"); diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 0c364b33a..13f979f9f 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -245,6 +245,25 @@ fn type_in_the_legacy_format_is_the_layers_own_and_the_page_has_text() { assert!(r.warnings.iter().any(|w| w.starts_with("1 text objects of the page are outlines on the layers")), "{:?}", r.warnings); } +/// The warning about fonts that aren't available names the fonts of the type the document keeps, +/// wherever it came from: type the editing copy sets in a font that isn't available is named +/// though the page sets it in one that is, and type only the page sets in such a font is not. +#[test] +fn the_missing_fonts_warning_names_the_fonts_of_the_documents_type() { + let page = |font: &str| format!("{}\n0 0 0 1 setcmykcolor /{font} findfont 12 scalefont setfont 10 50 moveto (Hi) show", page_square(10, 10)); + // `legacy_text` in `font`. + let words = |font: &str| format!("0 To\n1 0 0 1 10 50 0 Tp\nTP\n0 Tr\n0 0 0 1 k\n/_{font} 12 Tf\n(Hi) Tx\nTO"); + for (on_page, in_copy, named) in [("Inter-Regular", "Nowhere-Bold", Some("Nowhere")), ("Nowhere-Bold", "Inter-Regular", None)] { + let art = native(&(layer("Art", true, &square(10, 10)) + &layer("Words", true, &words(in_copy)))); + let r = import(&eps(&page(on_page), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art", "Words"], "{:?}", r.warnings); + assert_eq!(text_of(&r.document.layers[1]).len(), 1, "{:?}", r.warnings); + let notes: Vec<&String> = r.warnings.iter().filter(|w| w.starts_with(super::MISSING_FONTS_NOTE)).collect(); + let want: Vec = named.iter().map(|f| format!("{}: {f}", super::MISSING_FONTS_NOTE)).collect(); + assert_eq!(notes, want.iter().collect::>(), "page {on_page}, editing copy {in_copy}: {:?}", r.warnings); + } +} + #[test] fn a_damaged_editing_copy_leaves_the_page() { // Not ASCII85, ASCII85 of something that isn't Zstandard, Zstandard of something that isn't art. @@ -5203,7 +5222,7 @@ fn envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance() } }); assert_eq!(texts, [("CHONKYCATS".to_string(), false)], "{:?}", r.warnings); - assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert!(super::tests::program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings); } /// A live envelope that draws a little apart from its drawn art, in layers that already differ diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index e1cb64356..5868aa922 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -489,14 +489,7 @@ impl Interp<'_> { } let (family, font_style) = family_style(&name); // The available family's own spelling: `MicrosoftYaHei` is Microsoft YaHei, not "Microsoft Ya Hei". - // A family that isn't available keeps its name and shows in the fallback font. - let font_family = match vectorcraft_text::FontDb::global().find_family(&family) { - Some(f) => f, - None => { - self.out.missing_font(&family); - family - } - }; + let font_family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family); let fill = if self.g.paint.is_none() { Paint::solid(vectorcraft_color::Color::BLACK) } else { self.g.paint.clone() }; let style = CharStyle { font_family, font_style, size, fill, stroke: Paint::None, ..CharStyle::default() }; let mut t = TextObject::point(Point::ZERO, text, style);