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/// Glyph widths for a standard PDF font.
57/// Indexed by WinAnsiEncoding code point: index 0 = code 32, index 223 = code 255.
58/// Values are advance widths in units of 1/1000 em.
59pub struct StandardFontMetrics {
60    pub widths: &'static [u16; 224],
61    pub default_width: u16,
62}
63
64impl StandardFontMetrics {
65    /// Get the advance width of a character in points.
66    pub fn char_width(&self, ch: char, font_size: f64) -> f64 {
67        let code = ch as u32;
68        let w = if (32..=255).contains(&code) {
69            let idx = (code - 32) as usize;
70            let w = self.widths[idx];
71            if w > 0 {
72                w
73            } else {
74                self.default_width
75            }
76        } else if let Some(winansi) = unicode_to_winansi(ch) {
77            if winansi >= 32 {
78                let idx = (winansi as u32 - 32) as usize;
79                let w = self.widths[idx];
80                if w > 0 {
81                    w
82                } else {
83                    self.default_width
84                }
85            } else {
86                self.default_width
87            }
88        } else {
89            self.default_width
90        };
91        (w as f64 / 1000.0) * font_size
92    }
93
94    /// Measure the width of a string in points.
95    pub fn measure_string(&self, text: &str, font_size: f64, letter_spacing: f64) -> f64 {
96        let mut width = 0.0;
97        for ch in text.chars() {
98            width += self.char_width(ch, font_size) + letter_spacing;
99        }
100        width
101    }
102}
103
104impl StandardFont {
105    /// Get the metrics for this standard font.
106    pub fn metrics(&self) -> StandardFontMetrics {
107        match self {
108            Self::Helvetica => StandardFontMetrics {
109                widths: &HELVETICA_WIDTHS,
110                default_width: 278,
111            },
112            Self::HelveticaBold => StandardFontMetrics {
113                widths: &HELVETICA_BOLD_WIDTHS,
114                default_width: 278,
115            },
116            Self::HelveticaOblique => StandardFontMetrics {
117                widths: &HELVETICA_OBLIQUE_WIDTHS,
118                default_width: 278,
119            },
120            Self::HelveticaBoldOblique => StandardFontMetrics {
121                widths: &HELVETICA_BOLD_OBLIQUE_WIDTHS,
122                default_width: 278,
123            },
124            Self::TimesRoman => StandardFontMetrics {
125                widths: &TIMES_ROMAN_WIDTHS,
126                default_width: 250,
127            },
128            Self::TimesBold => StandardFontMetrics {
129                widths: &TIMES_BOLD_WIDTHS,
130                default_width: 250,
131            },
132            Self::TimesItalic => StandardFontMetrics {
133                widths: &TIMES_ITALIC_WIDTHS,
134                default_width: 250,
135            },
136            Self::TimesBoldItalic => StandardFontMetrics {
137                widths: &TIMES_BOLD_ITALIC_WIDTHS,
138                default_width: 250,
139            },
140            Self::Courier => StandardFontMetrics {
141                widths: &COURIER_WIDTHS,
142                default_width: 600,
143            },
144            Self::CourierBold => StandardFontMetrics {
145                widths: &COURIER_BOLD_WIDTHS,
146                default_width: 600,
147            },
148            Self::CourierOblique => StandardFontMetrics {
149                widths: &COURIER_OBLIQUE_WIDTHS,
150                default_width: 600,
151            },
152            Self::CourierBoldOblique => StandardFontMetrics {
153                widths: &COURIER_BOLD_OBLIQUE_WIDTHS,
154                default_width: 600,
155            },
156            Self::Symbol => StandardFontMetrics {
157                widths: &SYMBOL_WIDTHS,
158                default_width: 250,
159            },
160            Self::ZapfDingbats => StandardFontMetrics {
161                widths: &ZAPF_DINGBATS_WIDTHS,
162                default_width: 278,
163            },
164        }
165    }
166}
167
168// ─── Helvetica ───────────────────────────────────────────────────
169// AFM: Helvetica (Adobe Standard), WinAnsiEncoding order
170
171static HELVETICA_WIDTHS: [u16; 224] = [
172    // 32-126: ASCII
173    278, 278, 355, 556, 556, 889, 667, 222, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
174    556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
175    611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
176    667, 611, 278, 278, 278, 469, 556, 222, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
177    222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
178    // 127: DEL
179    0, // 128-159: WinAnsi specials (0x80-0x9F)
180    556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, 0, 222, 222, 333,
181    333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
182    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
183    278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333,
184    333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667,
185    1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778,
186    722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556,
187    278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556,
188    500,
189];
190
191static HELVETICA_BOLD_WIDTHS: [u16; 224] = [
192    // 32-126: ASCII
193    278, 333, 474, 556, 556, 889, 722, 278, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
194    556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
195    611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
196    667, 611, 333, 278, 333, 584, 556, 278, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
197    278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
198    // 127: DEL
199    0, // 128-159: WinAnsi specials (0x80-0x9F)
200    556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, 0, 278, 278, 500,
201    500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
202    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
203    278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333,
204    333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722,
205    1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778,
206    722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556,
207    278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611,
208    556,
209];
210
211// Helvetica-Oblique has identical widths to Helvetica
212static HELVETICA_OBLIQUE_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
232// Helvetica-BoldOblique has identical widths to Helvetica-Bold
233static HELVETICA_BOLD_OBLIQUE_WIDTHS: [u16; 224] = [
234    // 32-126: ASCII
235    278, 333, 474, 556, 556, 889, 722, 278, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
236    556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
237    611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
238    667, 611, 333, 278, 333, 584, 556, 278, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
239    278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
240    // 127: DEL
241    0, // 128-159: WinAnsi specials (0x80-0x9F)
242    556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, 0, 278, 278, 500,
243    500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
244    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
245    278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333,
246    333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722,
247    1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778,
248    722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556,
249    278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611,
250    556,
251];
252
253// ─── Times ───────────────────────────────────────────────────────
254
255static TIMES_ROMAN_WIDTHS: [u16; 224] = [
256    // 32-126: ASCII
257    250, 333, 408, 500, 500, 833, 778, 333, 333, 333, 500, 564, 250, 333, 250, 278, 500, 500, 500,
258    500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444, 921, 722, 667, 667, 722, 611,
259    556, 722, 722, 333, 389, 722, 611, 889, 722, 722, 556, 722, 667, 556, 611, 722, 722, 944, 722,
260    722, 611, 333, 278, 333, 469, 500, 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500,
261    278, 778, 500, 500, 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541,
262    // 127: DEL
263    0, // 128-159: WinAnsi specials (0x80-0x9F)
264    500, 0, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 0, 611, 0, 0, 333, 333, 444,
265    444, 350, 500, 1000, 333, 980, 389, 333, 722, 0, 444, 722,
266    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
267    250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333, 400, 564, 300,
268    300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444, 722, 722, 722, 722, 722, 722,
269    889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 722, 722, 722, 722, 722, 722, 564, 722,
270    722, 722, 722, 722, 722, 556, 500, 444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444,
271    278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500,
272    500,
273];
274
275static TIMES_BOLD_WIDTHS: [u16; 224] = [
276    // 32-126: ASCII
277    250, 333, 555, 500, 500, 1000, 833, 333, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
278    500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 930, 722, 667, 722, 722, 667,
279    611, 778, 778, 389, 500, 778, 667, 944, 722, 778, 611, 778, 722, 556, 667, 722, 722, 1000, 722,
280    722, 667, 333, 278, 333, 581, 500, 333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556,
281    278, 833, 556, 500, 556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520,
282    // 127: DEL
283    0, // 128-159: WinAnsi specials (0x80-0x9F)
284    500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 0, 667, 0, 0, 333, 333, 500,
285    500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 444, 722,
286    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
287    250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333, 400, 570, 300,
288    300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500, 722, 722, 722, 722, 722, 722,
289    1000, 722, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 778, 778, 778, 778, 778, 570, 778,
290    722, 722, 722, 722, 722, 611, 556, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444,
291    278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556,
292    500,
293];
294
295static TIMES_ITALIC_WIDTHS: [u16; 224] = [
296    // 32-126: ASCII
297    250, 333, 420, 500, 500, 833, 778, 333, 333, 333, 500, 675, 250, 333, 250, 278, 500, 500, 500,
298    500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500, 920, 611, 611, 667, 722, 611,
299    611, 722, 722, 333, 444, 667, 556, 833, 667, 722, 611, 722, 611, 500, 556, 722, 611, 833, 611,
300    556, 556, 389, 278, 389, 422, 500, 333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444,
301    278, 722, 500, 500, 500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541,
302    // 127: DEL
303    0, // 128-159: WinAnsi specials (0x80-0x9F)
304    500, 0, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 0, 556, 0, 0, 333, 333, 556,
305    556, 350, 500, 889, 333, 980, 389, 333, 667, 0, 389, 556,
306    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
307    250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333, 400, 675, 300,
308    300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500, 611, 611, 611, 611, 611, 611,
309    889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 667, 722, 722, 722, 722, 722, 675, 722,
310    722, 722, 722, 722, 556, 611, 500, 500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444,
311    278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500,
312    444,
313];
314
315static TIMES_BOLD_ITALIC_WIDTHS: [u16; 224] = [
316    // 32-126: ASCII
317    250, 389, 555, 500, 500, 833, 778, 333, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
318    500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 832, 667, 667, 667, 722, 667,
319    667, 722, 778, 389, 500, 667, 611, 889, 722, 722, 611, 722, 667, 556, 611, 722, 667, 889, 667,
320    611, 611, 333, 278, 333, 570, 500, 333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500,
321    278, 778, 556, 500, 500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570,
322    // 127: DEL
323    0, // 128-159: WinAnsi specials (0x80-0x9F)
324    500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 0, 611, 0, 0, 333, 333, 500,
325    500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 389, 611,
326    // 160-255: Latin-1 Supplement (0xA0-0xFF) — WinAnsi order from Adobe AFM
327    250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 570, 333, 747, 333, 400, 570, 300,
328    300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500, 667, 667, 667, 667, 667, 667,
329    944, 667, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 722, 722, 722, 722, 722, 570, 722,
330    722, 722, 722, 722, 611, 611, 500, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444,
331    278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500,
332    444,
333];
334
335// ─── Courier (monospaced — all 600) ─────────────────────────────
336
337static COURIER_WIDTHS: [u16; 224] = [
338    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
339    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
340    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
341    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
342    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
343    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
344    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
345    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
346    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
347    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
348    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
349    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
350];
351
352static COURIER_BOLD_WIDTHS: [u16; 224] = [
353    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
354    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
355    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
356    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
357    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
358    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
359    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
360    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
361    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
362    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
363    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
364    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
365];
366
367static COURIER_OBLIQUE_WIDTHS: [u16; 224] = [
368    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
369    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
370    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
371    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
372    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
373    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
374    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
375    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
376    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
377    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
378    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
379    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
380];
381
382static COURIER_BOLD_OBLIQUE_WIDTHS: [u16; 224] = [
383    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
384    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
385    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 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    0, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0, 0, 600, 600, 600,
389    600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600,
390    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
391    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
392    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
393    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
394    600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
395];
396
397// ─── Symbol & ZapfDingbats (own encodings, unaffected) ──────────
398
399static SYMBOL_WIDTHS: [u16; 224] = [
400    250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278, 500, 500, 500,
401    500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444, 549, 722, 667, 722, 612, 611,
402    763, 603, 722, 333, 631, 722, 686, 889, 722, 722, 768, 741, 556, 592, 611, 690, 439, 768, 645,
403    795, 611, 333, 863, 333, 658, 500, 500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549,
404    549, 576, 521, 549, 549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549,
405    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,
406    0, 750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603, 400, 549,
407    411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658, 823, 686, 795, 987,
408    768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713, 768, 713, 790, 790, 890, 823, 549,
409    250, 713, 603, 603, 1042, 987, 603, 987, 603, 494, 329, 790, 790, 786, 713, 384, 384, 384, 384,
410    384, 384, 494, 494, 494, 494, 0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494,
411    494, 494, 0,
412];
413
414static ZAPF_DINGBATS_WIDTHS: [u16; 224] = [
415    278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933, 911, 945, 974,
416    755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537, 577, 692, 786, 788, 788, 790,
417    793, 794, 816, 823, 789, 841, 823, 833, 816, 831, 923, 744, 723, 749, 790, 792, 695, 776, 768,
418    792, 759, 707, 708, 682, 701, 826, 815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785,
419    791, 873, 761, 762, 762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668,
420    0, 390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0, 0, 0, 0, 0, 0,
421    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788,
422    788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
423    788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
424    788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834, 873, 828, 924, 924, 917, 930,
425    931, 463, 883, 836, 836, 867, 867, 696, 696, 874, 0, 874, 760, 946, 771, 865, 771, 888, 967,
426    888, 831, 873, 927, 970, 918, 0,
427];
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_helvetica_known_widths() {
435        let m = StandardFont::Helvetica.metrics();
436        // space = 278, A = 667, m = 833
437        assert_eq!(m.widths[0], 278); // space (code 32)
438        assert_eq!(m.widths[33], 667); // A (code 65)
439        assert_eq!(m.widths[77], 833); // m (code 109)
440    }
441
442    #[test]
443    fn test_helvetica_bold_wider() {
444        let regular = StandardFont::Helvetica.metrics();
445        let bold = StandardFont::HelveticaBold.metrics();
446        // Bold A = 722 > regular A = 667
447        assert_eq!(bold.widths[33], 722);
448        assert!(bold.widths[33] > regular.widths[33]);
449    }
450
451    #[test]
452    fn test_courier_monospaced() {
453        let m = StandardFont::Courier.metrics();
454        // All Courier glyphs are 600
455        for &w in m.widths.iter() {
456            if w > 0 {
457                assert_eq!(w, 600, "Courier should be monospaced");
458            }
459        }
460    }
461
462    #[test]
463    fn test_char_width_calculation() {
464        let m = StandardFont::Helvetica.metrics();
465        // space at 12pt: 278/1000 * 12 = 3.336
466        let w = m.char_width(' ', 12.0);
467        assert!((w - 3.336).abs() < 0.001);
468    }
469
470    #[test]
471    fn test_measure_string() {
472        let m = StandardFont::Helvetica.metrics();
473        let w = m.measure_string("A", 12.0, 0.0);
474        assert!((w - 8.004).abs() < 0.001); // 667/1000 * 12
475    }
476
477    #[test]
478    fn test_char_width_em_dash_uses_winansi() {
479        let m = StandardFont::Helvetica.metrics();
480        // Em dash (U+2014) maps to WinAnsi 0x97 = 151, index 151-32 = 119
481        // Helvetica em dash width = 1000
482        let w = m.char_width('\u{2014}', 10.0);
483        assert!(
484            (w - 10.0).abs() < 0.001,
485            "em dash should be 1000/1000 * 10 = 10.0, got {}",
486            w
487        );
488    }
489
490    #[test]
491    fn test_char_width_en_dash_uses_winansi() {
492        let m = StandardFont::Helvetica.metrics();
493        // En dash (U+2013) maps to WinAnsi 0x96 = 150, index 150-32 = 118
494        // Helvetica en dash width = 556
495        let w = m.char_width('\u{2013}', 10.0);
496        assert!(
497            (w - 5.56).abs() < 0.001,
498            "en dash should be 556/1000 * 10 = 5.56, got {}",
499            w
500        );
501    }
502
503    #[test]
504    fn test_unicode_to_winansi_mappings() {
505        assert_eq!(unicode_to_winansi('\u{2014}'), Some(0x97)); // Em dash
506        assert_eq!(unicode_to_winansi('\u{2013}'), Some(0x96)); // En dash
507        assert_eq!(unicode_to_winansi('\u{201C}'), Some(0x93)); // Left double quote
508        assert_eq!(unicode_to_winansi('\u{2026}'), Some(0x85)); // Ellipsis
509        assert_eq!(unicode_to_winansi('A'), Some(0x41)); // ASCII
510        assert_eq!(unicode_to_winansi('\u{4E00}'), None); // CJK — not in WinAnsi
511    }
512
513    #[test]
514    fn test_latin_extended_character_widths() {
515        let m = StandardFont::Helvetica.metrics();
516        // Accented capitals must have the same width as their base character
517        let a_width = m.widths[33]; // A = 667
518        assert_eq!(m.widths[0xC0 - 32], a_width, "Agrave should match A"); // À
519        assert_eq!(m.widths[0xC1 - 32], a_width, "Aacute should match A"); // Á
520        assert_eq!(m.widths[0xC2 - 32], a_width, "Acircumflex should match A"); // Â
521        assert_eq!(m.widths[0xC3 - 32], a_width, "Atilde should match A"); // Ã
522        assert_eq!(m.widths[0xC4 - 32], a_width, "Adieresis should match A"); // Ä
523        assert_eq!(m.widths[0xC5 - 32], a_width, "Aring should match A"); // Å
524
525        // Verify char_width gives correct results for Latin Extended
526        let w_a_dieresis = m.char_width('Ä', 10.0);
527        let w_a = m.char_width('A', 10.0);
528        assert!(
529            (w_a_dieresis - w_a).abs() < 0.001,
530            "Ä width ({}) should equal A width ({})",
531            w_a_dieresis,
532            w_a
533        );
534
535        let w_a_ring = m.char_width('Å', 10.0);
536        assert!(
537            (w_a_ring - w_a).abs() < 0.001,
538            "Å width ({}) should equal A width ({})",
539            w_a_ring,
540            w_a
541        );
542
543        // O-dieresis should match O
544        let o_width = m.widths[0x4F - 32]; // O = 778
545        assert_eq!(m.widths[0xD6 - 32], o_width, "Odieresis should match O");
546
547        // AE should be 1000
548        assert_eq!(m.widths[0xC6 - 32], 1000, "AE should be 1000");
549
550        // Verify sequential characters don't stack (each x position increases)
551        let test_str = "ÅÄÖÉÈÊÑÜÚÙû";
552        let mut prev_x = 0.0;
553        for (i, ch) in test_str.chars().enumerate() {
554            let w = m.char_width(ch, 12.0);
555            assert!(
556                w > 1.0,
557                "Character '{}' (U+{:04X}) has suspiciously small width: {}",
558                ch,
559                ch as u32,
560                w
561            );
562            if i > 0 {
563                assert!(
564                    prev_x > 0.0,
565                    "Previous character should have non-zero width"
566                );
567            }
568            prev_x += w;
569        }
570        // Total width should be reasonable (not near-zero from stacking)
571        assert!(
572            prev_x > 50.0,
573            "Total width of '{}' at 12pt should be >50pt, got {}",
574            test_str,
575            prev_x
576        );
577    }
578
579    #[test]
580    fn test_latin_extended_widths_all_fonts() {
581        // Verify accented characters have correct widths across all proportional fonts
582        let fonts = [
583            StandardFont::Helvetica,
584            StandardFont::HelveticaBold,
585            StandardFont::HelveticaOblique,
586            StandardFont::HelveticaBoldOblique,
587            StandardFont::TimesRoman,
588            StandardFont::TimesBold,
589            StandardFont::TimesItalic,
590            StandardFont::TimesBoldItalic,
591        ];
592
593        for font in &fonts {
594            let m = font.metrics();
595            // Ä (0xC4) should have same width as A (0x41)
596            let a_width = m.widths[0x41 - 32];
597            let a_dieresis_width = m.widths[0xC4 - 32];
598            assert_eq!(
599                a_dieresis_width, a_width,
600                "{:?}: Ä width ({}) != A width ({})",
601                font, a_dieresis_width, a_width
602            );
603
604            // Å (0xC5) should have same width as A (0x41)
605            let a_ring_width = m.widths[0xC5 - 32];
606            assert_eq!(
607                a_ring_width, a_width,
608                "{:?}: Å width ({}) != A width ({})",
609                font, a_ring_width, a_width
610            );
611
612            // ö (0xF6) should have same width as o (0x6F)
613            let o_width = m.widths[0x6F - 32];
614            let o_dieresis_width = m.widths[0xF6 - 32];
615            assert_eq!(
616                o_dieresis_width, o_width,
617                "{:?}: ö width ({}) != o width ({})",
618                font, o_dieresis_width, o_width
619            );
620        }
621    }
622
623    #[test]
624    fn test_page_placeholder_width_difference() {
625        let m = StandardFont::Helvetica.metrics();
626        let literal = m.measure_string("{{pageNumber}}", 12.0, 0.0);
627        let substituted = m.measure_string("00", 12.0, 0.0);
628        // "{{pageNumber}}" is 16 chars, "00" is 2 chars — massive difference
629        assert!(
630            literal > substituted * 3.0,
631            "Literal placeholder ({}) is much wider than substituted ({})",
632            literal,
633            substituted
634        );
635    }
636}