diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 57e865093..c41269990 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -5,6 +5,20 @@ use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke}; use crate::scene::text::font_face::Face; use crate::scene::text::lff; +/// A font reference a drawing holds — a style's font file, or an inline `\f` +/// code — turned into an installed family. +/// +/// An embedded stroke font wins over anything installed, so `romans.shx` keeps +/// rendering as it did even where a font of that name happens to be installed; +/// only a name no stroke font answers to is looked up among the system faces, +/// first by family and then by file name (see `sysfont::family_for_reference`). +fn resolve_font_reference(reference: &str) -> Option { + if lff::is_builtin(reference) { + return None; + } + crate::scene::text::sysfont::family_for_reference(reference) +} + pub struct ResolvedTextStyle { pub font_name: String, pub width_factor: f32, @@ -21,14 +35,20 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT }); let mut font_name = if let Some(style) = style { - if !style.true_type_font.trim().is_empty() { - style.true_type_font.trim().to_string() - } else if !style.font_file.trim().is_empty() { - let file = style.font_file.trim(); + // A style names a font *file* — `GOST2304_TypeA_italic.ttf`, + // `romans.shx`, sometimes with a directory; `true_type_font` carries the + // same thing for the styles that set it. + let file = if !style.true_type_font.trim().is_empty() { + style.true_type_font.trim() + } else { + style.font_file.trim() + }; + if !file.is_empty() { // A .shx font that resolves on disk (as stored, or next to the // drawing) renders its REAL stroke glyphs — pass the absolute - // path through so `Face::resolve` picks the SHX face. Only an - // unresolvable file falls back to the stem's LFF substitute. + // path through so `Face::resolve` picks the SHX face. Fonts fetched + // from the community repository live in the per-user fonts folder, + // so that is searched last, after the drawing folder. let shx_path = file .to_ascii_lowercase() .ends_with(".shx") @@ -38,10 +58,7 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT .as_deref() .map(std::path::Path::new) .and_then(|p| p.parent()); - crate::io::resolve_image_file(file, base) - // Fonts fetched from the community repository live in - // the per-user fonts folder — search it last. - .or_else(|| { + crate::io::resolve_image_file(file, base).or_else(|| { crate::io::font_repo::local_font_file(file) .map(|path| path.to_string_lossy().into_owned()) }) @@ -49,6 +66,11 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT .flatten(); if let Some(p) = shx_path { p + } else if let Some(family) = resolve_font_reference(file) { + // The file is installed, so its family is the face to draw with. + // Substituting a stroke font instead drops every letter that + // font does not have — for a Cyrillic drawing, all of them. + family } else { let basename = file.rsplit(['/', '\\']).next().unwrap_or(file); let stem = basename.split('.').next().unwrap_or(basename).trim(); @@ -900,10 +922,9 @@ pub fn resolve_font<'a>(state: &'a RunState, base: &'a str) -> std::borrow::Cow< if lff::is_builtin(font) { return std::borrow::Cow::Borrowed(font); } - if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(font) { - std::borrow::Cow::Owned(canonical) - } else { - std::borrow::Cow::Borrowed(base) + match resolve_font_reference(font) { + Some(canonical) => std::borrow::Cow::Owned(canonical), + None => std::borrow::Cow::Borrowed(base), } } diff --git a/src/scene/text/sysfont.rs b/src/scene/text/sysfont.rs index 7e24c4d2a..09e2427fb 100644 --- a/src/scene/text/sysfont.rs +++ b/src/scene/text/sysfont.rs @@ -13,6 +13,10 @@ struct SysFonts { db: fontdb::Database, /// Sorted, de-duplicated family names for the picker. families: Vec, + /// Installed face file name (lower-case) → the family that face holds. + /// A drawing references a TrueType style by *file* name, so this is what + /// makes such a style resolve — see [`family_for_reference`]. + by_file: HashMap, } static FONTS: OnceLock = OnceLock::new(); @@ -22,14 +26,31 @@ fn fonts() -> &'static SysFonts { let mut db = fontdb::Database::new(); db.load_system_fonts(); - let mut families: Vec = db - .faces() - .filter_map(|face| face.families.first().map(|(name, _)| name.clone())) - .collect(); + let mut families: Vec = Vec::new(); + let mut by_file: HashMap = HashMap::new(); + for face in db.faces() { + let Some((family, _)) = face.families.first() else { + continue; + }; + families.push(family.clone()); + // Remember every face file: a DWG style names the file, and the + // family inside it is usually a different string. + if let fontdb::Source::File(path) = &face.source { + if let Some(name) = path.file_name() { + by_file + .entry(name.to_string_lossy().to_ascii_lowercase()) + .or_insert_with(|| family.clone()); + } + } + } families.sort_by_key(|n| n.to_lowercase()); families.dedup(); - SysFonts { db, families } + SysFonts { + db, + families, + by_file, + } }) } @@ -136,9 +157,70 @@ pub fn with_face_data(family: &str, f: impl FnOnce(&[u8], u32) -> T) -> Optio fonts().db.with_face_data(id, f) } +/// The family of the installed face whose file is named `name` — a bare file +/// name or any path to one, matched case-insensitively. +fn family_for_file(name: &str) -> Option { + let file = name.rsplit(['/', '\\']).next()?.trim(); + if file.is_empty() { + return None; + } + fonts().by_file.get(&file.to_ascii_lowercase()).cloned() +} + +/// The installed family a drawing's font reference names, or `None` when +/// nothing installed answers to it. +/// +/// A DWG text style stores the TrueType font *file* (`GOST2304_TypeA_italic.ttf`, +/// sometimes with a directory), which is what AutoCAD resolves; the family name +/// lives inside the file and is usually not the same string. Inline `\f` codes +/// carry either form, so both are tried. +pub fn family_for_reference(reference: &str) -> Option { + let reference = reference.trim(); + if reference.is_empty() { + return None; + } + canonical_family_name(reference).or_else(|| family_for_file(reference)) +} + /// Whether `family` matches an installed system font (case-insensitive via /// fontdb's own matching). pub fn has_family(family: &str) -> bool { face_id(family).is_some() } +#[cfg(test)] +mod tests { + use super::*; + + /// A DWG stores a style's TrueType font as a file name, so every installed + /// face has to be reachable by the name of its file — case-insensitively, + /// and whether it comes as a bare name or as a path. + #[test] + fn face_files_resolve_to_their_family() { + let found = fonts().db.faces().find_map(|face| match &face.source { + fontdb::Source::File(path) => { + let file = path.file_name()?.to_string_lossy().into_owned(); + let family = face.families.first().map(|(name, _)| name.clone())?; + Some((file, family)) + } + _ => None, + }); + let Some((file, family)) = found else { + eprintln!("no installed face files on this host; skipping"); + return; + }; + + assert_eq!(family_for_file(&file).as_deref(), Some(family.as_str())); + assert_eq!( + family_for_file(&file.to_ascii_uppercase()).as_deref(), + Some(family.as_str()) + ); + assert_eq!( + family_for_reference(&format!("/somewhere/else/{file}")).as_deref(), + Some(family.as_str()) + ); + assert_eq!(family_for_file("no-such-face-9f3.ttf"), None); + assert_eq!(family_for_reference(" "), None); + } +} +