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);