Skip to main content

pdfboss_encoding/
lib.rs

1//! Shared PDF font-encoding tables (WinAnsi / MacRoman / Standard, from
2//! ISO 32000 Appendix D) and a bundled glyph-name-to-Unicode subset, consumed
3//! by the pdfboss text-extraction and rendering crates.
4
5mod afm;
6pub use afm::{is_standard_14, standard_14_width};
7
8/// WinAnsiEncoding codes `0x80..=0x9F` (the region that differs from
9/// Latin-1); `None` marks unassigned codes.
10const WIN_ANSI_80_9F: [Option<char>; 32] = [
11    Some('\u{20AC}'),
12    None,
13    Some('\u{201A}'),
14    Some('\u{0192}'),
15    Some('\u{201E}'),
16    Some('\u{2026}'),
17    Some('\u{2020}'),
18    Some('\u{2021}'),
19    Some('\u{02C6}'),
20    Some('\u{2030}'),
21    Some('\u{0160}'),
22    Some('\u{2039}'),
23    Some('\u{0152}'),
24    None,
25    Some('\u{017D}'),
26    None,
27    None,
28    Some('\u{2018}'),
29    Some('\u{2019}'),
30    Some('\u{201C}'),
31    Some('\u{201D}'),
32    Some('\u{2022}'),
33    Some('\u{2013}'),
34    Some('\u{2014}'),
35    Some('\u{02DC}'),
36    Some('\u{2122}'),
37    Some('\u{0161}'),
38    Some('\u{203A}'),
39    Some('\u{0153}'),
40    None,
41    Some('\u{017E}'),
42    Some('\u{0178}'),
43];
44
45/// Unicode value of `code` in `WinAnsiEncoding`.
46pub fn win_ansi(code: u8) -> Option<char> {
47    match code {
48        0x20..=0x7E => Some(code as char),
49        0x80..=0x9F => WIN_ANSI_80_9F[(code - 0x80) as usize],
50        0xA0..=0xFF => Some(code as char),
51        _ => None,
52    }
53}
54
55/// WinAnsiEncoding glyph name for `code` (ISO 32000-1 Annex D.2
56/// "WinAnsiEncoding" column). `None` for exactly the codes [`win_ansi`]
57/// leaves unassigned. Two ASCII codes diverge from StandardEncoding's
58/// names: `0x27` is `quotesingle` and `0x60` is `grave` (the straight
59/// marks, matching `win_ansi`'s identity mapping there). Two codes render
60/// an existing glyph rather than owning one: `0xA0` carries `space` (the
61/// nonbreaking space draws as the space glyph) and `0xAD` carries `hyphen`
62/// (likewise the soft hyphen) — see the self-verifying
63/// `win_ansi_glyph_name_matches_win_ansi_table` test below.
64pub fn win_ansi_glyph_name(code: u8) -> Option<&'static str> {
65    match code {
66        0x27 => Some("quotesingle"),
67        0x60 => Some("grave"),
68        0x20..=0x7E => Some(STANDARD_ASCII_NAMES[(code - 0x20) as usize]),
69        0x80..=0x9F => WIN_ANSI_80_9F_NAMES[(code - 0x80) as usize],
70        0xA0..=0xFF => Some(WIN_ANSI_A0_FF_NAMES[(code - 0xA0) as usize]),
71        _ => None,
72    }
73}
74
75/// WinAnsiEncoding glyph names for codes `0x80..=0x9F`, parallel to
76/// [`WIN_ANSI_80_9F`]; `None` marks the same unassigned codes.
77const WIN_ANSI_80_9F_NAMES: [Option<&str>; 32] = [
78    Some("Euro"),
79    None,
80    Some("quotesinglbase"),
81    Some("florin"),
82    Some("quotedblbase"),
83    Some("ellipsis"),
84    Some("dagger"),
85    Some("daggerdbl"),
86    Some("circumflex"),
87    Some("perthousand"),
88    Some("Scaron"),
89    Some("guilsinglleft"),
90    Some("OE"),
91    None,
92    Some("Zcaron"),
93    None,
94    None,
95    Some("quoteleft"),
96    Some("quoteright"),
97    Some("quotedblleft"),
98    Some("quotedblright"),
99    Some("bullet"),
100    Some("endash"),
101    Some("emdash"),
102    Some("tilde"),
103    Some("trademark"),
104    Some("scaron"),
105    Some("guilsinglright"),
106    Some("oe"),
107    None,
108    Some("zcaron"),
109    Some("Ydieresis"),
110];
111
112/// WinAnsiEncoding glyph names for codes `0xA0..=0xFF` (ISO 32000-1
113/// Annex D.2 "WinAnsiEncoding" column), in code order (index `0` is code
114/// `0xA0`).
115const WIN_ANSI_A0_FF_NAMES: [&str; 96] = [
116    "space",
117    "exclamdown",
118    "cent",
119    "sterling",
120    "currency",
121    "yen",
122    "brokenbar",
123    "section",
124    "dieresis",
125    "copyright",
126    "ordfeminine",
127    "guillemotleft",
128    "logicalnot",
129    "hyphen",
130    "registered",
131    "macron",
132    "degree",
133    "plusminus",
134    "twosuperior",
135    "threesuperior",
136    "acute",
137    "mu",
138    "paragraph",
139    "periodcentered",
140    "cedilla",
141    "onesuperior",
142    "ordmasculine",
143    "guillemotright",
144    "onequarter",
145    "onehalf",
146    "threequarters",
147    "questiondown",
148    "Agrave",
149    "Aacute",
150    "Acircumflex",
151    "Atilde",
152    "Adieresis",
153    "Aring",
154    "AE",
155    "Ccedilla",
156    "Egrave",
157    "Eacute",
158    "Ecircumflex",
159    "Edieresis",
160    "Igrave",
161    "Iacute",
162    "Icircumflex",
163    "Idieresis",
164    "Eth",
165    "Ntilde",
166    "Ograve",
167    "Oacute",
168    "Ocircumflex",
169    "Otilde",
170    "Odieresis",
171    "multiply",
172    "Oslash",
173    "Ugrave",
174    "Uacute",
175    "Ucircumflex",
176    "Udieresis",
177    "Yacute",
178    "Thorn",
179    "germandbls",
180    "agrave",
181    "aacute",
182    "acircumflex",
183    "atilde",
184    "adieresis",
185    "aring",
186    "ae",
187    "ccedilla",
188    "egrave",
189    "eacute",
190    "ecircumflex",
191    "edieresis",
192    "igrave",
193    "iacute",
194    "icircumflex",
195    "idieresis",
196    "eth",
197    "ntilde",
198    "ograve",
199    "oacute",
200    "ocircumflex",
201    "otilde",
202    "odieresis",
203    "divide",
204    "oslash",
205    "ugrave",
206    "uacute",
207    "ucircumflex",
208    "udieresis",
209    "yacute",
210    "thorn",
211    "ydieresis",
212];
213
214/// MacRomanEncoding codes `0x80..=0xFF` (codes below coincide with ASCII).
215const MAC_ROMAN_HIGH: [char; 128] = [
216    '\u{C4}', '\u{C5}', '\u{C7}', '\u{C9}', '\u{D1}', '\u{D6}', '\u{DC}', '\u{E1}', '\u{E0}',
217    '\u{E2}', '\u{E4}', '\u{E3}', '\u{E5}', '\u{E7}', '\u{E9}', '\u{E8}', '\u{EA}', '\u{EB}',
218    '\u{ED}', '\u{EC}', '\u{EE}', '\u{EF}', '\u{F1}', '\u{F3}', '\u{F2}', '\u{F4}', '\u{F6}',
219    '\u{F5}', '\u{FA}', '\u{F9}', '\u{FB}', '\u{FC}', '\u{2020}', '\u{B0}', '\u{A2}', '\u{A3}',
220    '\u{A7}', '\u{2022}', '\u{B6}', '\u{DF}', '\u{AE}', '\u{A9}', '\u{2122}', '\u{B4}', '\u{A8}',
221    '\u{2260}', '\u{C6}', '\u{D8}', '\u{221E}', '\u{B1}', '\u{2264}', '\u{2265}', '\u{A5}',
222    '\u{B5}', '\u{2202}', '\u{2211}', '\u{220F}', '\u{3C0}', '\u{222B}', '\u{AA}', '\u{BA}',
223    '\u{3A9}', '\u{E6}', '\u{F8}', '\u{BF}', '\u{A1}', '\u{AC}', '\u{221A}', '\u{192}', '\u{2248}',
224    '\u{2206}', '\u{AB}', '\u{BB}', '\u{2026}', '\u{A0}', '\u{C0}', '\u{C3}', '\u{D5}', '\u{152}',
225    '\u{153}', '\u{2013}', '\u{2014}', '\u{201C}', '\u{201D}', '\u{2018}', '\u{2019}', '\u{F7}',
226    '\u{25CA}', '\u{FF}', '\u{178}', '\u{2044}', '\u{20AC}', '\u{2039}', '\u{203A}', '\u{FB01}',
227    '\u{FB02}', '\u{2021}', '\u{B7}', '\u{201A}', '\u{201E}', '\u{2030}', '\u{C2}', '\u{CA}',
228    '\u{C1}', '\u{CB}', '\u{C8}', '\u{CD}', '\u{CE}', '\u{CF}', '\u{CC}', '\u{D3}', '\u{D4}',
229    '\u{F8FF}', '\u{D2}', '\u{DA}', '\u{DB}', '\u{D9}', '\u{131}', '\u{2C6}', '\u{2DC}', '\u{AF}',
230    '\u{2D8}', '\u{2D9}', '\u{2DA}', '\u{B8}', '\u{2DD}', '\u{2DB}', '\u{2C7}',
231];
232
233/// Unicode value of `code` in `MacRomanEncoding`.
234pub fn mac_roman(code: u8) -> Option<char> {
235    match code {
236        0x20..=0x7E => Some(code as char),
237        0x80..=0xFF => Some(MAC_ROMAN_HIGH[(code - 0x80) as usize]),
238        _ => None,
239    }
240}
241
242/// StandardEncoding codes above 0x7E that are assigned (sparse).
243const STANDARD_HIGH: &[(u8, char)] = &[
244    (0xA1, '\u{A1}'),
245    (0xA2, '\u{A2}'),
246    (0xA3, '\u{A3}'),
247    (0xA4, '\u{2044}'),
248    (0xA5, '\u{A5}'),
249    (0xA6, '\u{192}'),
250    (0xA7, '\u{A7}'),
251    (0xA8, '\u{A4}'),
252    (0xA9, '\u{27}'),
253    (0xAA, '\u{201C}'),
254    (0xAB, '\u{AB}'),
255    (0xAC, '\u{2039}'),
256    (0xAD, '\u{203A}'),
257    (0xAE, '\u{FB01}'),
258    (0xAF, '\u{FB02}'),
259    (0xB1, '\u{2013}'),
260    (0xB2, '\u{2020}'),
261    (0xB3, '\u{2021}'),
262    (0xB4, '\u{B7}'),
263    (0xB6, '\u{B6}'),
264    (0xB7, '\u{2022}'),
265    (0xB8, '\u{201A}'),
266    (0xB9, '\u{201E}'),
267    (0xBA, '\u{201D}'),
268    (0xBB, '\u{BB}'),
269    (0xBC, '\u{2026}'),
270    (0xBD, '\u{2030}'),
271    (0xBF, '\u{BF}'),
272    (0xC1, '\u{60}'),
273    (0xC2, '\u{B4}'),
274    (0xC3, '\u{2C6}'),
275    (0xC4, '\u{2DC}'),
276    (0xC5, '\u{AF}'),
277    (0xC6, '\u{2D8}'),
278    (0xC7, '\u{2D9}'),
279    (0xC8, '\u{A8}'),
280    (0xCA, '\u{2DA}'),
281    (0xCB, '\u{B8}'),
282    (0xCD, '\u{2DD}'),
283    (0xCE, '\u{2DB}'),
284    (0xCF, '\u{2C7}'),
285    (0xD0, '\u{2014}'),
286    (0xE1, '\u{C6}'),
287    (0xE3, '\u{AA}'),
288    (0xE8, '\u{141}'),
289    (0xE9, '\u{D8}'),
290    (0xEA, '\u{152}'),
291    (0xEB, '\u{BA}'),
292    (0xF1, '\u{E6}'),
293    (0xF5, '\u{131}'),
294    (0xF8, '\u{142}'),
295    (0xF9, '\u{F8}'),
296    (0xFA, '\u{153}'),
297    (0xFB, '\u{DF}'),
298];
299
300/// Unicode value of `code` in `StandardEncoding`.
301pub fn standard(code: u8) -> Option<char> {
302    match code {
303        0x27 => Some('\u{2019}'),
304        0x60 => Some('\u{2018}'),
305        0x20..=0x7E => Some(code as char),
306        0xA1..=0xFF => STANDARD_HIGH
307            .iter()
308            .find(|&&(c, _)| c == code)
309            .map(|&(_, u)| u),
310        _ => None,
311    }
312}
313
314/// StandardEncoding names for codes `0x20..=0x7E` (space..asciitilde), in
315/// code order (index `0` is code `0x20`). Two codes diverge from their plain
316/// ASCII name: `0x27` is `quoteright` (a curly right quote, not the straight
317/// `quotesingle` apostrophe) and `0x60` is `quoteleft` (a curly left quote,
318/// not `grave`) -- matching `standard`'s `0x27`/`0x60` special cases above.
319const STANDARD_ASCII_NAMES: [&str; 95] = [
320    "space",
321    "exclam",
322    "quotedbl",
323    "numbersign",
324    "dollar",
325    "percent",
326    "ampersand",
327    "quoteright",
328    "parenleft",
329    "parenright",
330    "asterisk",
331    "plus",
332    "comma",
333    "hyphen",
334    "period",
335    "slash",
336    "zero",
337    "one",
338    "two",
339    "three",
340    "four",
341    "five",
342    "six",
343    "seven",
344    "eight",
345    "nine",
346    "colon",
347    "semicolon",
348    "less",
349    "equal",
350    "greater",
351    "question",
352    "at",
353    "A",
354    "B",
355    "C",
356    "D",
357    "E",
358    "F",
359    "G",
360    "H",
361    "I",
362    "J",
363    "K",
364    "L",
365    "M",
366    "N",
367    "O",
368    "P",
369    "Q",
370    "R",
371    "S",
372    "T",
373    "U",
374    "V",
375    "W",
376    "X",
377    "Y",
378    "Z",
379    "bracketleft",
380    "backslash",
381    "bracketright",
382    "asciicircum",
383    "underscore",
384    "quoteleft",
385    "a",
386    "b",
387    "c",
388    "d",
389    "e",
390    "f",
391    "g",
392    "h",
393    "i",
394    "j",
395    "k",
396    "l",
397    "m",
398    "n",
399    "o",
400    "p",
401    "q",
402    "r",
403    "s",
404    "t",
405    "u",
406    "v",
407    "w",
408    "x",
409    "y",
410    "z",
411    "braceleft",
412    "bar",
413    "braceright",
414    "asciitilde",
415];
416
417/// StandardEncoding names for codes above `0x7E` (ISO 32000-1 Annex D.2
418/// "StandardEncoding" column), parallel to [`STANDARD_HIGH`]'s codes, in the
419/// same order.
420const STANDARD_HIGH_NAMES: &[(u8, &str)] = &[
421    (0xA1, "exclamdown"),
422    (0xA2, "cent"),
423    (0xA3, "sterling"),
424    (0xA4, "fraction"),
425    (0xA5, "yen"),
426    (0xA6, "florin"),
427    (0xA7, "section"),
428    (0xA8, "currency"),
429    (0xA9, "quotesingle"),
430    (0xAA, "quotedblleft"),
431    (0xAB, "guillemotleft"),
432    (0xAC, "guilsinglleft"),
433    (0xAD, "guilsinglright"),
434    (0xAE, "fi"),
435    (0xAF, "fl"),
436    (0xB1, "endash"),
437    (0xB2, "dagger"),
438    (0xB3, "daggerdbl"),
439    (0xB4, "periodcentered"),
440    (0xB6, "paragraph"),
441    (0xB7, "bullet"),
442    (0xB8, "quotesinglbase"),
443    (0xB9, "quotedblbase"),
444    (0xBA, "quotedblright"),
445    (0xBB, "guillemotright"),
446    (0xBC, "ellipsis"),
447    (0xBD, "perthousand"),
448    (0xBF, "questiondown"),
449    (0xC1, "grave"),
450    (0xC2, "acute"),
451    (0xC3, "circumflex"),
452    (0xC4, "tilde"),
453    (0xC5, "macron"),
454    (0xC6, "breve"),
455    (0xC7, "dotaccent"),
456    (0xC8, "dieresis"),
457    (0xCA, "ring"),
458    (0xCB, "cedilla"),
459    (0xCD, "hungarumlaut"),
460    (0xCE, "ogonek"),
461    (0xCF, "caron"),
462    (0xD0, "emdash"),
463    (0xE1, "AE"),
464    (0xE3, "ordfeminine"),
465    (0xE8, "Lslash"),
466    (0xE9, "Oslash"),
467    (0xEA, "OE"),
468    (0xEB, "ordmasculine"),
469    (0xF1, "ae"),
470    (0xF5, "dotlessi"),
471    (0xF8, "lslash"),
472    (0xF9, "oslash"),
473    (0xFA, "oe"),
474    (0xFB, "germandbls"),
475];
476
477/// Adobe StandardEncoding glyph name for `code` (ISO 32000-1 Annex D.2
478/// "StandardEncoding" column; equivalently Adobe Type 1 Font Format
479/// Appendix C). `None` for exactly the codes `standard` leaves unassigned
480/// (see the self-verifying `standard_encoding_name_matches_standard_table`
481/// test below, which ties this table to that one so an authoring mistake
482/// here fails a test rather than silently mis-encoding a glyph).
483pub fn standard_encoding_name(code: u8) -> Option<&'static str> {
484    match code {
485        0x20..=0x7E => Some(STANDARD_ASCII_NAMES[(code - 0x20) as usize]),
486        0xA1..=0xFF => STANDARD_HIGH_NAMES
487            .iter()
488            .find(|&&(c, _)| c == code)
489            .map(|&(_, n)| n),
490        _ => None,
491    }
492}
493
494/// Resolves a glyph name (as used in `/Differences`) to a Unicode scalar:
495/// `uniXXXX` and `uXXXX`–`uXXXXXX` hex forms, single ASCII letters, and a
496/// bundled subset of the standard glyph list.
497pub fn glyph_to_unicode(name: &str) -> Option<char> {
498    if let Some(hex) = name.strip_prefix("uni") {
499        if hex.len() == 4 && hex.bytes().all(|b| b.is_ascii_hexdigit()) {
500            return char::from_u32(u32::from_str_radix(hex, 16).ok()?);
501        }
502    }
503    if let Some(hex) = name.strip_prefix('u') {
504        if (4..=6).contains(&hex.len()) && hex.bytes().all(|b| b.is_ascii_hexdigit()) {
505            return char::from_u32(u32::from_str_radix(hex, 16).ok()?);
506        }
507    }
508    let mut chars = name.chars();
509    if let (Some(c), None) = (chars.next(), chars.next()) {
510        if c.is_ascii_alphabetic() {
511            return Some(c);
512        }
513    }
514    GLYPH_TABLES
515        .iter()
516        .flat_map(|t| t.iter())
517        .find(|&&(n, _)| n == name)
518        .map(|&(_, u)| u)
519}
520
521/// Resolves a glyph name to the text it represents, per the Adobe Glyph
522/// List algorithm: everything from the first period on is dropped
523/// (`eight.oldstyle` → `8`), underscore-joined components each resolve and
524/// concatenate (`f_i` → `fi`, `T_h` → `Th`), and a `uni` prefix may carry
525/// several 4-digit hex groups. `None` unless every component resolves —
526/// a partially-resolved ligature would silently drop letters, where the
527/// caller's U+FFFD at least stays visible.
528pub fn glyph_to_text(name: &str) -> Option<String> {
529    let base = name.split('.').next().unwrap_or_default();
530    if base.is_empty() {
531        return None;
532    }
533    let mut out = String::new();
534    for component in base.split('_') {
535        push_component(component, &mut out)?;
536    }
537    Some(out)
538}
539
540/// Appends one underscore-separated component of a glyph name; `None` when
541/// the component resolves to nothing.
542fn push_component(component: &str, out: &mut String) -> Option<()> {
543    let hex = component.strip_prefix("uni").unwrap_or_default();
544    if hex.len() >= 8 && hex.len().is_multiple_of(4) && hex.bytes().all(|b| b.is_ascii_hexdigit()) {
545        // Multi-group form: `uni20AC0308` is two scalars. The single-group
546        // form stays on the `glyph_to_unicode` path below.
547        for group in hex.as_bytes().chunks(4) {
548            let group = std::str::from_utf8(group).ok()?;
549            let scalar = u32::from_str_radix(group, 16).ok()?;
550            out.push(char::from_u32(scalar)?);
551        }
552        return Some(());
553    }
554    out.push(glyph_to_unicode(component)?);
555    Some(())
556}
557
558/// All bundled glyph-name tables, searched in order.
559const GLYPH_TABLES: [&[(&str, char)]; 5] = [
560    GLYPHS_ASCII,
561    GLYPHS_LATIN1,
562    GLYPHS_PUNCT,
563    GLYPHS_GREEK,
564    GLYPHS_MISC,
565];
566
567/// Names for the ASCII range (letters are handled separately).
568const GLYPHS_ASCII: &[(&str, char)] = &[
569    ("space", ' '),
570    ("exclam", '!'),
571    ("quotedbl", '"'),
572    ("numbersign", '#'),
573    ("dollar", '$'),
574    ("percent", '%'),
575    ("ampersand", '&'),
576    ("quotesingle", '\''),
577    ("parenleft", '('),
578    ("parenright", ')'),
579    ("asterisk", '*'),
580    ("plus", '+'),
581    ("comma", ','),
582    ("hyphen", '-'),
583    ("period", '.'),
584    ("slash", '/'),
585    ("zero", '0'),
586    ("one", '1'),
587    ("two", '2'),
588    ("three", '3'),
589    ("four", '4'),
590    ("five", '5'),
591    ("six", '6'),
592    ("seven", '7'),
593    ("eight", '8'),
594    ("nine", '9'),
595    ("colon", ':'),
596    ("semicolon", ';'),
597    ("less", '<'),
598    ("equal", '='),
599    ("greater", '>'),
600    ("question", '?'),
601    ("at", '@'),
602    ("bracketleft", '['),
603    ("backslash", '\\'),
604    ("bracketright", ']'),
605    ("asciicircum", '^'),
606    ("underscore", '_'),
607    ("grave", '`'),
608    ("braceleft", '{'),
609    ("bar", '|'),
610    ("braceright", '}'),
611    ("asciitilde", '~'),
612];
613
614/// Names for the Latin-1 supplement.
615const GLYPHS_LATIN1: &[(&str, char)] = &[
616    ("exclamdown", '\u{A1}'),
617    ("cent", '\u{A2}'),
618    ("sterling", '\u{A3}'),
619    ("currency", '\u{A4}'),
620    ("yen", '\u{A5}'),
621    ("brokenbar", '\u{A6}'),
622    ("section", '\u{A7}'),
623    ("dieresis", '\u{A8}'),
624    ("copyright", '\u{A9}'),
625    ("ordfeminine", '\u{AA}'),
626    ("guillemotleft", '\u{AB}'),
627    ("logicalnot", '\u{AC}'),
628    ("registered", '\u{AE}'),
629    ("macron", '\u{AF}'),
630    ("degree", '\u{B0}'),
631    ("plusminus", '\u{B1}'),
632    ("twosuperior", '\u{B2}'),
633    ("threesuperior", '\u{B3}'),
634    ("acute", '\u{B4}'),
635    ("mu", '\u{B5}'),
636    ("paragraph", '\u{B6}'),
637    ("periodcentered", '\u{B7}'),
638    ("cedilla", '\u{B8}'),
639    ("onesuperior", '\u{B9}'),
640    ("ordmasculine", '\u{BA}'),
641    ("guillemotright", '\u{BB}'),
642    ("onequarter", '\u{BC}'),
643    ("onehalf", '\u{BD}'),
644    ("threequarters", '\u{BE}'),
645    ("questiondown", '\u{BF}'),
646    ("Agrave", '\u{C0}'),
647    ("Aacute", '\u{C1}'),
648    ("Acircumflex", '\u{C2}'),
649    ("Atilde", '\u{C3}'),
650    ("Adieresis", '\u{C4}'),
651    ("Aring", '\u{C5}'),
652    ("AE", '\u{C6}'),
653    ("Ccedilla", '\u{C7}'),
654    ("Egrave", '\u{C8}'),
655    ("Eacute", '\u{C9}'),
656    ("Ecircumflex", '\u{CA}'),
657    ("Edieresis", '\u{CB}'),
658    ("Igrave", '\u{CC}'),
659    ("Iacute", '\u{CD}'),
660    ("Icircumflex", '\u{CE}'),
661    ("Idieresis", '\u{CF}'),
662    ("Eth", '\u{D0}'),
663    ("Ntilde", '\u{D1}'),
664    ("Ograve", '\u{D2}'),
665    ("Oacute", '\u{D3}'),
666    ("Ocircumflex", '\u{D4}'),
667    ("Otilde", '\u{D5}'),
668    ("Odieresis", '\u{D6}'),
669    ("multiply", '\u{D7}'),
670    ("Oslash", '\u{D8}'),
671    ("Ugrave", '\u{D9}'),
672    ("Uacute", '\u{DA}'),
673    ("Ucircumflex", '\u{DB}'),
674    ("Udieresis", '\u{DC}'),
675    ("Yacute", '\u{DD}'),
676    ("Thorn", '\u{DE}'),
677    ("germandbls", '\u{DF}'),
678    ("agrave", '\u{E0}'),
679    ("aacute", '\u{E1}'),
680    ("acircumflex", '\u{E2}'),
681    ("atilde", '\u{E3}'),
682    ("adieresis", '\u{E4}'),
683    ("aring", '\u{E5}'),
684    ("ae", '\u{E6}'),
685    ("ccedilla", '\u{E7}'),
686    ("egrave", '\u{E8}'),
687    ("eacute", '\u{E9}'),
688    ("ecircumflex", '\u{EA}'),
689    ("edieresis", '\u{EB}'),
690    ("igrave", '\u{EC}'),
691    ("iacute", '\u{ED}'),
692    ("icircumflex", '\u{EE}'),
693    ("idieresis", '\u{EF}'),
694    ("eth", '\u{F0}'),
695    ("ntilde", '\u{F1}'),
696    ("ograve", '\u{F2}'),
697    ("oacute", '\u{F3}'),
698    ("ocircumflex", '\u{F4}'),
699    ("otilde", '\u{F5}'),
700    ("odieresis", '\u{F6}'),
701    ("divide", '\u{F7}'),
702    ("oslash", '\u{F8}'),
703    ("ugrave", '\u{F9}'),
704    ("uacute", '\u{FA}'),
705    ("ucircumflex", '\u{FB}'),
706    ("udieresis", '\u{FC}'),
707    ("yacute", '\u{FD}'),
708    ("thorn", '\u{FE}'),
709    ("ydieresis", '\u{FF}'),
710];
711
712/// Typographic punctuation, ligatures, and accents.
713const GLYPHS_PUNCT: &[(&str, char)] = &[
714    ("quoteleft", '\u{2018}'),
715    ("quoteright", '\u{2019}'),
716    ("quotesinglbase", '\u{201A}'),
717    ("quotedblleft", '\u{201C}'),
718    ("quotedblright", '\u{201D}'),
719    ("quotedblbase", '\u{201E}'),
720    ("endash", '\u{2013}'),
721    ("emdash", '\u{2014}'),
722    ("bullet", '\u{2022}'),
723    ("ellipsis", '\u{2026}'),
724    ("dagger", '\u{2020}'),
725    ("daggerdbl", '\u{2021}'),
726    ("perthousand", '\u{2030}'),
727    ("guilsinglleft", '\u{2039}'),
728    ("guilsinglright", '\u{203A}'),
729    ("fraction", '\u{2044}'),
730    ("minus", '\u{2212}'),
731    ("florin", '\u{192}'),
732    ("Euro", '\u{20AC}'),
733    ("trademark", '\u{2122}'),
734    ("fi", '\u{FB01}'),
735    ("fl", '\u{FB02}'),
736    ("ff", '\u{FB00}'),
737    ("ffi", '\u{FB03}'),
738    ("ffl", '\u{FB04}'),
739    ("circumflex", '\u{2C6}'),
740    ("caron", '\u{2C7}'),
741    ("breve", '\u{2D8}'),
742    ("dotaccent", '\u{2D9}'),
743    ("ring", '\u{2DA}'),
744    ("ogonek", '\u{2DB}'),
745    ("tilde", '\u{2DC}'),
746    ("hungarumlaut", '\u{2DD}'),
747    ("OE", '\u{152}'),
748    ("oe", '\u{153}'),
749    ("Scaron", '\u{160}'),
750    ("scaron", '\u{161}'),
751    ("Zcaron", '\u{17D}'),
752    ("zcaron", '\u{17E}'),
753    ("Ydieresis", '\u{178}'),
754    ("Lslash", '\u{141}'),
755    ("lslash", '\u{142}'),
756    ("dotlessi", '\u{131}'),
757    ("nbspace", '\u{A0}'),
758    ("sfthyphen", '\u{AD}'),
759];
760
761/// Greek letters (per the glyph list, `Delta`/`Omega`/`mu` map to their
762/// technical-symbol codepoints; `mu` lives in the Latin-1 table).
763const GLYPHS_GREEK: &[(&str, char)] = &[
764    ("Alpha", '\u{391}'),
765    ("Beta", '\u{392}'),
766    ("Gamma", '\u{393}'),
767    ("Delta", '\u{2206}'),
768    ("Epsilon", '\u{395}'),
769    ("Zeta", '\u{396}'),
770    ("Eta", '\u{397}'),
771    ("Theta", '\u{398}'),
772    ("Iota", '\u{399}'),
773    ("Kappa", '\u{39A}'),
774    ("Lambda", '\u{39B}'),
775    ("Mu", '\u{39C}'),
776    ("Nu", '\u{39D}'),
777    ("Xi", '\u{39E}'),
778    ("Omicron", '\u{39F}'),
779    ("Pi", '\u{3A0}'),
780    ("Rho", '\u{3A1}'),
781    ("Sigma", '\u{3A3}'),
782    ("Tau", '\u{3A4}'),
783    ("Upsilon", '\u{3A5}'),
784    ("Phi", '\u{3A6}'),
785    ("Chi", '\u{3A7}'),
786    ("Psi", '\u{3A8}'),
787    ("Omega", '\u{2126}'),
788    ("alpha", '\u{3B1}'),
789    ("beta", '\u{3B2}'),
790    ("gamma", '\u{3B3}'),
791    ("delta", '\u{3B4}'),
792    ("epsilon", '\u{3B5}'),
793    ("zeta", '\u{3B6}'),
794    ("eta", '\u{3B7}'),
795    ("theta", '\u{3B8}'),
796    ("iota", '\u{3B9}'),
797    ("kappa", '\u{3BA}'),
798    ("lambda", '\u{3BB}'),
799    ("nu", '\u{3BD}'),
800    ("xi", '\u{3BE}'),
801    ("omicron", '\u{3BF}'),
802    ("pi", '\u{3C0}'),
803    ("rho", '\u{3C1}'),
804    ("sigma", '\u{3C3}'),
805    ("sigma1", '\u{3C2}'),
806    ("tau", '\u{3C4}'),
807    ("upsilon", '\u{3C5}'),
808    ("phi", '\u{3C6}'),
809    ("chi", '\u{3C7}'),
810    ("psi", '\u{3C8}'),
811    ("omega", '\u{3C9}'),
812];
813
814/// Mathematical and miscellaneous symbols.
815const GLYPHS_MISC: &[(&str, char)] = &[
816    ("infinity", '\u{221E}'),
817    ("notequal", '\u{2260}'),
818    ("lessequal", '\u{2264}'),
819    ("greaterequal", '\u{2265}'),
820    ("partialdiff", '\u{2202}'),
821    ("summation", '\u{2211}'),
822    ("product", '\u{220F}'),
823    ("integral", '\u{222B}'),
824    ("radical", '\u{221A}'),
825    ("approxequal", '\u{2248}'),
826    ("equivalence", '\u{2261}'),
827    ("element", '\u{2208}'),
828    ("intersection", '\u{2229}'),
829    ("union", '\u{222A}'),
830    ("arrowleft", '\u{2190}'),
831    ("arrowup", '\u{2191}'),
832    ("arrowright", '\u{2192}'),
833    ("arrowdown", '\u{2193}'),
834    ("arrowboth", '\u{2194}'),
835    ("lozenge", '\u{25CA}'),
836    ("apple", '\u{F8FF}'),
837];
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842
843    #[test]
844    fn win_ansi_spot_checks() {
845        assert_eq!(win_ansi(b'A'), Some('A'));
846        assert_eq!(win_ansi(0x93), Some('\u{201C}')); // left double quote
847        assert_eq!(win_ansi(0x80), Some('\u{20AC}')); // euro sign
848        assert_eq!(win_ansi(0xE9), Some('\u{E9}')); // e acute (Latin-1)
849        assert_eq!(win_ansi(0x81), None); // unassigned
850        assert_eq!(win_ansi(0x0A), None); // control
851    }
852
853    #[test]
854    fn win_ansi_glyph_name_spot_checks() {
855        assert_eq!(win_ansi_glyph_name(0x41), Some("A"));
856        assert_eq!(win_ansi_glyph_name(0x20), Some("space"));
857        assert_eq!(win_ansi_glyph_name(0x27), Some("quotesingle")); // not quoteright
858        assert_eq!(win_ansi_glyph_name(0x60), Some("grave")); // not quoteleft
859        assert_eq!(win_ansi_glyph_name(0x80), Some("Euro"));
860        assert_eq!(win_ansi_glyph_name(0x93), Some("quotedblleft"));
861        assert_eq!(win_ansi_glyph_name(0xE9), Some("eacute"));
862        assert_eq!(win_ansi_glyph_name(0xFF), Some("ydieresis"));
863        assert_eq!(win_ansi_glyph_name(0x81), None); // unassigned
864        assert_eq!(win_ansi_glyph_name(0x0A), None); // control
865    }
866
867    /// Self-verifying anchor for `win_ansi_glyph_name`: ties the name table
868    /// to the pre-existing, trusted `win_ansi` (code -> Unicode) and
869    /// `glyph_to_unicode` (name -> Unicode) tables. Domain equality must
870    /// hold for every code, and every name must resolve to the code's
871    /// Unicode value — with exactly two documented exceptions, codes that
872    /// render an existing glyph rather than owning one: `0xA0` (nonbreaking
873    /// space, drawn by `space`) and `0xAD` (soft hyphen, drawn by `hyphen`).
874    #[test]
875    fn win_ansi_glyph_name_matches_win_ansi_table() {
876        assert_eq!(win_ansi_glyph_name(0xA0), Some("space"));
877        assert_eq!(win_ansi_glyph_name(0xAD), Some("hyphen"));
878        for code in 0u16..=255 {
879            let code = code as u8;
880            assert_eq!(
881                win_ansi_glyph_name(code).is_some(),
882                win_ansi(code).is_some(),
883                "code {code:#04x}: win_ansi_glyph_name/win_ansi domain mismatch"
884            );
885            let Some(name) = win_ansi_glyph_name(code) else {
886                continue;
887            };
888            let expected = match code {
889                0xA0 => ' ',
890                0xAD => '-',
891                _ => win_ansi(code).unwrap(),
892            };
893            assert_eq!(
894                glyph_to_unicode(name),
895                Some(expected),
896                "code {code:#04x} name {name:?}: glyph_to_unicode disagrees with win_ansi"
897            );
898        }
899    }
900
901    #[test]
902    fn mac_roman_spot_checks() {
903        assert_eq!(mac_roman(b'A'), Some('A'));
904        assert_eq!(mac_roman(0xD0), Some('\u{2013}')); // en dash
905        assert_eq!(mac_roman(0x80), Some('\u{C4}')); // A dieresis
906        assert_eq!(mac_roman(0xA5), Some('\u{2022}')); // bullet
907        assert_eq!(mac_roman(0xFF), Some('\u{2C7}')); // caron
908        assert_eq!(mac_roman(0x00), None);
909    }
910
911    #[test]
912    fn standard_spot_checks() {
913        assert_eq!(standard(b'A'), Some('A'));
914        assert_eq!(standard(0xA9), Some('\u{27}')); // straight apostrophe
915        assert_eq!(standard(0x27), Some('\u{2019}')); // curly right quote
916        assert_eq!(standard(0x60), Some('\u{2018}')); // curly left quote
917        assert_eq!(standard(0xD0), Some('\u{2014}')); // em dash
918        assert_eq!(standard(0x7F), None);
919        assert_eq!(standard(0xA0), None); // unassigned in Standard
920    }
921
922    #[test]
923    fn glyph_names_hex_forms() {
924        assert_eq!(glyph_to_unicode("uni03B1"), Some('\u{3B1}'));
925        assert_eq!(glyph_to_unicode("uni20AC"), Some('\u{20AC}'));
926        assert_eq!(glyph_to_unicode("u1F600"), Some('\u{1F600}'));
927        assert_eq!(glyph_to_unicode("u00E9"), Some('\u{E9}'));
928        assert_eq!(glyph_to_unicode("uniD800"), None); // surrogate
929        assert_eq!(glyph_to_unicode("uniXYZW"), None);
930    }
931
932    /// Self-verifying anchor for `standard_encoding_name`: ties the new table
933    /// to the pre-existing, trusted `standard` (code -> Unicode) and
934    /// `glyph_to_unicode` (name -> Unicode) tables so an authoring typo in
935    /// the new table fails a test instead of silently mis-encoding a glyph.
936    /// Domain equality (StandardEncoding assigns a name to exactly the codes
937    /// `standard` maps to a char) must hold for every code; value agreement
938    /// only where `glyph_to_unicode` also resolves the name (some names
939    /// aren't in the bundled glyph-name subset).
940    #[test]
941    fn standard_encoding_name_matches_standard_table() {
942        for code in 0u16..=255 {
943            let code = code as u8;
944            assert_eq!(
945                standard_encoding_name(code).is_some(),
946                standard(code).is_some(),
947                "code {code:#04x}: standard_encoding_name/standard domain mismatch"
948            );
949            if let (Some(name), Some(expected)) = (standard_encoding_name(code), standard(code)) {
950                if let Some(resolved) = glyph_to_unicode(name) {
951                    assert_eq!(
952                        resolved, expected,
953                        "code {code:#04x} name {name:?}: glyph_to_unicode disagrees with standard"
954                    );
955                }
956            }
957        }
958    }
959
960    #[test]
961    fn standard_encoding_name_spot_checks() {
962        assert_eq!(standard_encoding_name(b'A'), Some("A"));
963        assert_eq!(standard_encoding_name(0x27), Some("quoteright"));
964        assert_eq!(standard_encoding_name(0x60), Some("quoteleft"));
965        assert_eq!(standard_encoding_name(0xA1), Some("exclamdown"));
966        assert_eq!(standard_encoding_name(0xA4), Some("fraction"));
967        assert_eq!(standard_encoding_name(0xA6), Some("florin"));
968        assert_eq!(standard_encoding_name(0xC1), Some("grave"));
969        assert_eq!(standard_encoding_name(0xC6), Some("breve"));
970        assert_eq!(standard_encoding_name(0xE1), Some("AE"));
971        assert_eq!(standard_encoding_name(0xF1), Some("ae"));
972        assert_eq!(standard_encoding_name(0xFB), Some("germandbls"));
973        assert_eq!(standard_encoding_name(0x7F), None);
974        assert_eq!(standard_encoding_name(0xA0), None);
975    }
976
977    #[test]
978    fn glyph_names_letters_and_tables() {
979        assert_eq!(glyph_to_unicode("A"), Some('A'));
980        assert_eq!(glyph_to_unicode("z"), Some('z'));
981        assert_eq!(glyph_to_unicode("alpha"), Some('\u{3B1}'));
982        assert_eq!(glyph_to_unicode("eacute"), Some('\u{E9}'));
983        assert_eq!(glyph_to_unicode("quotedblleft"), Some('\u{201C}'));
984        assert_eq!(glyph_to_unicode("seven"), Some('7'));
985        assert_eq!(glyph_to_unicode("union"), Some('\u{222A}'));
986        assert_eq!(glyph_to_unicode("nosuchglyphname"), None);
987    }
988
989    #[test]
990    fn glyph_text_ligatures_and_variants() {
991        assert_eq!(glyph_to_text("f_i").as_deref(), Some("fi"));
992        assert_eq!(glyph_to_text("f_l").as_deref(), Some("fl"));
993        assert_eq!(glyph_to_text("T_h").as_deref(), Some("Th"));
994        assert_eq!(glyph_to_text("f_f_i").as_deref(), Some("ffi"));
995        assert_eq!(glyph_to_text("eight.oldstyle").as_deref(), Some("8"));
996        assert_eq!(glyph_to_text("x.sc").as_deref(), Some("x"));
997        assert_eq!(glyph_to_text("C.a").as_deref(), Some("C"));
998        // Suffix stripping happens before underscore splitting.
999        assert_eq!(glyph_to_text("f_i.alt").as_deref(), Some("fi"));
1000        assert_eq!(glyph_to_text("uni00A0").as_deref(), Some("\u{A0}"));
1001        assert_eq!(glyph_to_text("eacute").as_deref(), Some("\u{E9}"));
1002    }
1003
1004    #[test]
1005    fn glyph_text_multi_group_uni() {
1006        assert_eq!(
1007            glyph_to_text("uni20AC0308").as_deref(),
1008            Some("\u{20AC}\u{0308}")
1009        );
1010        assert_eq!(glyph_to_text("uniD800DC00"), None); // surrogates never decode
1011    }
1012
1013    #[test]
1014    fn glyph_text_rejects_unknowns() {
1015        assert_eq!(glyph_to_text(".notdef"), None);
1016        assert_eq!(glyph_to_text(""), None);
1017        assert_eq!(glyph_to_text("glorp"), None);
1018        // Every component must resolve, or the whole name is unknown.
1019        assert_eq!(glyph_to_text("f_glorp"), None);
1020        assert_eq!(glyph_to_text("f__i"), None);
1021    }
1022}