Skip to main content

forme/font/
metrics.rs

1//! # Standard Font Metrics
2//!
3//! Glyph advance widths for the 14 standard PDF fonts.
4//! Data sourced from Adobe AFM files with correct WinAnsiEncoding order.
5//! Widths are in units of 1/1000 em (the standard AFM unit).
6//!
7//! Each array covers WinAnsiEncoding code points 32..=255 (224 entries).
8//! Index 0 = code 32 (space), index 223 = code 255 (ydieresis).
9
10use super::StandardFont;
11
12/// Map a Unicode codepoint to a WinAnsiEncoding byte value.
13///
14/// WinAnsiEncoding is based on Windows-1252. Most codepoints in
15/// 0x20..=0x7E and 0xA0..=0xFF map directly. The 0x80..=0x9F range
16/// contains special mappings for smart quotes, bullets, dashes, etc.
17pub fn unicode_to_winansi(ch: char) -> Option<u8> {
18    let cp = ch as u32;
19    // ASCII printable range maps directly
20    if (0x20..=0x7E).contains(&cp) || (0xA0..=0xFF).contains(&cp) {
21        return Some(cp as u8);
22    }
23    // Windows-1252 special mappings (0x80-0x9F)
24    match cp {
25        0x20AC => Some(0x80), // Euro sign
26        0x201A => Some(0x82), // Single low-9 quotation mark
27        0x0192 => Some(0x83), // Latin small letter f with hook
28        0x201E => Some(0x84), // Double low-9 quotation mark
29        0x2026 => Some(0x85), // Horizontal ellipsis
30        0x2020 => Some(0x86), // Dagger
31        0x2021 => Some(0x87), // Double dagger
32        0x02C6 => Some(0x88), // Modifier letter circumflex accent
33        0x2030 => Some(0x89), // Per mille sign
34        0x0160 => Some(0x8A), // Latin capital letter S with caron
35        0x2039 => Some(0x8B), // Single left-pointing angle quotation
36        0x0152 => Some(0x8C), // Latin capital ligature OE
37        0x017D => Some(0x8E), // Latin capital letter Z with caron
38        0x2018 => Some(0x91), // Left single quotation mark
39        0x2019 => Some(0x92), // Right single quotation mark
40        0x201C => Some(0x93), // Left double quotation mark
41        0x201D => Some(0x94), // Right double quotation mark
42        0x2022 => Some(0x95), // Bullet
43        0x2013 => Some(0x96), // En dash
44        0x2014 => Some(0x97), // Em dash
45        0x02DC => Some(0x98), // Small tilde
46        0x2122 => Some(0x99), // Trade mark sign
47        0x0161 => Some(0x9A), // Latin small letter s with caron
48        0x203A => Some(0x9B), // Single right-pointing angle quotation
49        0x0153 => Some(0x9C), // Latin small ligature oe
50        0x017E => Some(0x9E), // Latin small letter z with caron
51        0x0178 => Some(0x9F), // Latin capital letter Y with diaeresis
52        _ => None,
53    }
54}
55
56/// Reverse of `unicode_to_winansi`: the Unicode scalar a WinAnsiEncoding code
57/// maps to. Used when embedding a metric-compatible substitute to look up the
58/// substitute's own advance for a code (the PDF/A width carve-out).
59pub fn winansi_to_char(code: u8) -> Option<char> {
60    let cp = code as u32;
61    if (0x20..=0x7E).contains(&cp) || (0xA0..=0xFF).contains(&cp) {
62        return char::from_u32(cp);
63    }
64    let uni: u32 = match code {
65        0x80 => 0x20AC,
66        0x82 => 0x201A,
67        0x83 => 0x0192,
68        0x84 => 0x201E,
69        0x85 => 0x2026,
70        0x86 => 0x2020,
71        0x87 => 0x2021,
72        0x88 => 0x02C6,
73        0x89 => 0x2030,
74        0x8A => 0x0160,
75        0x8B => 0x2039,
76        0x8C => 0x0152,
77        0x8E => 0x017D,
78        0x91 => 0x2018,
79        0x92 => 0x2019,
80        0x93 => 0x201C,
81        0x94 => 0x201D,
82        0x95 => 0x2022,
83        0x96 => 0x2013,
84        0x97 => 0x2014,
85        0x98 => 0x02DC,
86        0x99 => 0x2122,
87        0x9A => 0x0161,
88        0x9B => 0x203A,
89        0x9C => 0x0153,
90        0x9E => 0x017E,
91        0x9F => 0x0178,
92        _ => return None,
93    };
94    char::from_u32(uni)
95}
96
97/// Glyph widths for a standard PDF font.
98/// Indexed by WinAnsiEncoding code point: index 0 = code 32, index 223 = code 255.
99/// Values are advance widths in units of 1/1000 em.
100pub struct StandardFontMetrics {
101    pub widths: &'static [u16; 224],
102    pub default_width: u16,
103}
104
105impl StandardFontMetrics {
106    /// Get the advance width of a character in points.
107    pub fn char_width(&self, ch: char, font_size: f64) -> f64 {
108        let code = ch as u32;
109        let w = if (32..=255).contains(&code) {
110            let idx = (code - 32) as usize;
111            let w = self.widths[idx];
112            if w > 0 {
113                w
114            } else {
115                self.default_width
116            }
117        } else if let Some(winansi) = unicode_to_winansi(ch) {
118            if winansi >= 32 {
119                let idx = (winansi as u32 - 32) as usize;
120                let w = self.widths[idx];
121                if w > 0 {
122                    w
123                } else {
124                    self.default_width
125                }
126            } else {
127                self.default_width
128            }
129        } else {
130            self.default_width
131        };
132        (w as f64 / 1000.0) * font_size
133    }
134
135    /// Measure the width of a string in points.
136    pub fn measure_string(&self, text: &str, font_size: f64, letter_spacing: f64) -> f64 {
137        let mut width = 0.0;
138        for ch in text.chars() {
139            width += self.char_width(ch, font_size) + letter_spacing;
140        }
141        width
142    }
143}
144
145impl StandardFont {
146    /// Get the metrics for this standard font.
147    pub fn metrics(&self) -> StandardFontMetrics {
148        match self {
149            Self::Helvetica => StandardFontMetrics {
150                widths: &HELVETICA_WIDTHS,
151                default_width: 278,
152            },
153            Self::HelveticaBold => StandardFontMetrics {
154                widths: &HELVETICA_BOLD_WIDTHS,
155                default_width: 278,
156            },
157            Self::HelveticaOblique => StandardFontMetrics {
158                widths: &HELVETICA_OBLIQUE_WIDTHS,
159                default_width: 278,
160            },
161            Self::HelveticaBoldOblique => StandardFontMetrics {
162                widths: &HELVETICA_BOLD_OBLIQUE_WIDTHS,
163                default_width: 278,
164            },
165            Self::TimesRoman => StandardFontMetrics {
166                widths: &TIMES_ROMAN_WIDTHS,
167                default_width: 250,
168            },
169            Self::TimesBold => StandardFontMetrics {
170                widths: &TIMES_BOLD_WIDTHS,
171                default_width: 250,
172            },
173            Self::TimesItalic => StandardFontMetrics {
174                widths: &TIMES_ITALIC_WIDTHS,
175                default_width: 250,
176            },
177            Self::TimesBoldItalic => StandardFontMetrics {
178                widths: &TIMES_BOLD_ITALIC_WIDTHS,
179                default_width: 250,
180            },
181            Self::Courier => StandardFontMetrics {
182                widths: &COURIER_WIDTHS,
183                default_width: 600,
184            },
185            Self::CourierBold => StandardFontMetrics {
186                widths: &COURIER_BOLD_WIDTHS,
187                default_width: 600,
188            },
189            Self::CourierOblique => StandardFontMetrics {
190                widths: &COURIER_OBLIQUE_WIDTHS,
191                default_width: 600,
192            },
193            Self::CourierBoldOblique => StandardFontMetrics {
194                widths: &COURIER_BOLD_OBLIQUE_WIDTHS,
195                default_width: 600,
196            },
197            Self::Symbol => StandardFontMetrics {
198                widths: &SYMBOL_WIDTHS,
199                default_width: 250,
200            },
201            Self::ZapfDingbats => StandardFontMetrics {
202                widths: &ZAPF_DINGBATS_WIDTHS,
203                default_width: 278,
204            },
205        }
206    }
207}
208
209// ─── Helvetica ───────────────────────────────────────────────────
210// AFM: Helvetica (Adobe Standard), WinAnsiEncoding order
211
212static HELVETICA_WIDTHS: [u16; 224] = [
213    // 32-126: ASCII
214    278, 278, 355, 556, 556, 889, 667, 222, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
215    556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
216    611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
217    667, 611, 278, 278, 278, 469, 556, 222, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
218    222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
219    // 127: DEL
220    0, // 128-159: WinAnsi specials (0x80-0x9F)
221    556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, 0, 222, 222, 333,
222    333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
223    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
224    278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333,
225    333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667,
226    1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778,
227    722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556,
228    278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556,
229    500,
230];
231
232static HELVETICA_BOLD_WIDTHS: [u16; 224] = [
233    // 32-126: ASCII
234    278, 333, 474, 556, 556, 889, 722, 278, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
235    556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
236    611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
237    667, 611, 333, 278, 333, 584, 556, 278, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
238    278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
239    // 127: DEL
240    0, // 128-159: WinAnsi specials (0x80-0x9F)
241    556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, 0, 278, 278, 500,
242    500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
243    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
244    278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333,
245    333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722,
246    1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778,
247    722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556,
248    278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611,
249    556,
250];
251
252// Helvetica-Oblique has identical widths to Helvetica
253static HELVETICA_OBLIQUE_WIDTHS: [u16; 224] = [
254    // 32-126: ASCII
255    278, 278, 355, 556, 556, 889, 667, 222, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
256    556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
257    611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
258    667, 611, 278, 278, 278, 469, 556, 222, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
259    222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
260    // 127: DEL
261    0, // 128-159: WinAnsi specials (0x80-0x9F)
262    556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, 0, 222, 222, 333,
263    333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
264    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
265    278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333,
266    333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667,
267    1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778,
268    722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556,
269    278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556,
270    500,
271];
272
273// Helvetica-BoldOblique has identical widths to Helvetica-Bold
274static HELVETICA_BOLD_OBLIQUE_WIDTHS: [u16; 224] = [
275    // 32-126: ASCII
276    278, 333, 474, 556, 556, 889, 722, 278, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
277    556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
278    611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
279    667, 611, 333, 278, 333, 584, 556, 278, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
280    278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
281    // 127: DEL
282    0, // 128-159: WinAnsi specials (0x80-0x9F)
283    556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, 0, 278, 278, 500,
284    500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
285    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
286    278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333,
287    333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722,
288    1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778,
289    722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556,
290    278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611,
291    556,
292];
293
294// ─── Times ───────────────────────────────────────────────────────
295
296static TIMES_ROMAN_WIDTHS: [u16; 224] = [
297    // 32-126: ASCII
298    250, 333, 408, 500, 500, 833, 778, 333, 333, 333, 500, 564, 250, 333, 250, 278, 500, 500, 500,
299    500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444, 921, 722, 667, 667, 722, 611,
300    556, 722, 722, 333, 389, 722, 611, 889, 722, 722, 556, 722, 667, 556, 611, 722, 722, 944, 722,
301    722, 611, 333, 278, 333, 469, 500, 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500,
302    278, 778, 500, 500, 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541,
303    // 127: DEL
304    0, // 128-159: WinAnsi specials (0x80-0x9F)
305    500, 0, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 0, 611, 0, 0, 333, 333, 444,
306    444, 350, 500, 1000, 333, 980, 389, 333, 722, 0, 444, 722,
307    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
308    250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333, 400, 564, 300,
309    300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444, 722, 722, 722, 722, 722, 722,
310    889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 722, 722, 722, 722, 722, 722, 564, 722,
311    722, 722, 722, 722, 722, 556, 500, 444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444,
312    278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500,
313    500,
314];
315
316static TIMES_BOLD_WIDTHS: [u16; 224] = [
317    // 32-126: ASCII
318    250, 333, 555, 500, 500, 1000, 833, 333, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
319    500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 930, 722, 667, 722, 722, 667,
320    611, 778, 778, 389, 500, 778, 667, 944, 722, 778, 611, 778, 722, 556, 667, 722, 722, 1000, 722,
321    722, 667, 333, 278, 333, 581, 500, 333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556,
322    278, 833, 556, 500, 556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520,
323    // 127: DEL
324    0, // 128-159: WinAnsi specials (0x80-0x9F)
325    500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 0, 667, 0, 0, 333, 333, 500,
326    500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 444, 722,
327    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
328    250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333, 400, 570, 300,
329    300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500, 722, 722, 722, 722, 722, 722,
330    1000, 722, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 778, 778, 778, 778, 778, 570, 778,
331    722, 722, 722, 722, 722, 611, 556, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444,
332    278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556,
333    500,
334];
335
336static TIMES_ITALIC_WIDTHS: [u16; 224] = [
337    // 32-126: ASCII
338    250, 333, 420, 500, 500, 833, 778, 333, 333, 333, 500, 675, 250, 333, 250, 278, 500, 500, 500,
339    500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500, 920, 611, 611, 667, 722, 611,
340    611, 722, 722, 333, 444, 667, 556, 833, 667, 722, 611, 722, 611, 500, 556, 722, 611, 833, 611,
341    556, 556, 389, 278, 389, 422, 500, 333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444,
342    278, 722, 500, 500, 500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541,
343    // 127: DEL
344    0, // 128-159: WinAnsi specials (0x80-0x9F)
345    500, 0, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 0, 556, 0, 0, 333, 333, 556,
346    556, 350, 500, 889, 333, 980, 389, 333, 667, 0, 389, 556,
347    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
348    250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333, 400, 675, 300,
349    300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500, 611, 611, 611, 611, 611, 611,
350    889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 667, 722, 722, 722, 722, 722, 675, 722,
351    722, 722, 722, 722, 556, 611, 500, 500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444,
352    278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500,
353    444,
354];
355
356static TIMES_BOLD_ITALIC_WIDTHS: [u16; 224] = [
357    // 32-126: ASCII
358    250, 389, 555, 500, 500, 833, 778, 333, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
359    500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 832, 667, 667, 667, 722, 667,
360    667, 722, 778, 389, 500, 667, 611, 889, 722, 722, 611, 722, 667, 556, 611, 722, 667, 889, 667,
361    611, 611, 333, 278, 333, 570, 500, 333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500,
362    278, 778, 556, 500, 500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570,
363    // 127: DEL
364    0, // 128-159: WinAnsi specials (0x80-0x9F)
365    500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 0, 611, 0, 0, 333, 333, 500,
366    500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 389, 611,
367    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
368    250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 570, 333, 747, 333, 400, 570, 300,
369    300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500, 667, 667, 667, 667, 667, 667,
370    944, 667, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 722, 722, 722, 722, 722, 570, 722,
371    722, 722, 722, 722, 611, 611, 500, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444,
372    278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500,
373    444,
374];
375
376// ─── Courier (monospaced — all 600) ─────────────────────────────
377
378static COURIER_WIDTHS: [u16; 224] = [
379    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
380    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
381    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
382    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
383    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
384    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
385    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
386    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
387    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
388    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
389    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
390    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
391];
392
393static COURIER_BOLD_WIDTHS: [u16; 224] = [
394    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
395    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
396    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
397    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
398    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
399    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
400    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
401    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
402    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
403    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
404    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
405    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
406];
407
408static COURIER_OBLIQUE_WIDTHS: [u16; 224] = [
409    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
410    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
411    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
412    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
413    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
414    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
415    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
416    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
417    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
418    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
419    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
420    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
421];
422
423static COURIER_BOLD_OBLIQUE_WIDTHS: [u16; 224] = [
424    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
425    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
426    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
427    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
428    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
429    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
430    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
431    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
432    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
433    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
434    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
435    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
436];
437
438// ─── Symbol & ZapfDingbats (own encodings, unaffected) ──────────
439
440static SYMBOL_WIDTHS: [u16; 224] = [
441    250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278, 500, 500, 500,
442    500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444, 549, 722, 667, 722, 612, 611,
443    763, 603, 722, 333, 631, 722, 686, 889, 722, 722, 768, 741, 556, 592, 611, 690, 439, 768, 645,
444    795, 611, 333, 863, 333, 658, 500, 500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549,
445    549, 576, 521, 549, 549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549,
446    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
447    0, 750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603, 400, 549,
448    411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658, 823, 686, 795, 987,
449    768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713, 768, 713, 790, 790, 890, 823, 549,
450    250, 713, 603, 603, 1042, 987, 603, 987, 603, 494, 329, 790, 790, 786, 713, 384, 384, 384, 384,
451    384, 384, 494, 494, 494, 494, 0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494,
452    494, 494, 0,
453];
454
455static ZAPF_DINGBATS_WIDTHS: [u16; 224] = [
456    278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933, 911, 945, 974,
457    755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537, 577, 692, 786, 788, 788, 790,
458    793, 794, 816, 823, 789, 841, 823, 833, 816, 831, 923, 744, 723, 749, 790, 792, 695, 776, 768,
459    792, 759, 707, 708, 682, 701, 826, 815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785,
460    791, 873, 761, 762, 762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668,
461    0, 390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0, 0, 0, 0, 0, 0,
462    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788,
463    788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
464    788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
465    788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834, 873, 828, 924, 924, 917, 930,
466    931, 463, 883, 836, 836, 867, 867, 696, 696, 874, 0, 874, 760, 946, 771, 865, 771, 888, 967,
467    888, 831, 873, 927, 970, 918, 0,
468];
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_helvetica_known_widths() {
476        let m = StandardFont::Helvetica.metrics();
477        // space = 278, A = 667, m = 833
478        assert_eq!(m.widths[0], 278); // space (code 32)
479        assert_eq!(m.widths[33], 667); // A (code 65)
480        assert_eq!(m.widths[77], 833); // m (code 109)
481    }
482
483    #[test]
484    fn test_helvetica_bold_wider() {
485        let regular = StandardFont::Helvetica.metrics();
486        let bold = StandardFont::HelveticaBold.metrics();
487        // Bold A = 722 > regular A = 667
488        assert_eq!(bold.widths[33], 722);
489        assert!(bold.widths[33] > regular.widths[33]);
490    }
491
492    #[test]
493    fn test_courier_monospaced() {
494        let m = StandardFont::Courier.metrics();
495        // All Courier glyphs are 600
496        for &w in m.widths.iter() {
497            if w > 0 {
498                assert_eq!(w, 600, "Courier should be monospaced");
499            }
500        }
501    }
502
503    #[test]
504    fn test_char_width_calculation() {
505        let m = StandardFont::Helvetica.metrics();
506        // space at 12pt: 278/1000 * 12 = 3.336
507        let w = m.char_width(' ', 12.0);
508        assert!((w - 3.336).abs() < 0.001);
509    }
510
511    #[test]
512    fn test_measure_string() {
513        let m = StandardFont::Helvetica.metrics();
514        let w = m.measure_string("A", 12.0, 0.0);
515        assert!((w - 8.004).abs() < 0.001); // 667/1000 * 12
516    }
517
518    #[test]
519    fn test_char_width_em_dash_uses_winansi() {
520        let m = StandardFont::Helvetica.metrics();
521        // Em dash (U+2014) maps to WinAnsi 0x97 = 151, index 151-32 = 119
522        // Helvetica em dash width = 1000
523        let w = m.char_width('\u{2014}', 10.0);
524        assert!(
525            (w - 10.0).abs() < 0.001,
526            "em dash should be 1000/1000 * 10 = 10.0, got {}",
527            w
528        );
529    }
530
531    #[test]
532    fn test_char_width_en_dash_uses_winansi() {
533        let m = StandardFont::Helvetica.metrics();
534        // En dash (U+2013) maps to WinAnsi 0x96 = 150, index 150-32 = 118
535        // Helvetica en dash width = 556
536        let w = m.char_width('\u{2013}', 10.0);
537        assert!(
538            (w - 5.56).abs() < 0.001,
539            "en dash should be 556/1000 * 10 = 5.56, got {}",
540            w
541        );
542    }
543
544    #[test]
545    fn test_unicode_to_winansi_mappings() {
546        assert_eq!(unicode_to_winansi('\u{2014}'), Some(0x97)); // Em dash
547        assert_eq!(unicode_to_winansi('\u{2013}'), Some(0x96)); // En dash
548        assert_eq!(unicode_to_winansi('\u{201C}'), Some(0x93)); // Left double quote
549        assert_eq!(unicode_to_winansi('\u{2026}'), Some(0x85)); // Ellipsis
550        assert_eq!(unicode_to_winansi('A'), Some(0x41)); // ASCII
551        assert_eq!(unicode_to_winansi('\u{4E00}'), None); // CJK — not in WinAnsi
552    }
553
554    #[test]
555    fn test_latin_extended_character_widths() {
556        let m = StandardFont::Helvetica.metrics();
557        // Accented capitals must have the same width as their base character
558        let a_width = m.widths[33]; // A = 667
559        assert_eq!(m.widths[0xC0 - 32], a_width, "Agrave should match A"); // À
560        assert_eq!(m.widths[0xC1 - 32], a_width, "Aacute should match A"); // Á
561        assert_eq!(m.widths[0xC2 - 32], a_width, "Acircumflex should match A"); // Â
562        assert_eq!(m.widths[0xC3 - 32], a_width, "Atilde should match A"); // Ã
563        assert_eq!(m.widths[0xC4 - 32], a_width, "Adieresis should match A"); // Ä
564        assert_eq!(m.widths[0xC5 - 32], a_width, "Aring should match A"); // Å
565
566        // Verify char_width gives correct results for Latin Extended
567        let w_a_dieresis = m.char_width('Ä', 10.0);
568        let w_a = m.char_width('A', 10.0);
569        assert!(
570            (w_a_dieresis - w_a).abs() < 0.001,
571            "Ä width ({}) should equal A width ({})",
572            w_a_dieresis,
573            w_a
574        );
575
576        let w_a_ring = m.char_width('Å', 10.0);
577        assert!(
578            (w_a_ring - w_a).abs() < 0.001,
579            "Å width ({}) should equal A width ({})",
580            w_a_ring,
581            w_a
582        );
583
584        // O-dieresis should match O
585        let o_width = m.widths[0x4F - 32]; // O = 778
586        assert_eq!(m.widths[0xD6 - 32], o_width, "Odieresis should match O");
587
588        // AE should be 1000
589        assert_eq!(m.widths[0xC6 - 32], 1000, "AE should be 1000");
590
591        // Verify sequential characters don't stack (each x position increases)
592        let test_str = "ÅÄÖÉÈÊÑÜÚÙû";
593        let mut prev_x = 0.0;
594        for (i, ch) in test_str.chars().enumerate() {
595            let w = m.char_width(ch, 12.0);
596            assert!(
597                w > 1.0,
598                "Character '{}' (U+{:04X}) has suspiciously small width: {}",
599                ch,
600                ch as u32,
601                w
602            );
603            if i > 0 {
604                assert!(
605                    prev_x > 0.0,
606                    "Previous character should have non-zero width"
607                );
608            }
609            prev_x += w;
610        }
611        // Total width should be reasonable (not near-zero from stacking)
612        assert!(
613            prev_x > 50.0,
614            "Total width of '{}' at 12pt should be >50pt, got {}",
615            test_str,
616            prev_x
617        );
618    }
619
620    #[test]
621    fn test_latin_extended_widths_all_fonts() {
622        // Verify accented characters have correct widths across all proportional fonts
623        let fonts = [
624            StandardFont::Helvetica,
625            StandardFont::HelveticaBold,
626            StandardFont::HelveticaOblique,
627            StandardFont::HelveticaBoldOblique,
628            StandardFont::TimesRoman,
629            StandardFont::TimesBold,
630            StandardFont::TimesItalic,
631            StandardFont::TimesBoldItalic,
632        ];
633
634        for font in &fonts {
635            let m = font.metrics();
636            // Ä (0xC4) should have same width as A (0x41)
637            let a_width = m.widths[0x41 - 32];
638            let a_dieresis_width = m.widths[0xC4 - 32];
639            assert_eq!(
640                a_dieresis_width, a_width,
641                "{:?}: Ä width ({}) != A width ({})",
642                font, a_dieresis_width, a_width
643            );
644
645            // Å (0xC5) should have same width as A (0x41)
646            let a_ring_width = m.widths[0xC5 - 32];
647            assert_eq!(
648                a_ring_width, a_width,
649                "{:?}: Å width ({}) != A width ({})",
650                font, a_ring_width, a_width
651            );
652
653            // ö (0xF6) should have same width as o (0x6F)
654            let o_width = m.widths[0x6F - 32];
655            let o_dieresis_width = m.widths[0xF6 - 32];
656            assert_eq!(
657                o_dieresis_width, o_width,
658                "{:?}: ö width ({}) != o width ({})",
659                font, o_dieresis_width, o_width
660            );
661        }
662    }
663
664    #[test]
665    fn test_page_placeholder_width_difference() {
666        let m = StandardFont::Helvetica.metrics();
667        let literal = m.measure_string("{{pageNumber}}", 12.0, 0.0);
668        let substituted = m.measure_string("00", 12.0, 0.0);
669        // "{{pageNumber}}" is 16 chars, "00" is 2 chars — massive difference
670        assert!(
671            literal > substituted * 3.0,
672            "Literal placeholder ({}) is much wider than substituted ({})",
673            literal,
674            substituted
675        );
676    }
677}