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