Skip to main content

pdfboss_encoding/
lib.rs

1//! Shared PDF font-encoding tables (WinAnsi / MacRoman / Standard, from
2//! ISO 32000 Appendix D), glyph-name-to-Unicode resolution over the Adobe
3//! Glyph List, and the built-in encoding of a Type 1 font program, consumed
4//! by the pdfboss text-extraction and rendering crates.
5
6mod afm;
7mod agl;
8mod symbol;
9pub use afm::{is_standard_14, standard_14_width};
10
11/// WinAnsiEncoding codes `0x80..=0x9F` (the region that differs from
12/// Latin-1); `None` marks unassigned codes.
13const WIN_ANSI_80_9F: [Option<char>; 32] = [
14    Some('\u{20AC}'),
15    None,
16    Some('\u{201A}'),
17    Some('\u{0192}'),
18    Some('\u{201E}'),
19    Some('\u{2026}'),
20    Some('\u{2020}'),
21    Some('\u{2021}'),
22    Some('\u{02C6}'),
23    Some('\u{2030}'),
24    Some('\u{0160}'),
25    Some('\u{2039}'),
26    Some('\u{0152}'),
27    None,
28    Some('\u{017D}'),
29    None,
30    None,
31    Some('\u{2018}'),
32    Some('\u{2019}'),
33    Some('\u{201C}'),
34    Some('\u{201D}'),
35    Some('\u{2022}'),
36    Some('\u{2013}'),
37    Some('\u{2014}'),
38    Some('\u{02DC}'),
39    Some('\u{2122}'),
40    Some('\u{0161}'),
41    Some('\u{203A}'),
42    Some('\u{0153}'),
43    None,
44    Some('\u{017E}'),
45    Some('\u{0178}'),
46];
47
48/// Unicode value of `code` in `WinAnsiEncoding`.
49///
50/// Covers ISO 32000-1 Annex D.2.
51pub fn win_ansi(code: u8) -> Option<char> {
52    match code {
53        0x20..=0x7E => Some(code as char),
54        0x80..=0x9F => WIN_ANSI_80_9F[(code - 0x80) as usize],
55        0xA0..=0xFF => Some(code as char),
56        _ => None,
57    }
58}
59
60/// WinAnsiEncoding glyph name for `code` (ISO 32000-1 Annex D.2
61/// "WinAnsiEncoding" column). `None` for exactly the codes [`win_ansi`]
62/// leaves unassigned. Two ASCII codes diverge from StandardEncoding's
63/// names: `0x27` is `quotesingle` and `0x60` is `grave` (the straight
64/// marks, matching `win_ansi`'s identity mapping there). Two codes render
65/// an existing glyph rather than owning one: `0xA0` carries `space` (the
66/// nonbreaking space draws as the space glyph) and `0xAD` carries `hyphen`
67/// (likewise the soft hyphen) — see the self-verifying
68/// `win_ansi_glyph_name_matches_win_ansi_table` test below.
69///
70/// Covers ISO 32000-1 Annex D.2.
71pub fn win_ansi_glyph_name(code: u8) -> Option<&'static str> {
72    match code {
73        0x27 => Some("quotesingle"),
74        0x60 => Some("grave"),
75        0x20..=0x7E => Some(STANDARD_ASCII_NAMES[(code - 0x20) as usize]),
76        0x80..=0x9F => WIN_ANSI_80_9F_NAMES[(code - 0x80) as usize],
77        0xA0..=0xFF => Some(WIN_ANSI_A0_FF_NAMES[(code - 0xA0) as usize]),
78        _ => None,
79    }
80}
81
82/// WinAnsiEncoding glyph names for codes `0x80..=0x9F`, parallel to
83/// [`WIN_ANSI_80_9F`]; `None` marks the same unassigned codes.
84const WIN_ANSI_80_9F_NAMES: [Option<&str>; 32] = [
85    Some("Euro"),
86    None,
87    Some("quotesinglbase"),
88    Some("florin"),
89    Some("quotedblbase"),
90    Some("ellipsis"),
91    Some("dagger"),
92    Some("daggerdbl"),
93    Some("circumflex"),
94    Some("perthousand"),
95    Some("Scaron"),
96    Some("guilsinglleft"),
97    Some("OE"),
98    None,
99    Some("Zcaron"),
100    None,
101    None,
102    Some("quoteleft"),
103    Some("quoteright"),
104    Some("quotedblleft"),
105    Some("quotedblright"),
106    Some("bullet"),
107    Some("endash"),
108    Some("emdash"),
109    Some("tilde"),
110    Some("trademark"),
111    Some("scaron"),
112    Some("guilsinglright"),
113    Some("oe"),
114    None,
115    Some("zcaron"),
116    Some("Ydieresis"),
117];
118
119/// WinAnsiEncoding glyph names for codes `0xA0..=0xFF` (ISO 32000-1
120/// Annex D.2 "WinAnsiEncoding" column), in code order (index `0` is code
121/// `0xA0`).
122const WIN_ANSI_A0_FF_NAMES: [&str; 96] = [
123    "space",
124    "exclamdown",
125    "cent",
126    "sterling",
127    "currency",
128    "yen",
129    "brokenbar",
130    "section",
131    "dieresis",
132    "copyright",
133    "ordfeminine",
134    "guillemotleft",
135    "logicalnot",
136    "hyphen",
137    "registered",
138    "macron",
139    "degree",
140    "plusminus",
141    "twosuperior",
142    "threesuperior",
143    "acute",
144    "mu",
145    "paragraph",
146    "periodcentered",
147    "cedilla",
148    "onesuperior",
149    "ordmasculine",
150    "guillemotright",
151    "onequarter",
152    "onehalf",
153    "threequarters",
154    "questiondown",
155    "Agrave",
156    "Aacute",
157    "Acircumflex",
158    "Atilde",
159    "Adieresis",
160    "Aring",
161    "AE",
162    "Ccedilla",
163    "Egrave",
164    "Eacute",
165    "Ecircumflex",
166    "Edieresis",
167    "Igrave",
168    "Iacute",
169    "Icircumflex",
170    "Idieresis",
171    "Eth",
172    "Ntilde",
173    "Ograve",
174    "Oacute",
175    "Ocircumflex",
176    "Otilde",
177    "Odieresis",
178    "multiply",
179    "Oslash",
180    "Ugrave",
181    "Uacute",
182    "Ucircumflex",
183    "Udieresis",
184    "Yacute",
185    "Thorn",
186    "germandbls",
187    "agrave",
188    "aacute",
189    "acircumflex",
190    "atilde",
191    "adieresis",
192    "aring",
193    "ae",
194    "ccedilla",
195    "egrave",
196    "eacute",
197    "ecircumflex",
198    "edieresis",
199    "igrave",
200    "iacute",
201    "icircumflex",
202    "idieresis",
203    "eth",
204    "ntilde",
205    "ograve",
206    "oacute",
207    "ocircumflex",
208    "otilde",
209    "odieresis",
210    "divide",
211    "oslash",
212    "ugrave",
213    "uacute",
214    "ucircumflex",
215    "udieresis",
216    "yacute",
217    "thorn",
218    "ydieresis",
219];
220
221/// MacRomanEncoding codes `0x80..=0xFF` (codes below coincide with ASCII).
222const MAC_ROMAN_HIGH: [char; 128] = [
223    '\u{C4}', '\u{C5}', '\u{C7}', '\u{C9}', '\u{D1}', '\u{D6}', '\u{DC}', '\u{E1}', '\u{E0}',
224    '\u{E2}', '\u{E4}', '\u{E3}', '\u{E5}', '\u{E7}', '\u{E9}', '\u{E8}', '\u{EA}', '\u{EB}',
225    '\u{ED}', '\u{EC}', '\u{EE}', '\u{EF}', '\u{F1}', '\u{F3}', '\u{F2}', '\u{F4}', '\u{F6}',
226    '\u{F5}', '\u{FA}', '\u{F9}', '\u{FB}', '\u{FC}', '\u{2020}', '\u{B0}', '\u{A2}', '\u{A3}',
227    '\u{A7}', '\u{2022}', '\u{B6}', '\u{DF}', '\u{AE}', '\u{A9}', '\u{2122}', '\u{B4}', '\u{A8}',
228    '\u{2260}', '\u{C6}', '\u{D8}', '\u{221E}', '\u{B1}', '\u{2264}', '\u{2265}', '\u{A5}',
229    '\u{B5}', '\u{2202}', '\u{2211}', '\u{220F}', '\u{3C0}', '\u{222B}', '\u{AA}', '\u{BA}',
230    '\u{3A9}', '\u{E6}', '\u{F8}', '\u{BF}', '\u{A1}', '\u{AC}', '\u{221A}', '\u{192}', '\u{2248}',
231    '\u{2206}', '\u{AB}', '\u{BB}', '\u{2026}', '\u{A0}', '\u{C0}', '\u{C3}', '\u{D5}', '\u{152}',
232    '\u{153}', '\u{2013}', '\u{2014}', '\u{201C}', '\u{201D}', '\u{2018}', '\u{2019}', '\u{F7}',
233    '\u{25CA}', '\u{FF}', '\u{178}', '\u{2044}', '\u{20AC}', '\u{2039}', '\u{203A}', '\u{FB01}',
234    '\u{FB02}', '\u{2021}', '\u{B7}', '\u{201A}', '\u{201E}', '\u{2030}', '\u{C2}', '\u{CA}',
235    '\u{C1}', '\u{CB}', '\u{C8}', '\u{CD}', '\u{CE}', '\u{CF}', '\u{CC}', '\u{D3}', '\u{D4}',
236    '\u{F8FF}', '\u{D2}', '\u{DA}', '\u{DB}', '\u{D9}', '\u{131}', '\u{2C6}', '\u{2DC}', '\u{AF}',
237    '\u{2D8}', '\u{2D9}', '\u{2DA}', '\u{B8}', '\u{2DD}', '\u{2DB}', '\u{2C7}',
238];
239
240/// Unicode value of `code` in `MacRomanEncoding`.
241pub fn mac_roman(code: u8) -> Option<char> {
242    match code {
243        0x20..=0x7E => Some(code as char),
244        0x80..=0xFF => Some(MAC_ROMAN_HIGH[(code - 0x80) as usize]),
245        _ => None,
246    }
247}
248
249/// MacRomanEncoding glyph names for codes `0x80..=0xFF` (ISO 32000-1
250/// Annex D.2 "MacRomanEncoding" column), in code order (index `0` is code
251/// `0x80`). Names are paired with [`MAC_ROMAN_HIGH`] entry for entry: `0xCA`
252/// carries `space` (the nonbreaking space draws as the space glyph) and
253/// `0xDB` carries `Euro`, which is what that table holds for the code.
254#[rustfmt::skip]
255const MAC_ROMAN_HIGH_NAMES: [&str; 128] = [
256    "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis", "aacute",
257    "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute", "egrave",
258    "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", "ntilde", "oacute",
259    "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex", "udieresis",
260    "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph", "germandbls",
261    "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE", "Oslash",
262    "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff", "summation",
263    "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", "ae", "oslash",
264    "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal", "Delta", "guillemotleft",
265    "guillemotright", "ellipsis", "space", "Agrave", "Atilde", "Otilde", "OE", "oe",
266    "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright", "divide", "lozenge",
267    "ydieresis", "Ydieresis", "fraction", "Euro", "guilsinglleft", "guilsinglright", "fi", "fl",
268    "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", "Aacute",
269    "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex",
270    "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde",
271    "macron", "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron",
272];
273
274/// MacRomanEncoding glyph name for `code` (ISO 32000-1 Annex D.2
275/// "MacRomanEncoding" column). `None` for exactly the codes [`mac_roman`]
276/// leaves unassigned. As in WinAnsi, `0x27` is `quotesingle` and `0x60` is
277/// `grave`, and `0xCA` renders the space glyph for the nonbreaking space.
278/// See the self-verifying `mac_roman_glyph_name_matches_mac_roman_table`
279/// test below.
280///
281/// Covers ISO 32000-1 Annex D.2.
282pub fn mac_roman_glyph_name(code: u8) -> Option<&'static str> {
283    match code {
284        0x27 => Some("quotesingle"),
285        0x60 => Some("grave"),
286        0x20..=0x7E => Some(STANDARD_ASCII_NAMES[(code - 0x20) as usize]),
287        0x80..=0xFF => Some(MAC_ROMAN_HIGH_NAMES[(code - 0x80) as usize]),
288        _ => None,
289    }
290}
291
292/// StandardEncoding codes above 0x7E that are assigned (sparse).
293const STANDARD_HIGH: &[(u8, char)] = &[
294    (0xA1, '\u{A1}'),
295    (0xA2, '\u{A2}'),
296    (0xA3, '\u{A3}'),
297    (0xA4, '\u{2044}'),
298    (0xA5, '\u{A5}'),
299    (0xA6, '\u{192}'),
300    (0xA7, '\u{A7}'),
301    (0xA8, '\u{A4}'),
302    (0xA9, '\u{27}'),
303    (0xAA, '\u{201C}'),
304    (0xAB, '\u{AB}'),
305    (0xAC, '\u{2039}'),
306    (0xAD, '\u{203A}'),
307    (0xAE, '\u{FB01}'),
308    (0xAF, '\u{FB02}'),
309    (0xB1, '\u{2013}'),
310    (0xB2, '\u{2020}'),
311    (0xB3, '\u{2021}'),
312    (0xB4, '\u{B7}'),
313    (0xB6, '\u{B6}'),
314    (0xB7, '\u{2022}'),
315    (0xB8, '\u{201A}'),
316    (0xB9, '\u{201E}'),
317    (0xBA, '\u{201D}'),
318    (0xBB, '\u{BB}'),
319    (0xBC, '\u{2026}'),
320    (0xBD, '\u{2030}'),
321    (0xBF, '\u{BF}'),
322    (0xC1, '\u{60}'),
323    (0xC2, '\u{B4}'),
324    (0xC3, '\u{2C6}'),
325    (0xC4, '\u{2DC}'),
326    (0xC5, '\u{AF}'),
327    (0xC6, '\u{2D8}'),
328    (0xC7, '\u{2D9}'),
329    (0xC8, '\u{A8}'),
330    (0xCA, '\u{2DA}'),
331    (0xCB, '\u{B8}'),
332    (0xCD, '\u{2DD}'),
333    (0xCE, '\u{2DB}'),
334    (0xCF, '\u{2C7}'),
335    (0xD0, '\u{2014}'),
336    (0xE1, '\u{C6}'),
337    (0xE3, '\u{AA}'),
338    (0xE8, '\u{141}'),
339    (0xE9, '\u{D8}'),
340    (0xEA, '\u{152}'),
341    (0xEB, '\u{BA}'),
342    (0xF1, '\u{E6}'),
343    (0xF5, '\u{131}'),
344    (0xF8, '\u{142}'),
345    (0xF9, '\u{F8}'),
346    (0xFA, '\u{153}'),
347    (0xFB, '\u{DF}'),
348];
349
350/// Unicode value of `code` in `StandardEncoding`.
351pub fn standard(code: u8) -> Option<char> {
352    match code {
353        0x27 => Some('\u{2019}'),
354        0x60 => Some('\u{2018}'),
355        0x20..=0x7E => Some(code as char),
356        0xA1..=0xFF => STANDARD_HIGH
357            .iter()
358            .find(|&&(c, _)| c == code)
359            .map(|&(_, u)| u),
360        _ => None,
361    }
362}
363
364/// StandardEncoding names for codes `0x20..=0x7E` (space..asciitilde), in
365/// code order (index `0` is code `0x20`). Two codes diverge from their plain
366/// ASCII name: `0x27` is `quoteright` (a curly right quote, not the straight
367/// `quotesingle` apostrophe) and `0x60` is `quoteleft` (a curly left quote,
368/// not `grave`) -- matching `standard`'s `0x27`/`0x60` special cases above.
369const STANDARD_ASCII_NAMES: [&str; 95] = [
370    "space",
371    "exclam",
372    "quotedbl",
373    "numbersign",
374    "dollar",
375    "percent",
376    "ampersand",
377    "quoteright",
378    "parenleft",
379    "parenright",
380    "asterisk",
381    "plus",
382    "comma",
383    "hyphen",
384    "period",
385    "slash",
386    "zero",
387    "one",
388    "two",
389    "three",
390    "four",
391    "five",
392    "six",
393    "seven",
394    "eight",
395    "nine",
396    "colon",
397    "semicolon",
398    "less",
399    "equal",
400    "greater",
401    "question",
402    "at",
403    "A",
404    "B",
405    "C",
406    "D",
407    "E",
408    "F",
409    "G",
410    "H",
411    "I",
412    "J",
413    "K",
414    "L",
415    "M",
416    "N",
417    "O",
418    "P",
419    "Q",
420    "R",
421    "S",
422    "T",
423    "U",
424    "V",
425    "W",
426    "X",
427    "Y",
428    "Z",
429    "bracketleft",
430    "backslash",
431    "bracketright",
432    "asciicircum",
433    "underscore",
434    "quoteleft",
435    "a",
436    "b",
437    "c",
438    "d",
439    "e",
440    "f",
441    "g",
442    "h",
443    "i",
444    "j",
445    "k",
446    "l",
447    "m",
448    "n",
449    "o",
450    "p",
451    "q",
452    "r",
453    "s",
454    "t",
455    "u",
456    "v",
457    "w",
458    "x",
459    "y",
460    "z",
461    "braceleft",
462    "bar",
463    "braceright",
464    "asciitilde",
465];
466
467/// StandardEncoding names for codes above `0x7E` (ISO 32000-1 Annex D.2
468/// "StandardEncoding" column), parallel to [`STANDARD_HIGH`]'s codes, in the
469/// same order.
470const STANDARD_HIGH_NAMES: &[(u8, &str)] = &[
471    (0xA1, "exclamdown"),
472    (0xA2, "cent"),
473    (0xA3, "sterling"),
474    (0xA4, "fraction"),
475    (0xA5, "yen"),
476    (0xA6, "florin"),
477    (0xA7, "section"),
478    (0xA8, "currency"),
479    (0xA9, "quotesingle"),
480    (0xAA, "quotedblleft"),
481    (0xAB, "guillemotleft"),
482    (0xAC, "guilsinglleft"),
483    (0xAD, "guilsinglright"),
484    (0xAE, "fi"),
485    (0xAF, "fl"),
486    (0xB1, "endash"),
487    (0xB2, "dagger"),
488    (0xB3, "daggerdbl"),
489    (0xB4, "periodcentered"),
490    (0xB6, "paragraph"),
491    (0xB7, "bullet"),
492    (0xB8, "quotesinglbase"),
493    (0xB9, "quotedblbase"),
494    (0xBA, "quotedblright"),
495    (0xBB, "guillemotright"),
496    (0xBC, "ellipsis"),
497    (0xBD, "perthousand"),
498    (0xBF, "questiondown"),
499    (0xC1, "grave"),
500    (0xC2, "acute"),
501    (0xC3, "circumflex"),
502    (0xC4, "tilde"),
503    (0xC5, "macron"),
504    (0xC6, "breve"),
505    (0xC7, "dotaccent"),
506    (0xC8, "dieresis"),
507    (0xCA, "ring"),
508    (0xCB, "cedilla"),
509    (0xCD, "hungarumlaut"),
510    (0xCE, "ogonek"),
511    (0xCF, "caron"),
512    (0xD0, "emdash"),
513    (0xE1, "AE"),
514    (0xE3, "ordfeminine"),
515    (0xE8, "Lslash"),
516    (0xE9, "Oslash"),
517    (0xEA, "OE"),
518    (0xEB, "ordmasculine"),
519    (0xF1, "ae"),
520    (0xF5, "dotlessi"),
521    (0xF8, "lslash"),
522    (0xF9, "oslash"),
523    (0xFA, "oe"),
524    (0xFB, "germandbls"),
525];
526
527/// Adobe StandardEncoding glyph name for `code` (ISO 32000-1 Annex D.2
528/// "StandardEncoding" column; equivalently Adobe Type 1 Font Format
529/// Appendix C). `None` for exactly the codes `standard` leaves unassigned
530/// (see the self-verifying `standard_encoding_name_matches_standard_table`
531/// test below, which ties this table to that one so an authoring mistake
532/// here fails a test rather than silently mis-encoding a glyph).
533pub fn standard_encoding_name(code: u8) -> Option<&'static str> {
534    match code {
535        0x20..=0x7E => Some(STANDARD_ASCII_NAMES[(code - 0x20) as usize]),
536        0xA1..=0xFF => STANDARD_HIGH_NAMES
537            .iter()
538            .find(|&&(c, _)| c == code)
539            .map(|&(_, n)| n),
540        _ => None,
541    }
542}
543
544/// The Mac OS Roman code for `ch`: MacRomanEncoding (Annex D.2) plus the
545/// fifteen additions and the Euro of Table 115, which is the encoding a
546/// TrueType `(1, 0)` cmap subtable is indexed by. `None` for a character
547/// the encoding lacks.
548///
549/// Covers ISO 32000-1 §9.6.6.4.
550pub fn mac_os_roman_code(ch: char) -> Option<u8> {
551    // `MAC_ROMAN_HIGH` already carries Table 115's additions and the Euro;
552    // Omega is stored as the Greek letter, while the glyph list resolves the
553    // name to the ohm sign, so both reach code 189.
554    if ch == '\u{2126}' {
555        return Some(0xBD);
556    }
557    if ('\u{20}'..='\u{7E}').contains(&ch) {
558        return Some(ch as u8);
559    }
560    MAC_ROMAN_HIGH
561        .iter()
562        .position(|&c| c == ch)
563        .map(|i| 0x80 + i as u8)
564}
565
566/// The glyph name Symbol's built-in encoding gives `code` (ISO 32000-1
567/// Annex D.5), `None` for an unassigned code.
568///
569/// Covers ISO 32000-1 Annex D.5.
570pub fn symbol_glyph_name(code: u8) -> Option<&'static str> {
571    let i = symbol::SYMBOL
572        .binary_search_by_key(&code, |&(c, _)| c)
573        .ok()?;
574    Some(symbol::SYMBOL[i].1)
575}
576
577/// The character Symbol's built-in encoding gives `code`: its glyph name
578/// through the Adobe Glyph List, so the six bracket and radical pieces
579/// resolve to their Corporate Use Subarea code points.
580///
581/// Covers ISO 32000-1 Annex D.5.
582pub fn symbol(code: u8) -> Option<char> {
583    glyph_to_unicode(symbol_glyph_name(code)?)
584}
585
586/// The glyph name ZapfDingbats' built-in encoding gives `code` (ISO 32000-1
587/// Annex D.6), `None` for an unassigned code.
588///
589/// Covers ISO 32000-1 Annex D.6.
590pub fn zapf_dingbats_glyph_name(code: u8) -> Option<&'static str> {
591    let i = symbol::ZAPF_DINGBATS
592        .binary_search_by_key(&code, |&(c, _, _)| c)
593        .ok()?;
594    Some(symbol::ZAPF_DINGBATS[i].1)
595}
596
597/// The character ZapfDingbats' built-in encoding gives `code`, from the
598/// Dingbats block; the glyph list does not name these, so the table carries
599/// the value itself.
600///
601/// Covers ISO 32000-1 Annex D.6.
602pub fn zapf_dingbats(code: u8) -> Option<char> {
603    let i = symbol::ZAPF_DINGBATS
604        .binary_search_by_key(&code, |&(c, _, _)| c)
605        .ok()?;
606    Some(symbol::ZAPF_DINGBATS[i].2)
607}
608
609/// Resolves a glyph name (as used in `/Differences`) to a Unicode scalar:
610/// `uniXXXX` and `uXXXX`–`uXXXXXX` hex forms, single ASCII letters, the
611/// Adobe Glyph List, and the TeX math names the list never adopted. `None`
612/// for an unknown name, and for a listed name whose text is more than one
613/// scalar; [`glyph_to_text`] resolves those.
614pub fn glyph_to_unicode(name: &str) -> Option<char> {
615    if let Some(hex) = name.strip_prefix("uni") {
616        if hex.len() == 4 && hex.bytes().all(|b| b.is_ascii_hexdigit()) {
617            return char::from_u32(u32::from_str_radix(hex, 16).ok()?);
618        }
619    }
620    if let Some(hex) = name.strip_prefix('u') {
621        if (4..=6).contains(&hex.len()) && hex.bytes().all(|b| b.is_ascii_hexdigit()) {
622            return char::from_u32(u32::from_str_radix(hex, 16).ok()?);
623        }
624    }
625    let mut chars = name.chars();
626    if let (Some(c), None) = (chars.next(), chars.next()) {
627        if c.is_ascii_alphabetic() {
628            return Some(c);
629        }
630    }
631    if let Some(text) = agl_text(name) {
632        let mut chars = text.chars();
633        return match (chars.next(), chars.next()) {
634            (Some(c), None) => Some(c),
635            _ => None,
636        };
637    }
638    tex_glyph(name)
639}
640
641/// Resolves a glyph name to the text it represents, per the Adobe Glyph
642/// List algorithm: everything from the first period on is dropped
643/// (`eight.oldstyle` → `8`), underscore-joined components each resolve and
644/// concatenate (`f_i` → `fi`, `T_h` → `Th`), and a `uni` prefix may carry
645/// several 4-digit hex groups. `None` unless every component resolves —
646/// a partially-resolved ligature would silently drop letters, where the
647/// caller's U+FFFD at least stays visible.
648pub fn glyph_to_text(name: &str) -> Option<String> {
649    let base = name.split('.').next().unwrap_or_default();
650    if base.is_empty() {
651        return None;
652    }
653    let mut out = String::new();
654    for component in base.split('_') {
655        push_component(component, &mut out)?;
656    }
657    Some(out)
658}
659
660/// Appends one underscore-separated component of a glyph name; `None` when
661/// the component resolves to nothing.
662fn push_component(component: &str, out: &mut String) -> Option<()> {
663    let hex = component.strip_prefix("uni").unwrap_or_default();
664    if hex.len() >= 8 && hex.len().is_multiple_of(4) && hex.bytes().all(|b| b.is_ascii_hexdigit()) {
665        // Multi-group form: `uni20AC0308` is two scalars. The single-group
666        // form stays on the `glyph_to_unicode` path below.
667        for group in hex.as_bytes().chunks(4) {
668            let group = std::str::from_utf8(group).ok()?;
669            let scalar = u32::from_str_radix(group, 16).ok()?;
670            out.push(char::from_u32(scalar)?);
671        }
672        return Some(());
673    }
674    if let Some(text) = agl_text(component) {
675        out.push_str(text);
676        return Some(());
677    }
678    out.push(glyph_to_unicode(component)?);
679    Some(())
680}
681
682/// The Adobe Glyph List text for `name`, when the list carries it.
683fn agl_text(name: &str) -> Option<&'static str> {
684    let index = agl::AGL.binary_search_by(|(n, _)| n.cmp(&name)).ok()?;
685    Some(agl::AGL[index].1)
686}
687
688/// The TeX math name's scalar, when [`GLYPHS_TEX`] carries it.
689fn tex_glyph(name: &str) -> Option<char> {
690    let index = GLYPHS_TEX.binary_search_by(|(n, _)| n.cmp(&name)).ok()?;
691    Some(GLYPHS_TEX[index].1)
692}
693
694/// Glyph names of the TeX symbol, math-italic and extension fonts that the
695/// Adobe Glyph List does not carry, sorted by name. The extension font's
696/// size variants of a delimiter or operator (`parenleftbig`, `parenleftBig`,
697/// `summationdisplay`) all stand for the one character.
698const GLYPHS_TEX: &[(&str, char)] = &[
699    ("Circle", '\u{25CB}'),
700    ("Ifractur", '\u{2111}'),
701    ("Rfractur", '\u{211C}'),
702    ("angbracketleft", '\u{27E8}'),
703    ("angbracketleftBig", '\u{27E8}'),
704    ("angbracketleftBigg", '\u{27E8}'),
705    ("angbracketleftbig", '\u{27E8}'),
706    ("angbracketleftbigg", '\u{27E8}'),
707    ("angbracketright", '\u{27E9}'),
708    ("angbracketrightBig", '\u{27E9}'),
709    ("angbracketrightBigg", '\u{27E9}'),
710    ("angbracketrightbig", '\u{27E9}'),
711    ("angbracketrightbigg", '\u{27E9}'),
712    ("arrowbothv", '\u{2195}'),
713    ("arrowdblbothv", '\u{21D5}'),
714    ("arrowhookleft", '\u{21A9}'),
715    ("arrowhookright", '\u{21AA}'),
716    ("arrowleftbothalf", '\u{21BD}'),
717    ("arrowlefttophalf", '\u{21BC}'),
718    ("arrownortheast", '\u{2197}'),
719    ("arrownorthwest", '\u{2196}'),
720    ("arrowrightbothalf", '\u{21C1}'),
721    ("arrowrighttophalf", '\u{21C0}'),
722    ("arrowsoutheast", '\u{2198}'),
723    ("arrowsouthwest", '\u{2199}'),
724    ("asteriskmath", '\u{2217}'),
725    ("backslashBig", '\\'),
726    ("backslashBigg", '\\'),
727    ("backslashbig", '\\'),
728    ("backslashbigg", '\\'),
729    ("bardbl", '\u{2016}'),
730    ("braceleftBig", '{'),
731    ("braceleftBigg", '{'),
732    ("braceleftbig", '{'),
733    ("braceleftbigg", '{'),
734    ("bracerightBig", '}'),
735    ("bracerightBigg", '}'),
736    ("bracerightbig", '}'),
737    ("bracerightbigg", '}'),
738    ("bracketleftBig", '['),
739    ("bracketleftBigg", '['),
740    ("bracketleftbig", '['),
741    ("bracketleftbigg", '['),
742    ("bracketrightBig", ']'),
743    ("bracketrightBigg", ']'),
744    ("bracketrightbig", ']'),
745    ("bracketrightbigg", ']'),
746    ("ceilingleft", '\u{2308}'),
747    ("ceilingleftBig", '\u{2308}'),
748    ("ceilingleftBigg", '\u{2308}'),
749    ("ceilingleftbig", '\u{2308}'),
750    ("ceilingleftbigg", '\u{2308}'),
751    ("ceilingright", '\u{2309}'),
752    ("ceilingrightBig", '\u{2309}'),
753    ("ceilingrightBigg", '\u{2309}'),
754    ("ceilingrightbig", '\u{2309}'),
755    ("ceilingrightbigg", '\u{2309}'),
756    ("circlecopyrt", '\u{A9}'),
757    ("circledivide", '\u{2298}'),
758    ("circledot", '\u{2299}'),
759    ("circledotdisplay", '\u{2A00}'),
760    ("circledottext", '\u{2A00}'),
761    ("circleminus", '\u{2296}'),
762    ("circlemultiply", '\u{2297}'),
763    ("circlemultiplydisplay", '\u{2A02}'),
764    ("circlemultiplytext", '\u{2A02}'),
765    ("circleplus", '\u{2295}'),
766    ("circleplusdisplay", '\u{2A01}'),
767    ("circleplustext", '\u{2A01}'),
768    ("contintegraldisplay", '\u{222E}'),
769    ("contintegraltext", '\u{222E}'),
770    ("coproduct", '\u{2210}'),
771    ("coproductdisplay", '\u{2210}'),
772    ("coproducttext", '\u{2210}'),
773    ("diamondmath", '\u{22C4}'),
774    ("dotlessj", '\u{237}'),
775    ("epsilon1", '\u{3F5}'),
776    ("equivasymptotic", '\u{224D}'),
777    ("flat", '\u{266D}'),
778    ("floorleft", '\u{230A}'),
779    ("floorleftBig", '\u{230A}'),
780    ("floorleftBigg", '\u{230A}'),
781    ("floorleftbig", '\u{230A}'),
782    ("floorleftbigg", '\u{230A}'),
783    ("floorright", '\u{230B}'),
784    ("floorrightBig", '\u{230B}'),
785    ("floorrightBigg", '\u{230B}'),
786    ("floorrightbig", '\u{230B}'),
787    ("floorrightbigg", '\u{230B}'),
788    ("followsequal", '\u{227D}'),
789    ("greatermuch", '\u{226B}'),
790    ("greaterorsimilar", '\u{2273}'),
791    ("hatwide", '^'),
792    ("hatwider", '^'),
793    ("hatwidest", '^'),
794    ("integraldisplay", '\u{222B}'),
795    ("integraltext", '\u{222B}'),
796    ("intercal", '\u{22BA}'),
797    ("intersectiondisplay", '\u{22C2}'),
798    ("intersectionsq", '\u{2293}'),
799    ("intersectionsqdisplay", '\u{2A05}'),
800    ("intersectionsqtext", '\u{2A05}'),
801    ("intersectiontext", '\u{22C2}'),
802    ("latticetop", '\u{22A4}'),
803    ("lessmuch", '\u{226A}'),
804    ("lessorsimilar", '\u{2272}'),
805    ("logicalanddisplay", '\u{22C0}'),
806    ("logicalandtext", '\u{22C0}'),
807    ("logicalordisplay", '\u{22C1}'),
808    ("logicalortext", '\u{22C1}'),
809    ("lscript", '\u{2113}'),
810    ("mapsto", '\u{21A6}'),
811    ("measuredangle", '\u{2221}'),
812    ("minusplus", '\u{2213}'),
813    ("natural", '\u{266E}'),
814    ("negationslash", '\u{0338}'),
815    ("nequal", '\u{2260}'),
816    ("notexistential", '\u{2204}'),
817    ("openbullet", '\u{25E6}'),
818    ("owner", '\u{220B}'),
819    ("parenleftBig", '('),
820    ("parenleftBigg", '('),
821    ("parenleftbig", '('),
822    ("parenleftbigg", '('),
823    ("parenrightBig", ')'),
824    ("parenrightBigg", ')'),
825    ("parenrightbig", ')'),
826    ("parenrightbigg", ')'),
827    ("phi1", '\u{3D5}'),
828    ("pi1", '\u{3D6}'),
829    ("precedesequal", '\u{227C}'),
830    ("prime", '\u{2032}'),
831    ("productdisplay", '\u{220F}'),
832    ("producttext", '\u{220F}'),
833    ("radicalBig", '\u{221A}'),
834    ("radicalBigg", '\u{221A}'),
835    ("radicalbig", '\u{221A}'),
836    ("radicalbigg", '\u{221A}'),
837    ("rho1", '\u{3F1}'),
838    ("sharp", '\u{266F}'),
839    ("sigma1", '\u{3C2}'),
840    ("similarequal", '\u{2243}'),
841    ("slashBig", '/'),
842    ("slashBigg", '/'),
843    ("slashbig", '/'),
844    ("slashbigg", '/'),
845    ("slurabove", '\u{2322}'),
846    ("slurbelow", '\u{2323}'),
847    ("squaresolid", '\u{25A0}'),
848    ("star", '\u{22C6}'),
849    ("subsetnoteql", '\u{228A}'),
850    ("subsetsqequal", '\u{2291}'),
851    ("summationdisplay", '\u{2211}'),
852    ("summationtext", '\u{2211}'),
853    ("supersetsqequal", '\u{2292}'),
854    ("theta1", '\u{3D1}'),
855    ("tie", '\u{2040}'),
856    ("tildewide", '~'),
857    ("tildewider", '~'),
858    ("tildewidest", '~'),
859    ("triangle", '\u{25B3}'),
860    ("triangleinv", '\u{25BD}'),
861    ("triangleleft", '\u{25C1}'),
862    ("triangleright", '\u{25B7}'),
863    ("turnstileleft", '\u{22A2}'),
864    ("turnstileright", '\u{22A3}'),
865    ("uniondisplay", '\u{22C3}'),
866    ("unionmulti", '\u{228E}'),
867    ("unionmultidisplay", '\u{2A04}'),
868    ("unionmultitext", '\u{2A04}'),
869    ("unionsq", '\u{2294}'),
870    ("unionsqdisplay", '\u{2A06}'),
871    ("unionsqtext", '\u{2A06}'),
872    ("uniontext", '\u{22C3}'),
873    ("vector", '\u{20D7}'),
874    ("vextenddouble", '\u{2016}'),
875    ("vextendsingle", '|'),
876    ("weierstrass", '\u{2118}'),
877    ("wreathproduct", '\u{2240}'),
878];
879
880/// The built-in encoding of an embedded Type 1 font program: code to glyph
881/// name, read from the program's clear-text portion, which is where ISO
882/// 32000-1 9.6.6 sends a simple font that states no usable `/Encoding` of
883/// its own. A bare `StandardEncoding` token expands to that table, and any
884/// `dup <code> /<name> put` entries override it code by code. `None` when
885/// the program states no `/Encoding` at all.
886///
887/// Covers ISO 32000-1 §9.6.6.2.
888pub fn type1_builtin_encoding(program: &[u8]) -> Option<Box<[Option<String>; 256]>> {
889    let clear = type1_clear_text(program);
890    let mut tokens = PsTokens {
891        bytes: clear,
892        at: 0,
893    };
894    tokens.find(|t| *t == b"/Encoding")?;
895    let mut table: Box<[Option<String>; 256]> = Box::new(std::array::from_fn(|_| None));
896    let tokens: Vec<&[u8]> = tokens.collect();
897    if tokens.first() == Some(&b"StandardEncoding".as_slice()) {
898        for (code, slot) in table.iter_mut().enumerate() {
899            *slot = standard_encoding_name(code as u8).map(String::from);
900        }
901    }
902    for entry in tokens.windows(4) {
903        if entry[0] != b"dup" || entry[3] != b"put" {
904            continue;
905        }
906        let Some(code) = std::str::from_utf8(entry[1])
907            .ok()
908            .and_then(|t| t.parse::<usize>().ok())
909        else {
910            continue;
911        };
912        let Some(name) = entry[2]
913            .strip_prefix(b"/")
914            .and_then(|n| std::str::from_utf8(n).ok())
915        else {
916            continue;
917        };
918        if let Some(slot) = table.get_mut(code) {
919            *slot = Some(name.to_string());
920        }
921    }
922    Some(table)
923}
924
925/// The clear-text portion of a Type 1 program: the type-1 segments of a PFB
926/// wrapper, or everything up to the `eexec` token of a raw program (the
927/// whole program when there is none).
928fn type1_clear_text(program: &[u8]) -> &[u8] {
929    if program.first() == Some(&0x80) {
930        return pfb_clear_text(program);
931    }
932    const TOKEN: &[u8] = b"eexec";
933    let end = program
934        .windows(TOKEN.len())
935        .position(|w| w == TOKEN)
936        .unwrap_or(program.len());
937    &program[..end]
938}
939
940/// The first PFB segment's payload when it is clear text; the clear text of
941/// every real font is one segment, and a header claiming more bytes than
942/// the program holds yields whatever the program does hold.
943fn pfb_clear_text(program: &[u8]) -> &[u8] {
944    if program.get(1) != Some(&0x01) || program.len() < 6 {
945        return &[];
946    }
947    let len = u32::from_le_bytes([program[2], program[3], program[4], program[5]]) as usize;
948    let end = len.saturating_add(6).min(program.len());
949    &program[6..end]
950}
951
952/// PostScript tokens of a clear-text font header: whitespace-separated
953/// runs, with `%` comments skipped and each of `()<>[]{}` a token of its
954/// own. A `/` starts a name token.
955struct PsTokens<'a> {
956    bytes: &'a [u8],
957    at: usize,
958}
959
960impl<'a> Iterator for PsTokens<'a> {
961    type Item = &'a [u8];
962
963    fn next(&mut self) -> Option<&'a [u8]> {
964        loop {
965            let &b = self.bytes.get(self.at)?;
966            if is_ps_whitespace(b) {
967                self.at += 1;
968                continue;
969            }
970            if b != b'%' {
971                break;
972            }
973            while self
974                .bytes
975                .get(self.at)
976                .is_some_and(|&b| b != b'\n' && b != b'\r')
977            {
978                self.at += 1;
979            }
980        }
981        let start = self.at;
982        if is_ps_delimiter(self.bytes[start]) {
983            self.at += 1;
984            return Some(&self.bytes[start..self.at]);
985        }
986        self.at += 1;
987        while self
988            .bytes
989            .get(self.at)
990            .is_some_and(|&b| !is_ps_whitespace(b) && !is_ps_delimiter(b) && b != b'/')
991        {
992            self.at += 1;
993        }
994        Some(&self.bytes[start..self.at])
995    }
996}
997
998fn is_ps_whitespace(b: u8) -> bool {
999    matches!(b, b' ' | b'\t' | b'\r' | b'\n' | b'\x0C' | b'\0')
1000}
1001
1002fn is_ps_delimiter(b: u8) -> bool {
1003    matches!(b, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}')
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008    use super::*;
1009
1010    // Covers ISO 32000-1 §9.6.6.1 and Annex D.2.
1011    #[test]
1012    fn win_ansi_spot_checks() {
1013        assert_eq!(win_ansi(b'A'), Some('A'));
1014        assert_eq!(win_ansi(0x93), Some('\u{201C}')); // left double quote
1015        assert_eq!(win_ansi(0x80), Some('\u{20AC}')); // euro sign
1016        assert_eq!(win_ansi(0xE9), Some('\u{E9}')); // e acute (Latin-1)
1017        assert_eq!(win_ansi(0x81), None); // unassigned
1018        assert_eq!(win_ansi(0x0A), None); // control
1019    }
1020
1021    #[test]
1022    fn win_ansi_glyph_name_spot_checks() {
1023        assert_eq!(win_ansi_glyph_name(0x41), Some("A"));
1024        assert_eq!(win_ansi_glyph_name(0x20), Some("space"));
1025        assert_eq!(win_ansi_glyph_name(0x27), Some("quotesingle")); // not quoteright
1026        assert_eq!(win_ansi_glyph_name(0x60), Some("grave")); // not quoteleft
1027        assert_eq!(win_ansi_glyph_name(0x80), Some("Euro"));
1028        assert_eq!(win_ansi_glyph_name(0x93), Some("quotedblleft"));
1029        assert_eq!(win_ansi_glyph_name(0xE9), Some("eacute"));
1030        assert_eq!(win_ansi_glyph_name(0xFF), Some("ydieresis"));
1031        assert_eq!(win_ansi_glyph_name(0x81), None); // unassigned
1032        assert_eq!(win_ansi_glyph_name(0x0A), None); // control
1033    }
1034
1035    /// Self-verifying anchor for `win_ansi_glyph_name`: ties the name table
1036    /// to the pre-existing, trusted `win_ansi` (code -> Unicode) and
1037    /// `glyph_to_unicode` (name -> Unicode) tables. Domain equality must
1038    /// hold for every code, and every name must resolve to the code's
1039    /// Unicode value — with exactly two documented exceptions, codes that
1040    /// render an existing glyph rather than owning one: `0xA0` (nonbreaking
1041    /// space, drawn by `space`) and `0xAD` (soft hyphen, drawn by `hyphen`).
1042    /// The MacRoman name table is tied to the MacRoman value table the same
1043    /// way: same domain, every name resolving to the code's value, with two
1044    /// exceptions: `0xCA` (nonbreaking space, drawn by `space`) and `0xBD`,
1045    /// whose name `Omega` the glyph list gives the Ohm sign U+2126 while the
1046    /// value table holds the Greek capital U+03A9 (the same letter).
1047    // Covers ISO 32000-1 Annex D.2.
1048    #[test]
1049    fn mac_roman_glyph_name_matches_mac_roman_table() {
1050        assert_eq!(mac_roman_glyph_name(0xCA), Some("space"));
1051        assert_eq!(mac_roman_glyph_name(0xBD), Some("Omega"));
1052        assert_eq!(mac_roman_glyph_name(0xA5), Some("bullet"));
1053        assert_eq!(mac_roman_glyph_name(0xDE), Some("fi"));
1054        for code in 0u16..=255 {
1055            let code = code as u8;
1056            assert_eq!(
1057                mac_roman_glyph_name(code).is_some(),
1058                mac_roman(code).is_some(),
1059                "code {code:#04x}: mac_roman_glyph_name/mac_roman domain mismatch"
1060            );
1061            let Some(name) = mac_roman_glyph_name(code) else {
1062                continue;
1063            };
1064            let expected = match code {
1065                0xCA => ' ',
1066                0xBD => '\u{2126}',
1067                _ => mac_roman(code).unwrap(),
1068            };
1069            assert_eq!(
1070                glyph_to_unicode(name),
1071                Some(expected),
1072                "code {code:#04x} name {name:?}: glyph_to_unicode disagrees with mac_roman"
1073            );
1074        }
1075    }
1076
1077    // Covers ISO 32000-1 Annex D.2.
1078    #[test]
1079    fn win_ansi_glyph_name_matches_win_ansi_table() {
1080        assert_eq!(win_ansi_glyph_name(0xA0), Some("space"));
1081        assert_eq!(win_ansi_glyph_name(0xAD), Some("hyphen"));
1082        for code in 0u16..=255 {
1083            let code = code as u8;
1084            assert_eq!(
1085                win_ansi_glyph_name(code).is_some(),
1086                win_ansi(code).is_some(),
1087                "code {code:#04x}: win_ansi_glyph_name/win_ansi domain mismatch"
1088            );
1089            let Some(name) = win_ansi_glyph_name(code) else {
1090                continue;
1091            };
1092            let expected = match code {
1093                0xA0 => ' ',
1094                0xAD => '-',
1095                _ => win_ansi(code).unwrap(),
1096            };
1097            assert_eq!(
1098                glyph_to_unicode(name),
1099                Some(expected),
1100                "code {code:#04x} name {name:?}: glyph_to_unicode disagrees with win_ansi"
1101            );
1102        }
1103    }
1104
1105    // Covers ISO 32000-1 Annex D.2.
1106    #[test]
1107    fn mac_roman_spot_checks() {
1108        assert_eq!(mac_roman(b'A'), Some('A'));
1109        assert_eq!(mac_roman(0xD0), Some('\u{2013}')); // en dash
1110        assert_eq!(mac_roman(0x80), Some('\u{C4}')); // A dieresis
1111        assert_eq!(mac_roman(0xA5), Some('\u{2022}')); // bullet
1112        assert_eq!(mac_roman(0xFF), Some('\u{2C7}')); // caron
1113        assert_eq!(mac_roman(0x00), None);
1114    }
1115
1116    // Covers ISO 32000-1 Annex D.2.
1117    #[test]
1118    fn standard_spot_checks() {
1119        assert_eq!(standard(b'A'), Some('A'));
1120        assert_eq!(standard(0xA9), Some('\u{27}')); // straight apostrophe
1121        assert_eq!(standard(0x27), Some('\u{2019}')); // curly right quote
1122        assert_eq!(standard(0x60), Some('\u{2018}')); // curly left quote
1123        assert_eq!(standard(0xD0), Some('\u{2014}')); // em dash
1124        assert_eq!(standard(0x7F), None);
1125        assert_eq!(standard(0xA0), None); // unassigned in Standard
1126    }
1127
1128    // Covers ISO 32000-1 §9.10.2.
1129    #[test]
1130    fn glyph_names_hex_forms() {
1131        assert_eq!(glyph_to_unicode("uni03B1"), Some('\u{3B1}'));
1132        assert_eq!(glyph_to_unicode("uni20AC"), Some('\u{20AC}'));
1133        assert_eq!(glyph_to_unicode("u1F600"), Some('\u{1F600}'));
1134        assert_eq!(glyph_to_unicode("u00E9"), Some('\u{E9}'));
1135        assert_eq!(glyph_to_unicode("uniD800"), None); // surrogate
1136        assert_eq!(glyph_to_unicode("uniXYZW"), None);
1137    }
1138
1139    /// Self-verifying anchor for `standard_encoding_name`: ties the new table
1140    /// to the pre-existing, trusted `standard` (code -> Unicode) and
1141    /// `glyph_to_unicode` (name -> Unicode) tables so an authoring typo in
1142    /// the new table fails a test instead of silently mis-encoding a glyph.
1143    /// Domain equality (StandardEncoding assigns a name to exactly the codes
1144    /// `standard` maps to a char) must hold for every code; value agreement
1145    /// only where `glyph_to_unicode` also resolves the name (some names
1146    /// aren't in the bundled glyph-name subset).
1147    // Covers ISO 32000-1 Annex D.2.
1148    // Covers ISO 32000-1 §9.6.6.4.
1149    #[test]
1150    fn mac_os_roman_codes_follow_table_115() {
1151        assert_eq!(mac_os_roman_code('A'), Some(0x41));
1152        assert_eq!(mac_os_roman_code('\u{E9}'), Some(0x8E), "eacute");
1153        assert_eq!(mac_os_roman_code('\u{2022}'), Some(0xA5), "bullet");
1154        // Table 115: the codes MacRomanEncoding leaves out.
1155        assert_eq!(mac_os_roman_code('\u{2260}'), Some(173), "notequal");
1156        assert_eq!(mac_os_roman_code('\u{221E}'), Some(176), "infinity");
1157        assert_eq!(mac_os_roman_code('\u{03C0}'), Some(185), "pi");
1158        assert_eq!(
1159            mac_os_roman_code('\u{2126}'),
1160            Some(189),
1161            "Omega, the glyph list's ohm"
1162        );
1163        assert_eq!(
1164            mac_os_roman_code('\u{2206}'),
1165            Some(198),
1166            "Delta, the glyph list's increment"
1167        );
1168        assert_eq!(mac_os_roman_code('\u{25CA}'), Some(215), "lozenge");
1169        assert_eq!(
1170            mac_os_roman_code('\u{20AC}'),
1171            Some(219),
1172            "Euro replaces currency"
1173        );
1174        assert_eq!(mac_os_roman_code('\u{A4}'), None, "currency is gone");
1175        assert_eq!(mac_os_roman_code('\u{F8FF}'), Some(240), "apple");
1176        assert_eq!(
1177            mac_os_roman_code('\u{0394}'),
1178            None,
1179            "Greek Delta is not the glyph list's Delta"
1180        );
1181        assert_eq!(mac_os_roman_code('\u{4E00}'), None);
1182    }
1183
1184    // Covers ISO 32000-1 Annex D.5.
1185    #[test]
1186    fn symbol_encoding_follows_annex_d5() {
1187        // Greek letters sit on the Latin codes, the Euro at 0o240, the
1188        // Hebrew aleph and fraktur letters in the 0o300 row, and the
1189        // bracket pieces resolve to the Corporate Use Subarea.
1190        assert_eq!(symbol_glyph_name(0o141), Some("alpha"));
1191        assert_eq!(symbol(0o141), Some('\u{03B1}'));
1192        // The glyph list maps Omega, Delta and mu to the ohm sign, the
1193        // increment sign and the micro sign, as pdf.js reads them too.
1194        assert_eq!(symbol(0o127), Some('\u{2126}'), "Omega");
1195        assert_eq!(symbol(0o104), Some('\u{2206}'), "Delta");
1196        assert_eq!(symbol(0o155), Some('\u{00B5}'), "mu");
1197        assert_eq!(symbol(0o240), Some('\u{20AC}'), "Euro");
1198        assert_eq!(symbol(0o300), Some('\u{2135}'), "aleph");
1199        assert_eq!(symbol(0o301), Some('\u{2111}'), "Ifraktur");
1200        assert_eq!(symbol(0o354), Some('\u{F8F1}'), "bracelefttp");
1201        assert_eq!(symbol(0o040), Some(' '));
1202        assert_eq!(symbol(0o060), Some('0'), "digits keep their ASCII places");
1203        // Unassigned: everything below space, 0o177 to 0o237, 0o377, and
1204        // 0o360, where Adobe's font keeps its apple but the annex lists
1205        // nothing.
1206        for code in [0o000, 0o037, 0o177, 0o200, 0o237, 0o360, 0o377] {
1207            assert_eq!(symbol_glyph_name(code), None, "{code:o}");
1208            assert_eq!(symbol(code), None, "{code:o}");
1209        }
1210        let assigned = (0..=255u8)
1211            .filter(|c| symbol_glyph_name(*c).is_some())
1212            .count();
1213        assert_eq!(assigned, 189, "Annex D.5 assigns 189 codes");
1214        for code in 0..=255u8 {
1215            assert_eq!(
1216                symbol(code).is_some(),
1217                symbol_glyph_name(code).is_some(),
1218                "every named code has a character: {code:o}"
1219            );
1220        }
1221    }
1222
1223    // Covers ISO 32000-1 Annex D.6.
1224    #[test]
1225    fn zapf_dingbats_encoding_follows_annex_d6() {
1226        assert_eq!(zapf_dingbats_glyph_name(0o041), Some("a1"));
1227        assert_eq!(zapf_dingbats(0o041), Some('\u{2701}'));
1228        assert_eq!(zapf_dingbats_glyph_name(0o043), Some("a202"));
1229        assert_eq!(zapf_dingbats(0o043), Some('\u{2703}'));
1230        assert_eq!(zapf_dingbats_glyph_name(0o315), Some("a153"));
1231        assert_eq!(zapf_dingbats(0o315), Some('\u{278D}'));
1232        assert_eq!(zapf_dingbats_glyph_name(0o376), Some("a191"));
1233        assert_eq!(zapf_dingbats(0o376), Some('\u{27BE}'));
1234        assert_eq!(zapf_dingbats(0o040), Some(' '));
1235        for code in [0o000, 0o177, 0o200, 0o240, 0o360, 0o377] {
1236            assert_eq!(zapf_dingbats_glyph_name(code), None, "{code:o}");
1237            assert_eq!(zapf_dingbats(code), None, "{code:o}");
1238        }
1239        // Adobe's font also fills 0o200 to 0o215; the annex leaves them out.
1240        assert_eq!(zapf_dingbats_glyph_name(0o200), None);
1241        let assigned = (0..=255u8)
1242            .filter(|c| zapf_dingbats_glyph_name(*c).is_some())
1243            .count();
1244        assert_eq!(assigned, 188, "Annex D.6 assigns space and 187 dingbats");
1245        for code in 0..=255u8 {
1246            assert_eq!(
1247                zapf_dingbats(code).is_some(),
1248                zapf_dingbats_glyph_name(code).is_some(),
1249                "{code:o}"
1250            );
1251        }
1252    }
1253
1254    // Covers ISO 32000-1 Annex D.2.
1255    #[test]
1256    fn standard_encoding_name_matches_standard_table() {
1257        for code in 0u16..=255 {
1258            let code = code as u8;
1259            assert_eq!(
1260                standard_encoding_name(code).is_some(),
1261                standard(code).is_some(),
1262                "code {code:#04x}: standard_encoding_name/standard domain mismatch"
1263            );
1264            if let (Some(name), Some(expected)) = (standard_encoding_name(code), standard(code)) {
1265                if let Some(resolved) = glyph_to_unicode(name) {
1266                    assert_eq!(
1267                        resolved, expected,
1268                        "code {code:#04x} name {name:?}: glyph_to_unicode disagrees with standard"
1269                    );
1270                }
1271            }
1272        }
1273    }
1274
1275    #[test]
1276    fn standard_encoding_name_spot_checks() {
1277        assert_eq!(standard_encoding_name(b'A'), Some("A"));
1278        assert_eq!(standard_encoding_name(0x27), Some("quoteright"));
1279        assert_eq!(standard_encoding_name(0x60), Some("quoteleft"));
1280        assert_eq!(standard_encoding_name(0xA1), Some("exclamdown"));
1281        assert_eq!(standard_encoding_name(0xA4), Some("fraction"));
1282        assert_eq!(standard_encoding_name(0xA6), Some("florin"));
1283        assert_eq!(standard_encoding_name(0xC1), Some("grave"));
1284        assert_eq!(standard_encoding_name(0xC6), Some("breve"));
1285        assert_eq!(standard_encoding_name(0xE1), Some("AE"));
1286        assert_eq!(standard_encoding_name(0xF1), Some("ae"));
1287        assert_eq!(standard_encoding_name(0xFB), Some("germandbls"));
1288        assert_eq!(standard_encoding_name(0x7F), None);
1289        assert_eq!(standard_encoding_name(0xA0), None);
1290    }
1291
1292    #[test]
1293    fn glyph_names_letters_and_tables() {
1294        assert_eq!(glyph_to_unicode("A"), Some('A'));
1295        assert_eq!(glyph_to_unicode("z"), Some('z'));
1296        assert_eq!(glyph_to_unicode("alpha"), Some('\u{3B1}'));
1297        assert_eq!(glyph_to_unicode("eacute"), Some('\u{E9}'));
1298        assert_eq!(glyph_to_unicode("quotedblleft"), Some('\u{201C}'));
1299        assert_eq!(glyph_to_unicode("seven"), Some('7'));
1300        assert_eq!(glyph_to_unicode("union"), Some('\u{222A}'));
1301        assert_eq!(glyph_to_unicode("nosuchglyphname"), None);
1302    }
1303
1304    #[test]
1305    fn glyph_text_ligatures_and_variants() {
1306        assert_eq!(glyph_to_text("f_i").as_deref(), Some("fi"));
1307        assert_eq!(glyph_to_text("f_l").as_deref(), Some("fl"));
1308        assert_eq!(glyph_to_text("T_h").as_deref(), Some("Th"));
1309        assert_eq!(glyph_to_text("f_f_i").as_deref(), Some("ffi"));
1310        assert_eq!(glyph_to_text("eight.oldstyle").as_deref(), Some("8"));
1311        assert_eq!(glyph_to_text("x.sc").as_deref(), Some("x"));
1312        assert_eq!(glyph_to_text("C.a").as_deref(), Some("C"));
1313        // Suffix stripping happens before underscore splitting.
1314        assert_eq!(glyph_to_text("f_i.alt").as_deref(), Some("fi"));
1315        assert_eq!(glyph_to_text("uni00A0").as_deref(), Some("\u{A0}"));
1316        assert_eq!(glyph_to_text("eacute").as_deref(), Some("\u{E9}"));
1317    }
1318
1319    #[test]
1320    fn glyph_text_multi_group_uni() {
1321        assert_eq!(
1322            glyph_to_text("uni20AC0308").as_deref(),
1323            Some("\u{20AC}\u{0308}")
1324        );
1325        assert_eq!(glyph_to_text("uniD800DC00"), None); // surrogates never decode
1326    }
1327
1328    #[test]
1329    fn glyph_text_rejects_unknowns() {
1330        assert_eq!(glyph_to_text(".notdef"), None);
1331        assert_eq!(glyph_to_text(""), None);
1332        assert_eq!(glyph_to_text("glorp"), None);
1333        // Every component must resolve, or the whole name is unknown.
1334        assert_eq!(glyph_to_text("f_glorp"), None);
1335        assert_eq!(glyph_to_text("f__i"), None);
1336    }
1337
1338    /// The math and symbol names a TeX-produced document's fonts carry in
1339    /// their built-in encodings are all in the Adobe Glyph List, as are the
1340    /// Hebrew names whose text is two scalars.
1341    // Covers ISO 32000-1 §14.8.2.4.2.
1342    #[test]
1343    fn glyph_names_cover_the_full_adobe_glyph_list() {
1344        assert_eq!(glyph_to_unicode("universal"), Some('\u{2200}'));
1345        assert_eq!(glyph_to_unicode("existential"), Some('\u{2203}'));
1346        assert_eq!(glyph_to_unicode("emptyset"), Some('\u{2205}'));
1347        assert_eq!(glyph_to_unicode("copyright"), Some('\u{A9}'));
1348        assert_eq!(glyph_to_unicode("ff"), Some('\u{FB00}'));
1349        assert_eq!(glyph_to_unicode("afii57414"), Some('\u{0626}'));
1350        assert_eq!(
1351            glyph_to_text("dalethatafpatah").as_deref(),
1352            Some("\u{05D3}\u{05B2}")
1353        );
1354        assert_eq!(glyph_to_unicode("dalethatafpatah"), None);
1355    }
1356
1357    /// Names the Computer Modern symbol fonts use that the Adobe Glyph
1358    /// List never adopted.
1359    #[test]
1360    fn tex_math_glyph_names_resolve() {
1361        assert_eq!(glyph_to_unicode("owner"), Some('\u{220B}'));
1362        assert_eq!(glyph_to_unicode("arrowbothv"), Some('\u{2195}'));
1363        assert_eq!(glyph_to_unicode("angbracketleft"), Some('\u{27E8}'));
1364        assert_eq!(glyph_to_unicode("lessmuch"), Some('\u{226A}'));
1365        assert_eq!(glyph_to_unicode("prime"), Some('\u{2032}'));
1366        assert_eq!(glyph_to_unicode("negationslash"), Some('\u{0338}'));
1367        assert_eq!(glyph_to_unicode("lessorsimilar"), Some('\u{2272}'));
1368        assert_eq!(glyph_to_unicode("notexistential"), Some('\u{2204}'));
1369        assert_eq!(glyph_to_unicode("vextendsingle"), Some('|'));
1370        assert_eq!(glyph_to_unicode("Circle"), Some('\u{25CB}'));
1371    }
1372
1373    // Covers ISO 32000-1 §9.6.6.2.
1374    #[test]
1375    fn type1_program_encoding_reads_dup_put_entries() {
1376        let program: &[u8] = b"%!PS-AdobeFont-1.0: CMSY10\n/FontName /CMSY10 def\n\
1377            /Encoding 256 array\n0 1 255 {1 index exch /.notdef put} for\n\
1378            dup 56 /universal put\ndup 169 /copyright put\nreadonly def\n\
1379            currentdict end\ncurrentfile eexec\n\x80\x01dup 57 /existential put";
1380        let table = type1_builtin_encoding(program).expect("an /Encoding is present");
1381        assert_eq!(table[56].as_deref(), Some("universal"));
1382        assert_eq!(table[169].as_deref(), Some("copyright"));
1383        assert_eq!(table[57], None, "nothing past eexec is read");
1384        assert_eq!(table[0], None);
1385    }
1386
1387    #[test]
1388    fn type1_program_encoding_expands_the_standard_token() {
1389        let program: &[u8] = b"/Encoding StandardEncoding def\ncurrentfile eexec\n";
1390        let table = type1_builtin_encoding(program).unwrap();
1391        assert_eq!(table[0x41].as_deref(), Some("A"));
1392        assert_eq!(table[0x27].as_deref(), Some("quoteright"));
1393    }
1394
1395    #[test]
1396    fn type1_program_encoding_reads_pfb_segments() {
1397        let clear: &[u8] =
1398            b"/Encoding 256 array\ndup 65 /alpha put\nreadonly def\ncurrentfile eexec\n";
1399        let mut program = vec![0x80, 0x01];
1400        program.extend_from_slice(&(clear.len() as u32).to_le_bytes());
1401        program.extend_from_slice(clear);
1402        program.extend_from_slice(&[0x80, 0x02, 4, 0, 0, 0, 0xDE, 0xAD, 0xBE, 0xEF, 0x80, 0x03]);
1403        let table = type1_builtin_encoding(&program).unwrap();
1404        assert_eq!(table[65].as_deref(), Some("alpha"));
1405    }
1406
1407    #[test]
1408    fn type1_program_without_an_encoding_yields_none() {
1409        let program: &[u8] = b"/FontName /X def\ncurrentfile eexec\n";
1410        assert!(type1_builtin_encoding(program).is_none());
1411    }
1412}