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: <families>".

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.
This commit is contained in:
JonRC
2026-11-09 12:31:17 -03:00
parent 6dac80e75d
commit ddbc0d545a
7 changed files with 57 additions and 7 deletions
+7
View File
@@ -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<Arc<Node>>) -> 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 }
}
+14
View File
@@ -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<u64, Vec<Tiling>>,
/// 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<String>,
}
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());
}
}
+8 -2
View File
@@ -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`.
+1 -1
View File
@@ -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");
+1 -1
View File
@@ -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
+18 -2
View File
@@ -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::<Vec<_>>(), ["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();
+8 -1
View File
@@ -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);