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