From ddf348af225abd150367550e644a7b0149fbd9ae Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sat, 17 Oct 2026 05:56:14 +0100 Subject: [PATCH] fix(text): stroke aliases txt/monotxt must ignore a same-named TTF Face::resolve routes a name to a system TrueType face when installed_alias reports that an alias names a real TTF face (the #1764 escape hatch, added so an installed ISOCPEUR draws that face instead of its LFF stand-in). That hatch also caught TXT/MONOTXT: on a machine with a font family matching "txt" (e.g. a "Txt Regular"), the DXF stroke text font resolved to that TrueType face instead of the embedded stroke substitute, contradicting the documented contract that a missing .shx draws with the stroke substitute and never with a similarly-named TrueType face. TXT/MONOTXT are stroke aliases, so exclude them from the hatch. ISOCPEUR keeps its #1764 behaviour. Eight tests failed on such a machine and now pass: the four font-face/LFF assertions (shx_name_stays_lff, lff_falls_back_to_ttf_for_uncovered_glyph, big_font_pairing_routes_ideographs_and_leaves_latin_alone, lff_tessellation_golden) and the four searchable-PDF assertions that route through the same Face::resolve. Adds a regression test for the contract. --- src/scene/text/font_face.rs | 24 ++++++++++++++++++++++++ src/scene/text/lff.rs | 13 +++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/scene/text/font_face.rs b/src/scene/text/font_face.rs index 716adb298..a2f636d5a 100644 --- a/src/scene/text/font_face.rs +++ b/src/scene/text/font_face.rs @@ -279,6 +279,30 @@ mod tests { assert!(matches!(Face::resolve("romans.shx"), Face::Lff(_))); } + // `txt`/`monotxt` are the DXF *stroke* text fonts. An installed TrueType + // face that happens to share the name (e.g. "Txt Regular") is not that + // font and must never stand in for it, so the stroke substitute is drawn + // on every machine. The #1764 escape hatch (an installed copy of a real + // TrueType alias such as ISOCPEUR draws that face) stays for aliases that + // genuinely name one. + #[test] + fn stroke_aliases_ignore_a_same_named_true_type_face() { + for name in &["txt", "TXT", "monotxt", "MONOTXT"] { + assert!( + matches!(Face::resolve(name), Face::Lff(_)), + "{name} must resolve to the embedded stroke font" + ); + } + // A stroke face still supplies its own Latin glyphs. + assert!(Face::resolve("txt").glyph('A').is_some()); + // ISOCPEUR keeps the #1764 behaviour: Lff stand-in, or the installed + // TrueType face when one is present. + assert!(matches!( + Face::resolve("isocpeur"), + Face::Lff(_) | Face::Ttf { .. } + )); + } + #[test] fn lff_falls_back_to_ttf_for_uncovered_glyph() { // A CJK ideograph is in no stroke font; with a stroke style selected it diff --git a/src/scene/text/lff.rs b/src/scene/text/lff.rs index b3d77ebdd..1e19cbf44 100644 --- a/src/scene/text/lff.rs +++ b/src/scene/text/lff.rs @@ -96,6 +96,13 @@ const ALIASES: &[(&str, &str)] = &[ ("EXTFONT", "unicode"), ]; +/// Aliases that always mean the reference's *stroke* font and so never defer +/// to an installed TrueType face of the same name: `txt`/`monotxt` are the DXF +/// stroke text fonts, and a coincidental `Txt Regular` is not that font. The +/// `installed_alias` escape hatch stays for aliases that genuinely name a +/// TrueType face (ISOCPEUR, #1764). +const STROKE_ALIASES: &[&str] = &["TXT", "MONOTXT"]; + /// The space advance of the reference's stroke fonts, in glyph units (cap /// height 9), measured from their text extents: the SHX names these LFF /// fonts stand in for keep the reference's word gaps. @@ -529,11 +536,13 @@ pub fn is_builtin(name: &str) -> bool { /// draws with the stroke substitute, not the installed `.ttf` (measured). fn installed_alias(stem: &str, name: &str) -> bool { use crate::scene::text::sysfont; + let stem = stem.trim().to_ascii_uppercase(); !is_shx_name(name) + && !STROKE_ALIASES.contains(&stem.as_str()) && ALIASES.iter().any(|(alias, _)| *alias == stem) && (sysfont::family_for_file(name).is_some() - || sysfont::canonical_family_name(stem) - .is_some_and(|family| family.eq_ignore_ascii_case(stem))) + || sysfont::canonical_family_name(&stem) + .is_some_and(|family| family.eq_ignore_ascii_case(&stem))) } /// Whether `name` is written as an `.shx` file name.