fix(text): resolve a style's font file name against installed faces

A DWG text style stores its TrueType font as a *file* name — `GOST2304_TypeA_italic.ttf`,
sometimes with a directory — which is what AutoCAD resolves. The family lives
inside the file and is usually a different string (`GOST2304 Type A`), so looking
the reference up as a family missed an installed font and fell through to the
embedded stroke fonts. Those carry no Cyrillic at all (`simplex`, `standard`,
`romans`, `iso3098` hold no U+04xx glyph), so a Russian drawing came out with
every word missing: the letters went on to the system-font fallback, which
returns nothing on macOS.

Installed faces are now indexed by file name, and a reference — a style's font
file, or an inline `\f` code, which carries either form — resolves by family
first and by file name second. An embedded stroke font still wins, so
`romans.shx` keeps rendering as it did.

`cargo test --lib face_files_resolve_to_their_family`
This commit is contained in:
Alexey Bagno committed 2026-09-22 09:39:43 +03:00
1 parent baba5bccd3
commit f954c7f495
2 files changed
+122 -19

No files matched your search

+87 -5
View File
@@ -13,6 +13,10 @@ struct SysFonts {
db: fontdb::Database,
/// Sorted, de-duplicated family names for the picker.
families: Vec<String>,
/// 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<String, String>,
}
static FONTS: OnceLock<SysFonts> = OnceLock::new();
@@ -22,14 +26,31 @@ fn fonts() -> &'static SysFonts {
let mut db = fontdb::Database::new();
db.load_system_fonts();
let mut families: Vec<String> = db
.faces()
.filter_map(|face| face.families.first().map(|(name, _)| name.clone()))
.collect();
let mut families: Vec<String> = Vec::new();
let mut by_file: HashMap<String, String> = 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<T>(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<String> {
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<String> {
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);
}
}