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