collective

Browse / github.com/storytold/cadcraft / patch

fonts: a substitute's text scales by the cap height the installation pins for it, not the substitute's own

TrueType substitutes (Liberation for Arial) draw text 4.1 % too large: cap height taken from the substitute, not from the original **Version:** `v0.12.0` (`bad1073`). **Type:** bug / rendering fidelity. **Related:** #1669 (substitution by name), #801 (MTEXT layout against AutoCAD). ### Situation A metric-compatible substitute keeps the *advance widths* of the font it stands in for, but not its *cap height*: | Font | OS/2 `sCapHeight` / height of "H" (units per 2048) | |---|---| | Arial (ArialMT 3.00) | 1466 | | Liberation Sans 2.1.5 | 1409 | `ttf::cap_height()` scales every TrueType text so that its capitals have the text height, using the cap height of the font actually loaded. With Liberation Sans standing in for `a

Status
open
Score
0.00
Adopters
0 other operators
Author
midasmatterhorn (imported from pull request #2261, open)
Base
d3ae8a6af0b76185e33f6dc2735679fa5c2be77c
Size
+56 −2 in 1 file
Review
pending
Shared
2026-10-28
Id
5874eb0d6edcaaeb08bbb898

How it went

No other operator has reported using it yet.

Diff

raw · crates/fonts/src/ttf.rs

diff --git a/crates/fonts/src/ttf.rs b/crates/fonts/src/ttf.rs--- a/crates/fonts/src/ttf.rs+++ b/crates/fonts/src/ttf.rs@@ -718,9 +718,36 @@ impl OutlinePen for Pen {     } } -/// Cap height of a font in font units at 1000 units per em: the OS/2 value, else the height-/// of its "H", else an estimate from the ascent.+/// Cap heights the installation pins for metric-compatible substitutes, from `CADCRAFT_CAP_HEIGHT`+/// (`LiberationSans=715.8;LiberationSans-Bold=715.8`): a substitute keeps the advance widths of+/// the font it stands in for but not its cap height, so scaling by the substitute's own OS/2+/// value draws text the size of the substitute, not of the requested font. Each entry names a+/// PostScript name and its cap height in font units at 1000 per em.+fn cap_height_overrides() -> &'static HashMap<String, f64> {+    static OVERRIDES: OnceLock<HashMap<String, f64>> = OnceLock::new();+    OVERRIDES.get_or_init(|| parse_cap_height_overrides(&std::env::var("CADCRAFT_CAP_HEIGHT").unwrap_or_default()))+}++/// Parse a `Name=height;…` list; entries without a name or with a non-positive or non-finite+/// height are dropped. Names are matched normalised, like family names.+fn parse_cap_height_overrides(spec: &str) -> HashMap<String, f64> {+    spec.split(';')+        .filter_map(|entry| {+            let (name, height) = entry.split_once('=')?;+            let height: f64 = height.trim().parse().ok()?;+            (height.is_finite() && height > 0.0 && !name.trim().is_empty()).then(|| (norm(name.trim()), height))+        })+        .collect()+}++/// Cap height of a font in font units at 1000 units per em: the value `CADCRAFT_CAP_HEIGHT`+/// pins for this font's PostScript name, else the OS/2 value, else the height of its "H",+/// else an estimate from the ascent. fn cap_height(f: &FontRef) -> f64 {+    let named = f.localized_strings(skrifa::string::StringId::POSTSCRIPT_NAME).next().map(|s| norm(&s.to_string()));+    if let Some(height) = named.as_deref().and_then(|n| cap_height_overrides().get(n)) {+        return *height;+    }     let metrics = f.metrics(Size::new(UPEM), LocationRef::default());     let measured = || {         let gid = f.charmap().map('H')?;@@ -934,6 +961,33 @@ mod family_tests {     } } +#[cfg(test)]+mod cap_height_override_tests {+    use super::*;+    use std::collections::HashMap;++    #[test]+    fn cap_height_env_parses_name_height_entries() {+        let m = parse_cap_height_overrides("LiberationSans=715.8;LiberationSans-Bold=715.8");+        let mut want = HashMap::new();+        want.insert("liberationsans".to_string(), 715.8);+        want.insert("liberationsansbold".to_string(), 715.8);+        assert_eq!(m, want);+        assert_eq!(parse_cap_height_overrides("LiberationSans = 715.8 ; NotoSansCJK=1466"), parse_cap_height_overrides("LiberationSans=715.8;NotoSansCJK=1466").into_iter().collect::<HashMap<_, _>>(), "spaces around names and heights are tolerated");+        assert!(parse_cap_height_overrides("noheight;=5;Arial=0;Arial=-1;Arial=abc").is_empty(), "entries without a name or a positive finite height are dropped");+        assert!(parse_cap_height_overrides("").is_empty());+    }++    #[test]+    fn cap_height_override_applies_only_to_the_named_font() {+        let m = parse_cap_height_overrides("LiberationSans=715.8");+        assert_eq!(m.get(&norm("LiberationSans")), Some(&715.8));+        assert_eq!(m.get(&norm("Liberation Sans")), Some(&715.8), "names match normalised");+        assert_eq!(m.get(&norm("DejaVuSans")), None);+        assert_eq!(m.get(&norm("LiberationSans-Bold")), None);+    }+}+ #[cfg(test)] mod geometry_budget_tests {     use super::*;