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