mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Fonts: use the TrueType typeface from a style's xdata and find families by first word (#894)
AutoCAD stores a TrueType text style's family (e.g. "GOST type A") in the style's ACAD xdata and leaves the font file name empty. Styles without a file now take that family. Family-name lookups also look in files whose name starts with the family's first word, so "GOST type A" finds GOST.TTF (the stem shares only 4 characters with the family name). Co-authored-by: simanov <[email protected]> Co-authored-by: Claude Sonnet 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
3 files changed
+77
-12
No files matched your search
+41
-11
@@ -100,6 +100,8 @@ const MAX_FALLBACK_BYTES: usize = 160 << 20;
|
||||
const MAX_FALLBACK_FACES: usize = 256;
|
||||
/// Files loaded by one family-name lookup.
|
||||
const MAX_FAMILY_FILES: usize = 6;
|
||||
/// Further files a family-name lookup loads: those starting with the name's first word.
|
||||
const MAX_FIRST_WORD_FILES: usize = 24;
|
||||
/// Total bytes of system font files loaded from disk: a drawing naming many styles can't pull
|
||||
/// every installed font into memory.
|
||||
const MAX_LOADED_BYTES: usize = 256 << 20;
|
||||
@@ -311,22 +313,15 @@ impl Db {
|
||||
self.by_family(k)
|
||||
}
|
||||
|
||||
/// A face whose family or full name is `k`, among files with a similar stem.
|
||||
/// A face whose family or full name is `k`, among files with a similar stem, then among the
|
||||
/// files whose stem begins with the name's first word (`GOST.TTF` for "GOST type A").
|
||||
fn by_family(&mut self, k: &str) -> Option<Face> {
|
||||
let want = norm(k);
|
||||
if want.len() < 5 {
|
||||
return None;
|
||||
}
|
||||
let mut stems: Vec<(usize, usize, String)> = self
|
||||
.paths
|
||||
.keys()
|
||||
.chain(self.files.keys())
|
||||
.map(|s| (weight_penalty(s, &want), common_prefix(&norm(s), &want), s.clone()))
|
||||
.filter(|(_, p, _)| *p >= 5)
|
||||
.collect();
|
||||
stems.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)).then(a.2.cmp(&b.2)));
|
||||
stems.dedup_by(|a, b| a.2 == b.2);
|
||||
for (_, _, stem) in stems.into_iter().take(MAX_FAMILY_FILES) {
|
||||
let stems: Vec<String> = self.paths.keys().chain(self.files.keys()).cloned().collect();
|
||||
for stem in family_candidates(&stems, &want, &norm(k.split_whitespace().next().unwrap_or(""))) {
|
||||
let Some(bytes) = self.file(&stem) else { continue };
|
||||
if let Some(face) = faces(&bytes).into_iter().find(|f| f.names().iter().any(|n| *n == want || format!("{n}regular") == want)) {
|
||||
return Some(face);
|
||||
@@ -421,6 +416,25 @@ fn norm(s: &str) -> String {
|
||||
s.chars().filter(|c| !matches!(c, ' ' | '-' | '_')).flat_map(char::to_lowercase).collect()
|
||||
}
|
||||
|
||||
/// The font file stems to check for the family `want` (normalised), best first: those sharing a
|
||||
/// long prefix with it ([`MAX_FAMILY_FILES`]), then those starting with its `first` word
|
||||
/// ([`MAX_FIRST_WORD_FILES`]).
|
||||
fn family_candidates(stems: &[String], want: &str, first: &str) -> Vec<String> {
|
||||
let mut by_prefix: Vec<(usize, usize, String)> =
|
||||
stems.iter().map(|s| (weight_penalty(s, want), common_prefix(&norm(s), want), s.clone())).filter(|(_, p, _)| *p >= 5).collect();
|
||||
by_prefix.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)).then(a.2.cmp(&b.2)));
|
||||
by_prefix.dedup_by(|a, b| a.2 == b.2);
|
||||
let mut out: Vec<String> = by_prefix.into_iter().take(MAX_FAMILY_FILES).map(|(_, _, s)| s).collect();
|
||||
if first.chars().count() >= 4 {
|
||||
let mut by_word: Vec<(usize, usize, String)> =
|
||||
stems.iter().filter(|s| norm(s).starts_with(first) && !out.contains(s)).map(|s| (weight_penalty(s, want), s.len(), s.clone())).collect();
|
||||
by_word.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
|
||||
by_word.dedup_by(|a, b| a.2 == b.2);
|
||||
out.extend(by_word.into_iter().take(MAX_FIRST_WORD_FILES).map(|(_, _, s)| s));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn common_prefix(a: &str, b: &str) -> usize {
|
||||
a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count()
|
||||
}
|
||||
@@ -848,3 +862,19 @@ pub fn layout_line(face: &Face, s: &str, height: f64, width_factor: f64, oblique
|
||||
strokes.extend(sh.strokes);
|
||||
Some(Run { strokes, width: sh.width })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod family_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_family_is_looked_for_in_files_starting_with_its_first_word() {
|
||||
let stems: Vec<String> = ["gost", "gost2304a", "gost common", "arial", "isocpeur"].map(String::from).to_vec();
|
||||
let c = family_candidates(&stems, &norm("gost type a"), "gost");
|
||||
assert!(c.contains(&"gost".to_string()), "GOST.TTF holds \"GOST type A\": {c:?}");
|
||||
assert!(!c.contains(&"arial".to_string()));
|
||||
assert!(family_candidates(&stems, &norm("gost type a"), "go").is_empty(), "too short a word matches nothing");
|
||||
let many: Vec<String> = (0..100).map(|i| format!("gost{i:03}")).collect();
|
||||
assert!(family_candidates(&many, "gosttypea", "gost").len() <= MAX_FAMILY_FILES + MAX_FIRST_WORD_FILES);
|
||||
}
|
||||
}
|
||||
@@ -1277,8 +1277,16 @@ fn annotative(tags: &[Tag]) -> bool {
|
||||
}
|
||||
|
||||
/// A text style's font: CADCraft's own font name from `CADCRAFT` xdata (`1000 FONT`, name)
|
||||
/// while the font file is still the `txt` written for other readers, otherwise the file.
|
||||
/// while the font file is still the `txt` written for other readers; the TrueType family of the
|
||||
/// style's `ACAD` xdata (`1000` typeface) when it has no font file (how AutoCAD stores TrueType
|
||||
/// styles); otherwise the file.
|
||||
fn style_font(tags: &[Tag], file: String) -> String {
|
||||
if file.trim().is_empty()
|
||||
&& let Some(face) =
|
||||
crate::dxf_ext::xdata(tags, "ACAD").iter().find(|t| t.code == 1000).map(|t| t.str().trim().to_string()).filter(|f| !f.is_empty())
|
||||
{
|
||||
return face;
|
||||
}
|
||||
let x = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP);
|
||||
let own = x.windows(2).find_map(|w| match w {
|
||||
[m, n] if m.code == 1000 && m.str() == "FONT" && n.code == 1000 => Some(n.str()),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//! A TrueType text style without a font file takes its family from the style's `ACAD` xdata.
|
||||
|
||||
fn style(name: &str, file: &str, xdata: &[(&str, &str)]) -> String {
|
||||
let mut s = format!(" 0\nSTYLE\n 2\n{name}\n 70\n0\n 40\n0.0\n 41\n1.0\n 50\n0.0\n 71\n0\n 42\n0.2\n 3\n{file}\n 4\n\n");
|
||||
if !xdata.is_empty() {
|
||||
s.push_str("1001\nACAD\n");
|
||||
for (c, v) in xdata {
|
||||
s.push_str(&format!("{c}\n{v}\n"));
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_without_a_font_file_uses_the_xdata_typeface() {
|
||||
let dxf = format!(
|
||||
" 0\nSECTION\n 2\nTABLES\n 0\nTABLE\n 2\nSTYLE\n 70\n3\n{}{}{} 0\nENDTAB\n 0\nENDSEC\n 0\nEOF\n",
|
||||
style("TT", "", &[("1000", "GOST type A"), ("1071", "2")]),
|
||||
style("File", "arial.ttf", &[("1000", "Other"), ("1071", "2")]),
|
||||
style("Empty", "", &[]),
|
||||
);
|
||||
let d = cadcraft_io::read_dxf(dxf.as_bytes()).unwrap();
|
||||
let font = |n: &str| d.text_styles.iter().find(|s| s.name == n).map(|s| s.font.clone());
|
||||
assert_eq!(font("TT").as_deref(), Some("GOST type A"));
|
||||
assert_eq!(font("File").as_deref(), Some("arial.ttf"), "a font file wins over the typeface");
|
||||
assert_eq!(font("Empty").as_deref(), Some(""));
|
||||
}
|
||||
Reference in new issue
Block a user