From 24638c409b8ba136a6607d0ef134fe7cb5c914f7 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:45:05 +0200 Subject: [PATCH] Warn on export when a font style is drawn in another style (#3229) --- crates/engine/src/cmd/fonts.rs | 45 ++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/fonts.rs b/crates/engine/src/cmd/fonts.rs index 1d4785b55..0df9b7988 100644 --- a/crates/engine/src/cmd/fonts.rs +++ b/crates/engine/src/cmd/fonts.rs @@ -301,7 +301,8 @@ fn drawn_runs(t: &vectorcraft_doc::TextObject) -> (Vec<&TextRun>, bool) { } /// What an export that draws type (as outlines, embedded fonts or pixels) says when `d` uses font -/// families that aren't available: their type is drawn in the fallback font, not the font it names. +/// families that aren't available (their type is drawn in the fallback font, not the font it +/// names) or styles that its family doesn't have (the closest style is drawn). pub(super) fn substitution_warning(d: &Document) -> Option { substitution_warning_of(used_fonts(d)) } @@ -309,20 +310,34 @@ pub(super) fn substitution_warning(d: &Document) -> Option { /// [`substitution_warning`] for type set in `fonts`. pub(super) fn substitution_warning_of(fonts: BTreeSet) -> Option { let db = vectorcraft_text::FontDb::global(); - let mut missing: Vec = fonts - .into_iter() - .filter(|(family, style, _)| db.resolve(family, style).is_none_or(|(_, m)| m == vectorcraft_text::FontMatch::Missing)) - .map(|(family, _, _)| family) - .collect(); + let mut missing: Vec = Vec::new(); + let mut styles: BTreeSet = BTreeSet::new(); + for (family, style, _) in fonts { + match db.resolve(&family, &style) { + None | Some((_, vectorcraft_text::FontMatch::Missing)) => missing.push(family), + Some((face, vectorcraft_text::FontMatch::Style)) => { + styles.insert(format!("{family} {style} was drawn in {} {}", face.family, face.style)); + } + Some(_) => {} + } + } // Sorted by family: its styles and versions are neighbours. missing.dedup(); - (!missing.is_empty()).then(|| { - format!( + let mut parts = Vec::new(); + if !missing.is_empty() { + parts.push(format!( "fonts that aren't available were written in the fallback font, {}: {} (install them, or replace them with Find Font)", vectorcraft_text::FALLBACK_FAMILY, missing.join(", ") - ) - }) + )); + } + if !styles.is_empty() { + parts.push(format!( + "font styles that aren't available were written in the closest style: {} (pick a style the family has)", + styles.into_iter().collect::>().join(", ") + )); + } + (!parts.is_empty()).then(|| parts.join("; ")) } fn text(d: &Document, id: NodeId) -> Option<&vectorcraft_doc::TextObject> { @@ -585,6 +600,16 @@ mod open_tests { assert_eq!(inter["missingGlyphs"], 2, "雅 and 楽 come from a fallback font: {inter}"); } + #[test] + fn export_warns_when_a_style_is_drawn_in_another_style() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Hi", "font": "Source Sans 3", "style": "Black Wide"})).unwrap(); + let out = s.execute("document.export", &json!({"format": "pdf"})).unwrap(); + let warnings = out["warnings"].to_string(); + assert!(warnings.contains("Source Sans 3 Black Wide was drawn in Source Sans 3"), "{out}"); + } + #[test] fn opened_documents_have_exact_text_bounds() { let mut s = Session::new();