Skip to main content

edgeparse_core/pdf/
font_style.rs

1//! Shared font name / weight helpers (DRY across Type1 / Type3 / TrueType).
2
3/// Prefer `/BaseFont`, then `FontDescriptor /FontName`, else `"Unknown"`.
4pub fn resolve_base_font_name(
5    base_font: Option<&str>,
6    descriptor_font_name: Option<&str>,
7) -> String {
8    if let Some(name) = base_font {
9        let trimmed = name.trim();
10        if !trimmed.is_empty() && trimmed != "Unknown" {
11            return trimmed.to_string();
12        }
13    }
14    if let Some(name) = descriptor_font_name {
15        let trimmed = name.trim();
16        if !trimmed.is_empty() {
17            return trimmed.to_string();
18        }
19    }
20    "Unknown".to_string()
21}
22
23/// Parse a CSS / OpenType weight from a PostScript or variable-font name.
24///
25/// Handles:
26/// - Classic names: `Bold`, `Black`, `Heavy`, `Light`, `Thin`, `Regular`
27/// - Variable-font tokens: `wght600` (CSS) or `wght45875200` (16.16 fixed-point)
28///
29/// Returns `None` when the name carries no weight signal. Ambiguous Skia
30/// packing such as `wght2580000` (not CSS, not 16.16) is ignored — callers
31/// should prefer `/FontWeight` from the font descriptor.
32pub fn weight_from_font_name(name: &str) -> Option<f64> {
33    let lower = name.to_lowercase();
34
35    if let Some(idx) = lower.find("wght") {
36        let rest = &lower[idx + 4..];
37        let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
38        if let Ok(raw) = digits.parse::<u64>() {
39            if (1..=1000).contains(&raw) {
40                return Some(raw as f64);
41            }
42            // 16.16 fixed-point (CSS_weight << 16)
43            if raw > 65_536 {
44                let css = (raw as f64) / 65536.0;
45                if (1.0..=1000.0).contains(&css) {
46                    return Some(css.round());
47                }
48            }
49        }
50    }
51
52    if lower.contains("black") || lower.contains("heavy") {
53        return Some(900.0);
54    }
55    if lower.contains("bold") || lower.contains("semibold") || lower.contains("demibold") {
56        return Some(700.0);
57    }
58    if lower.contains("medium") {
59        return Some(500.0);
60    }
61    if lower.contains("light") {
62        return Some(300.0);
63    }
64    if lower.contains("thin") || lower.contains("hairline") {
65        return Some(100.0);
66    }
67    if lower.contains("regular") || lower.contains("book") || lower.contains("normal") {
68        return Some(400.0);
69    }
70    None
71}
72
73/// Resolve final numeric weight: `/FontWeight` → name token → StemV.
74pub fn resolve_font_weight(
75    font_name: &str,
76    descriptor_font_weight: Option<f64>,
77    stem_v: f64,
78) -> f64 {
79    // Explicit descriptor weight is authoritative (Skia publishes FontWeight=600
80    // while StemV=262 would otherwise look "bold").
81    if let Some(w) = descriptor_font_weight {
82        if (1.0..=1000.0).contains(&w) {
83            return w;
84        }
85    }
86    if let Some(w) = weight_from_font_name(font_name) {
87        return w;
88    }
89    // StemV heuristic (legacy)
90    if stem_v >= 140.0 {
91        700.0
92    } else if stem_v >= 100.0 {
93        500.0
94    } else {
95        400.0
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn base_font_preferred() {
105        assert_eq!(
106            resolve_base_font_name(Some("Helvetica-Bold"), Some("Other")),
107            "Helvetica-Bold"
108        );
109    }
110
111    #[test]
112    fn falls_back_to_descriptor_name() {
113        assert_eq!(
114            resolve_base_font_name(None, Some("AAAAAA+.SFNS-Regular")),
115            "AAAAAA+.SFNS-Regular"
116        );
117        assert_eq!(
118            resolve_base_font_name(Some("Unknown"), Some("DescName")),
119            "DescName"
120        );
121    }
122
123    #[test]
124    fn weight_from_classic_names() {
125        assert_eq!(weight_from_font_name("Helvetica-Bold"), Some(700.0));
126        assert_eq!(weight_from_font_name("Arial-Black"), Some(900.0));
127        assert_eq!(weight_from_font_name("Courier"), None);
128    }
129
130    #[test]
131    fn weight_from_wght_token_css() {
132        assert_eq!(weight_from_font_name("Family_wght600_opsz"), Some(600.0));
133    }
134
135    #[test]
136    fn weight_from_wght_16_16() {
137        // 700 << 16 = 45_875_200
138        assert_eq!(weight_from_font_name("Font_wght45875200"), Some(700.0));
139    }
140
141    #[test]
142    fn resolve_prefers_fontweight_over_stemv() {
143        // Skia: FontWeight=600, StemV=262 would otherwise look bold
144        let w = resolve_font_weight("AAAA+.SFNS-Regular_wght2580000", Some(600.0), 262.0);
145        assert!((w - 600.0).abs() < 1e-6, "w={w}");
146    }
147
148    #[test]
149    fn resolve_uses_name_when_no_fontweight() {
150        let w = resolve_font_weight("Helvetica-Bold", None, 80.0);
151        assert!((w - 700.0).abs() < 1e-6);
152    }
153
154    #[test]
155    fn resolve_uses_fontweight_when_no_name_signal() {
156        let w = resolve_font_weight("CustomEmbed", Some(600.0), 262.0);
157        assert!((w - 600.0).abs() < 1e-6);
158    }
159}