From ddbc0d545ae8789250e14f63f2715665f805deb7 Mon Sep 17 00:00:00 2001 From: JonRC Date: Mon, 9 Nov 2026 12:31:17 -0300 Subject: [PATCH] Fix #6320: EPS import warns about fonts that aren't available Type set in a family that isn't available keeps its name and shows in the fallback font, as in PDF import, but EPS import said nothing. It now notes each such family once, in the order its type comes (at most 64), and ends with PDF import's warning: "fonts that aren't available show in the fallback font until they are: ". Tests that set type in fonts the test machine may lack (Helvetica, Ryumin, made-up names) check the program's warnings without that note, since which fonts are installed depends on the machine. --- crates/eps/src/import.rs | 7 +++++++ crates/eps/src/import/graphics/state.rs | 14 ++++++++++++++ crates/eps/src/import/tests.rs | 10 ++++++++-- crates/eps/src/import/tests_cid.rs | 2 +- crates/eps/src/import/tests_native.rs | 2 +- crates/eps/src/import/tests_type1.rs | 20 ++++++++++++++++++-- crates/eps/src/import/text.rs | 9 ++++++++- 7 files changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/eps/src/import.rs b/crates/eps/src/import.rs index 9a61061c9..da75cfa84 100644 --- a/crates/eps/src/import.rs +++ b/crates/eps/src/import.rs @@ -98,6 +98,10 @@ const LETTER: [f64; 4] = [0.0, 0.0, 612.0, 792.0]; /// Largest page side read (points). 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"; + /// A read file. #[derive(Debug)] pub struct Imported { @@ -364,6 +368,9 @@ 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 489f675bf..692741c13 100644 --- a/crates/eps/src/import/graphics/state.rs +++ b/crates/eps/src/import/graphics/state.rs @@ -115,6 +115,9 @@ 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. @@ -152,6 +155,9 @@ 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 { @@ -201,6 +207,14 @@ 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 d654b2b2d..81ee59e8c 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -737,7 +737,7 @@ fn embedded_font_programs_are_skipped() { let r = read(&program); let NodeKind::Text(t) = &objects(&r.document)[0].kind else { panic!("{program}: {:?}", r.warnings) }; assert_eq!(t.runs[0].style.font_family, "My Font"); - assert!(r.warnings.is_empty(), "{program}: {:?}", r.warnings); + assert!(program_warnings(&r.warnings).is_empty(), "{program}: {:?}", r.warnings); } } @@ -833,12 +833,18 @@ fn device_settings_are_accepted_and_change_nothing() { } /// Check that `body` draws one square, blue: it sets blue when what it checks holds, else red. +/// `warnings` without the note naming fonts that aren't available: which those are depends on the +/// fonts installed where the tests run, not on the program. +pub(super) fn program_warnings(warnings: &[String]) -> Vec<&String> { + warnings.iter().filter(|w| !w.starts_with(super::MISSING_FONTS_NOTE)).collect() +} + fn blue(body: &str) { let r = read(&format!("{body} 10 10 80 80 rectfill")); let o = objects(&r.document); assert_eq!(o.len(), 1, "{body}: {:?}", r.warnings); assert_eq!(fill(&o[0]), Some(&Paint::solid(Color::rgb(0.0, 0.0, 1.0))), "{body}"); - assert!(r.warnings.is_empty(), "{body}: {:?}", r.warnings); + assert!(program_warnings(&r.warnings).is_empty(), "{body}: {:?}", r.warnings); } /// Check that PostScript `cond` leaves `true`. diff --git a/crates/eps/src/import/tests_cid.rs b/crates/eps/src/import/tests_cid.rs index 200ae4601..2b01b2b4f 100644 --- a/crates/eps/src/import/tests_cid.rs +++ b/crates/eps/src/import/tests_cid.rs @@ -392,7 +392,7 @@ fn named_cidfonts_over_unicode_cmaps_set_their_characters_as_type() { .collect() }; let r = super::tests::read("/Ryumin-Light-UniJIS-UCS2-H findfont 20 scalefont setfont 10 20 moveto <4e2d0041> show"); - assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert!(super::tests::program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings); let t = texts(&r); assert_eq!(t.len(), 1, "{:?}", all(&r.document)); assert_eq!(t[0].plain_text(), "\u{4e2d}A"); diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index d5946b008..0c364b33a 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -215,7 +215,7 @@ fn text_and_its_strokes_come_from_the_page_into_the_layer_of_its_text_object() { assert_eq!(kinds(&r.document.layers[0]), ["path"]); // The text and the path of its stroke, where the text object is (no group of their own). assert_eq!(kinds(&r.document.layers[1]), ["text", "path"], "{:?}", r.warnings); - assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert!(super::tests::program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings); } /// A point type object in the legacy format (`To` … `TO`), as an EPS saved for Illustrator 10 diff --git a/crates/eps/src/import/tests_type1.rs b/crates/eps/src/import/tests_type1.rs index c36b988fc..539904290 100644 --- a/crates/eps/src/import/tests_type1.rs +++ b/crates/eps/src/import/tests_type1.rs @@ -5,7 +5,7 @@ use vectorcraft_doc::{NodeKind, TextObject}; -use super::tests::{all, bounds, check, read}; +use super::tests::{all, bounds, check, program_warnings, read}; use crate::import::import; /// A Type 1 charstring number (§6.2). @@ -115,7 +115,7 @@ fn program(encoding: &str, hex: bool, body: &str) -> crate::Imported { fn embedded_type1_type_keeps_the_fonts_widths() { for hex in [true, false] { let r = program(OWN, hex, "/TestOne findfont 50 scalefont setfont 10 20 moveto (AB) show"); - assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert!(program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings); let t = texts(&r); assert_eq!(t.len(), 1, "{:?}", all(&r.document)); assert_eq!(t[0].plain_text(), "AB"); @@ -126,6 +126,22 @@ fn embedded_type1_type_keeps_the_fonts_widths() { } } +/// Type in an embedded font whose family isn't available keeps the font's name and shows in the +/// fallback font, and the import says so as PDF import does: each such family once, in the order +/// its type comes. A family that is available isn't named. +#[test] +fn type_in_fonts_that_arent_available_is_named_in_a_warning() { + let r = program( + OWN, + false, + "/TestOne findfont 50 scalefont setfont 10 20 moveto (AB) show 10 60 moveto (BA) show \ + /Nowhere-Bold 12 selectfont 10 80 moveto (x) show /Inter-Regular 12 selectfont 10 90 moveto (y) show", + ); + assert_eq!(texts(&r).iter().map(|t| t.runs[0].style.font_family.as_str()).collect::>(), ["Test One", "Test One", "Nowhere", "Inter"]); + // PDF import's words. + assert_eq!(r.warnings, ["fonts that aren't available show in the fallback font until they are: Test One, Nowhere"]); +} + #[test] fn embedded_type1_fonts_measure_and_outline_their_glyphs() { let fonts = String::from_utf8(font(OWN, true)).unwrap(); diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index 5868aa922..e1cb64356 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -489,7 +489,14 @@ impl Interp<'_> { } let (family, font_style) = family_style(&name); // The available family's own spelling: `MicrosoftYaHei` is Microsoft YaHei, not "Microsoft Ya Hei". - let font_family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family); + // 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 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);