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
@@ -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::*;