forked from 1b-agent/opencadstudio-upstream
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:
1 parent
baba5bccd3
commit
f954c7f495
2 files changed
+122
-19
No files matched your search
@@ -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<String> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user