Skip to main content

oxideav_pdf/reader/
encoding.rs

1//! PDF simple-font encoding resolver — `/Encoding` dictionary +
2//! `/Differences` array → 256-entry byte → Unicode map.
3//!
4//! ISO 32000-1:2008 §9.6.6.1 "Type 1 Encodings" defines a simple
5//! font's `/Encoding` as either a single name
6//! (`WinAnsiEncoding` / `MacRomanEncoding` / `MacExpertEncoding` /
7//! `StandardEncoding`) or a dictionary with a `/BaseEncoding` name
8//! and an optional `/Differences` array. The array is a flat sequence
9//! of *(code, glyph-name, glyph-name, …, code, glyph-name, …)*: every
10//! numeric starts a new run of code points, and every following name
11//! token is the glyph at the next consecutive code. Each glyph name is
12//! mapped to a Unicode scalar value via the Adobe Glyph List
13//! (`docs/document/pdf/agl/subset.txt`).
14//!
15//! Round 28 wires this resolver into the text-extraction path so a
16//! simple font whose `/Encoding` carries `/Differences` decodes to the
17//! correct Unicode payload (matching what `pdftotext` produces).
18//!
19//! ## Provenance
20//!
21//! ISO 32000-1:2008 §9.6.6.1 (Type 1 Encodings) + §D.2 (Latin character
22//! set) for the encoding tables; Adobe Glyph List v2.0 (public document,
23//! 5 Sep 2002) for the glyph-name → Unicode mapping. No third-party PDF
24//! library SOURCE was consulted.
25
26use crate::error::PdfError;
27use crate::objects::Object;
28use std::borrow::Cow;
29
30/// One `/Differences` array override: at code point `code`, the
31/// rendering glyph is `glyph_name`.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct EncodingOverride {
34    pub code: u8,
35    pub glyph_name: String,
36}
37
38/// Parsed `/Differences` array — flat list of `(code, name)` overrides.
39///
40/// Internally just a `Vec`; the resolver applies them in order on top of
41/// a base 256-entry table. Iterates in document order so a later entry
42/// for the same code wins (matching what Acrobat / Distiller does when
43/// a malformed PDF lists the same code twice).
44#[derive(Clone, Debug, Default, PartialEq, Eq)]
45pub struct EncodingDifferences {
46    pub overrides: Vec<EncodingOverride>,
47}
48
49impl EncodingDifferences {
50    pub fn is_empty(&self) -> bool {
51        self.overrides.is_empty()
52    }
53
54    pub fn len(&self) -> usize {
55        self.overrides.len()
56    }
57}
58
59/// The named base encodings ISO 32000-1 §9.6.6.1 + §D.2 define for
60/// simple Type 1 / TrueType fonts.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum BaseEncoding {
63    WinAnsi,
64    MacRoman,
65    MacExpert,
66    /// ISO 32000-1 §D.2 "Standard" — the Adobe Type 1 standard encoding
67    /// (also the implicit default when a Type 1 font omits `/Encoding`).
68    Standard,
69    /// Symbol encoding — non-Latin glyph repertoire (Greek + math). Used
70    /// by the built-in Symbol font.
71    Symbol,
72    /// ZapfDingbats encoding — the built-in dingbat font's repertoire.
73    ZapfDingbats,
74}
75
76impl BaseEncoding {
77    pub fn from_name(name: &str) -> Option<BaseEncoding> {
78        Some(match name {
79            "WinAnsiEncoding" => BaseEncoding::WinAnsi,
80            "MacRomanEncoding" => BaseEncoding::MacRoman,
81            "MacExpertEncoding" => BaseEncoding::MacExpert,
82            "StandardEncoding" => BaseEncoding::Standard,
83            "SymbolEncoding" => BaseEncoding::Symbol,
84            "ZapfDingbatsEncoding" => BaseEncoding::ZapfDingbats,
85            _ => return None,
86        })
87    }
88}
89
90/// 256-entry byte → Unicode (UTF-8 string) map. Most entries hold a
91/// single `char` but the AGL also defines ligature glyphs whose
92/// expansion is multi-character (`/fi` → "fi"), so the slot has to
93/// accommodate a short `String`. Slots for unassigned bytes hold the
94/// empty string — the decoder emits U+FFFD when it sees one.
95#[derive(Clone, Debug)]
96pub struct EncodingMap {
97    table: [String; 256],
98}
99
100impl Default for EncodingMap {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl EncodingMap {
107    pub fn new() -> Self {
108        Self {
109            table: std::array::from_fn(|_| String::new()),
110        }
111    }
112
113    pub fn set_char(&mut self, code: u8, c: char) {
114        self.table[code as usize] = String::from(c);
115    }
116
117    pub fn set_string(&mut self, code: u8, s: &str) {
118        self.table[code as usize] = s.to_owned();
119    }
120
121    /// Look up one byte. Returns the empty slice when unassigned (caller
122    /// substitutes U+FFFD).
123    pub fn lookup(&self, code: u8) -> &str {
124        &self.table[code as usize]
125    }
126
127    /// Decode a `Tj` / `TJ` byte string into Unicode by walking the
128    /// table. Unassigned codes become U+FFFD.
129    pub fn decode(&self, bytes: &[u8]) -> String {
130        let mut out = String::with_capacity(bytes.len());
131        for &b in bytes {
132            let slot = self.lookup(b);
133            if slot.is_empty() {
134                out.push('\u{FFFD}');
135            } else {
136                out.push_str(slot);
137            }
138        }
139        out
140    }
141
142    /// Build a map from a named [`BaseEncoding`].
143    pub fn from_base(base: BaseEncoding) -> EncodingMap {
144        let mut m = EncodingMap::new();
145        match base {
146            BaseEncoding::WinAnsi => fill_winansi(&mut m),
147            BaseEncoding::MacRoman => fill_macroman(&mut m),
148            BaseEncoding::MacExpert => fill_macexpert(&mut m),
149            BaseEncoding::Standard => fill_standard(&mut m),
150            BaseEncoding::Symbol => fill_symbol(&mut m),
151            BaseEncoding::ZapfDingbats => fill_zapfdingbats(&mut m),
152        }
153        m
154    }
155}
156
157// ────────────────────────── /Differences parser ──────────────────────────
158
159/// Parse a `/Differences` array — flat `[N name1 name2 … M nameK …]`.
160///
161/// Numeric tokens reset the running code; each successive name maps to
162/// the next consecutive code (running code is post-incremented). Tokens
163/// that are neither a number nor a `Name` are skipped silently — older
164/// `acroread` writers occasionally embed `null` or a comment-like
165/// reference in there and the spec mandates we tolerate that.
166///
167/// Returns `Ok(EncodingDifferences::default())` for an empty array;
168/// returns `Err` only when the supplied `Object` isn't an `Array` (the
169/// caller must have already pulled the array out of the encoding
170/// dictionary's `/Differences` slot).
171pub fn parse_encoding_differences(arr: &Object) -> Result<EncodingDifferences, PdfError> {
172    let items = match arr {
173        Object::Array(v) => v,
174        _ => {
175            return Err(PdfError::other(format!(
176                "PDF encoding: /Differences must be an array (got {arr:?})"
177            )));
178        }
179    };
180    let mut out = EncodingDifferences::default();
181    let mut running: Option<u32> = None;
182    for item in items {
183        match item {
184            Object::Integer(n) if (0..=255).contains(n) => {
185                running = Some(*n as u32);
186            }
187            Object::Real(r) => {
188                let v = *r as i64;
189                if (0..=255).contains(&v) {
190                    running = Some(v as u32);
191                }
192            }
193            Object::Name(name) => {
194                if let Some(code) = running {
195                    if code <= 255 {
196                        out.overrides.push(EncodingOverride {
197                            code: code as u8,
198                            glyph_name: name.clone(),
199                        });
200                    }
201                    running = Some(code + 1);
202                }
203            }
204            _ => {
205                // Unknown token — skip. Per the spec we just keep
206                // walking, so a malformed entry doesn't poison the
207                // rest of the array.
208            }
209        }
210    }
211    Ok(out)
212}
213
214/// Overlay `differences` on top of `base`, returning a fresh map. The
215/// base map is left untouched (cheap clone — `String` allocations are
216/// per-entry).
217pub fn apply_encoding_differences(
218    base: &EncodingMap,
219    differences: &EncodingDifferences,
220) -> EncodingMap {
221    let mut out = base.clone();
222    for ov in &differences.overrides {
223        if let Some(s) = glyph_name_to_unicode(&ov.glyph_name) {
224            out.set_string(ov.code, s.as_ref());
225        } else {
226            // Unknown glyph — leave the slot empty so the decoder emits
227            // U+FFFD. We do NOT propagate the raw glyph name as text —
228            // that would be worse than the replacement char (callers
229            // running keyword search would match the literal name).
230            out.set_string(ov.code, "");
231        }
232    }
233    out
234}
235
236// ────────────────────────── glyph-name → Unicode ──────────────────────────
237
238/// Adobe Glyph List lookup. Returns the UTF-8 expansion of a PostScript
239/// glyph name (multi-char for ligatures like `/fi`, single-char for the
240/// common case). Returns `None` for unknown names — the caller emits
241/// U+FFFD as a marker.
242///
243/// The table here is a transcription of the AGL subset under
244/// `docs/document/pdf/agl/subset.txt`. The PDF spec (§D.2 + §9.6.6.1)
245/// defines a fixed Latin repertoire that the four named encodings draw
246/// from; that's what we ship. Extending the table for non-Latin glyphs
247/// (CJK, Cyrillic, Devanagari) is a future-round followup.
248///
249/// In addition to the static AGL subset, this resolver honours the
250/// Adobe Glyph List Public Implementation Notes §3 `uniXXXX...` /
251/// `uXXXXXXXX` Unicode-by-name escape forms (round 175). Producers
252/// occasionally emit these escapes directly in a `/Differences` array
253/// rather than the AGL-aliased name (e.g. `/uni201C` instead of
254/// `/quotedblleft`), and the spec mandates we honour them.
255pub fn glyph_name_to_unicode(name: &str) -> Option<Cow<'static, str>> {
256    // Special PDF-spec aliases — `.notdef` is rendered as nothing, the
257    // `uniXXXX` / `uXXXXXXXX` forms are Unicode-by-name escapes that the
258    // AGL Public Implementation Notes (§3) mandates we honour first.
259    if name == ".notdef" || name.is_empty() {
260        return Some(Cow::Borrowed(""));
261    }
262    // Linear scan of the AGL subset first — the AGL alias is preferred
263    // when both forms resolve (a producer that emits `/A` and the
264    // AGL-aliased `/uni0041` should reach the same result, but the
265    // static-table hit avoids an allocation). The table is small enough
266    // that a hash map's setup cost outweighs the savings for the typical
267    // `/Differences` array (≤ 32 entries).
268    for (n, s) in AGL_SUBSET {
269        if *n == name {
270            return Some(Cow::Borrowed(*s));
271        }
272    }
273    // Fall through to the `uniXXXX` / `uXXXXXXXX` decoder.
274    uni_prefix_decode(name).map(Cow::Owned)
275}
276
277/// Adobe Glyph List Public Implementation Notes §3 — `uniXXXX...`
278/// (one or more consecutive 4-hex-digit BMP code points) and
279/// `uXXXXXXXX` (a single 4-to-6-hex-digit code point, including
280/// supplementary planes).
281///
282/// The two forms differ in their hex-digit count discipline:
283///
284/// 1. **`uni` prefix** — the remainder is split into consecutive
285///    4-character groups. Each group is a BMP code point (one of
286///    `U+0000..=U+D7FF` or `U+E000..=U+FFFD`). Surrogate halves
287///    (`U+D800..=U+DFFF`) are rejected. The decoded characters are
288///    concatenated (this is how a producer encodes a multi-character
289///    ligature without an AGL alias).
290/// 2. **`u` prefix** — the remainder is exactly 4, 5, or 6
291///    uppercase hex digits, denoting one Unicode scalar value. The
292///    value must be a valid `char` (`<= 0x10FFFF`, not a surrogate)
293///    and must not be `0xFFFF` (the "shall not be a noncharacter"
294///    rule from the AGL Public Implementation Notes).
295///
296/// Returns `None` for any name that doesn't match either shape
297/// strictly — the caller falls back to its unknown-glyph branch.
298fn uni_prefix_decode(name: &str) -> Option<String> {
299    if let Some(rest) = name.strip_prefix("uni") {
300        // BMP-group form. The trailing characters must split cleanly
301        // into 4-digit groups.
302        if rest.is_empty() || rest.len() % 4 != 0 {
303            return None;
304        }
305        let mut out = String::with_capacity(rest.len() / 4);
306        for chunk in rest.as_bytes().chunks(4) {
307            // SAFETY: chunks of 4 ASCII bytes are always valid UTF-8.
308            let hex = std::str::from_utf8(chunk).ok()?;
309            // AGL PIN §3 mandates uppercase ASCII hex. Reject
310            // lowercase / mixed-case to keep the canonical form clean
311            // (some producers write lowercase; treat them as unknown).
312            if !is_uppercase_hex(hex) {
313                return None;
314            }
315            let cp = u32::from_str_radix(hex, 16).ok()?;
316            // Surrogate halves and noncharacter U+FFFF rejected.
317            if (0xD800..=0xDFFF).contains(&cp) || cp == 0xFFFF {
318                return None;
319            }
320            let c = char::from_u32(cp)?;
321            out.push(c);
322        }
323        Some(out)
324    } else if let Some(rest) = name.strip_prefix('u') {
325        // Single-codepoint form, 4..=6 hex digits, supplementary
326        // planes allowed.
327        if !(4..=6).contains(&rest.len()) {
328            return None;
329        }
330        if !is_uppercase_hex(rest) {
331            return None;
332        }
333        let cp = u32::from_str_radix(rest, 16).ok()?;
334        if (0xD800..=0xDFFF).contains(&cp) || cp == 0xFFFF {
335            return None;
336        }
337        let c = char::from_u32(cp)?;
338        Some(c.to_string())
339    } else {
340        None
341    }
342}
343
344/// Returns true iff every byte in `s` is `0..=9` / `A..=F`. AGL PIN
345/// §3 specifies uppercase; we treat lowercase as a non-match so a
346/// `/u00ff` doesn't collide with the canonical `/u00FF`.
347fn is_uppercase_hex(s: &str) -> bool {
348    !s.is_empty()
349        && s.bytes()
350            .all(|b| b.is_ascii_digit() || (b'A'..=b'F').contains(&b))
351}
352
353/// The shipping AGL subset. Held as an array literal so the binary
354/// embeds it verbatim; a build script that generates this from
355/// `subset.txt` would be churn for negligible win.
356const AGL_SUBSET: &[(&str, &str)] = &[
357    // ── Basic Latin (§D.2 names that match the AGL) ──
358    ("space", " "),
359    ("exclam", "!"),
360    ("quotedbl", "\""),
361    ("numbersign", "#"),
362    ("dollar", "$"),
363    ("percent", "%"),
364    ("ampersand", "&"),
365    ("quoteright", "\u{2019}"),
366    ("quotesingle", "'"),
367    ("parenleft", "("),
368    ("parenright", ")"),
369    ("asterisk", "*"),
370    ("plus", "+"),
371    ("comma", ","),
372    ("hyphen", "-"),
373    ("period", "."),
374    ("slash", "/"),
375    ("zero", "0"),
376    ("one", "1"),
377    ("two", "2"),
378    ("three", "3"),
379    ("four", "4"),
380    ("five", "5"),
381    ("six", "6"),
382    ("seven", "7"),
383    ("eight", "8"),
384    ("nine", "9"),
385    ("colon", ":"),
386    ("semicolon", ";"),
387    ("less", "<"),
388    ("equal", "="),
389    ("greater", ">"),
390    ("question", "?"),
391    ("at", "@"),
392    ("A", "A"),
393    ("B", "B"),
394    ("C", "C"),
395    ("D", "D"),
396    ("E", "E"),
397    ("F", "F"),
398    ("G", "G"),
399    ("H", "H"),
400    ("I", "I"),
401    ("J", "J"),
402    ("K", "K"),
403    ("L", "L"),
404    ("M", "M"),
405    ("N", "N"),
406    ("O", "O"),
407    ("P", "P"),
408    ("Q", "Q"),
409    ("R", "R"),
410    ("S", "S"),
411    ("T", "T"),
412    ("U", "U"),
413    ("V", "V"),
414    ("W", "W"),
415    ("X", "X"),
416    ("Y", "Y"),
417    ("Z", "Z"),
418    ("bracketleft", "["),
419    ("backslash", "\\"),
420    ("bracketright", "]"),
421    ("asciicircum", "^"),
422    ("underscore", "_"),
423    ("quoteleft", "\u{2018}"),
424    ("grave", "`"),
425    ("a", "a"),
426    ("b", "b"),
427    ("c", "c"),
428    ("d", "d"),
429    ("e", "e"),
430    ("f", "f"),
431    ("g", "g"),
432    ("h", "h"),
433    ("i", "i"),
434    ("j", "j"),
435    ("k", "k"),
436    ("l", "l"),
437    ("m", "m"),
438    ("n", "n"),
439    ("o", "o"),
440    ("p", "p"),
441    ("q", "q"),
442    ("r", "r"),
443    ("s", "s"),
444    ("t", "t"),
445    ("u", "u"),
446    ("v", "v"),
447    ("w", "w"),
448    ("x", "x"),
449    ("y", "y"),
450    ("z", "z"),
451    ("braceleft", "{"),
452    ("bar", "|"),
453    ("braceright", "}"),
454    ("asciitilde", "~"),
455    // ── Latin-1 supplement (§D.2 Standard + WinAnsi shared names) ──
456    ("exclamdown", "\u{00A1}"),
457    ("cent", "\u{00A2}"),
458    ("sterling", "\u{00A3}"),
459    ("currency", "\u{00A4}"),
460    ("yen", "\u{00A5}"),
461    ("brokenbar", "\u{00A6}"),
462    ("section", "\u{00A7}"),
463    ("dieresis", "\u{00A8}"),
464    ("copyright", "\u{00A9}"),
465    ("ordfeminine", "\u{00AA}"),
466    ("guillemotleft", "\u{00AB}"),
467    ("logicalnot", "\u{00AC}"),
468    ("hyphensoft", "\u{00AD}"),
469    ("registered", "\u{00AE}"),
470    ("macron", "\u{00AF}"),
471    ("degree", "\u{00B0}"),
472    ("plusminus", "\u{00B1}"),
473    ("twosuperior", "\u{00B2}"),
474    ("threesuperior", "\u{00B3}"),
475    ("acute", "\u{00B4}"),
476    ("mu", "\u{00B5}"),
477    ("paragraph", "\u{00B6}"),
478    ("periodcentered", "\u{00B7}"),
479    ("cedilla", "\u{00B8}"),
480    ("onesuperior", "\u{00B9}"),
481    ("ordmasculine", "\u{00BA}"),
482    ("guillemotright", "\u{00BB}"),
483    ("onequarter", "\u{00BC}"),
484    ("onehalf", "\u{00BD}"),
485    ("threequarters", "\u{00BE}"),
486    ("questiondown", "\u{00BF}"),
487    ("Agrave", "\u{00C0}"),
488    ("Aacute", "\u{00C1}"),
489    ("Acircumflex", "\u{00C2}"),
490    ("Atilde", "\u{00C3}"),
491    ("Adieresis", "\u{00C4}"),
492    ("Aring", "\u{00C5}"),
493    ("AE", "\u{00C6}"),
494    ("Ccedilla", "\u{00C7}"),
495    ("Egrave", "\u{00C8}"),
496    ("Eacute", "\u{00C9}"),
497    ("Ecircumflex", "\u{00CA}"),
498    ("Edieresis", "\u{00CB}"),
499    ("Igrave", "\u{00CC}"),
500    ("Iacute", "\u{00CD}"),
501    ("Icircumflex", "\u{00CE}"),
502    ("Idieresis", "\u{00CF}"),
503    ("Eth", "\u{00D0}"),
504    ("Ntilde", "\u{00D1}"),
505    ("Ograve", "\u{00D2}"),
506    ("Oacute", "\u{00D3}"),
507    ("Ocircumflex", "\u{00D4}"),
508    ("Otilde", "\u{00D5}"),
509    ("Odieresis", "\u{00D6}"),
510    ("multiply", "\u{00D7}"),
511    ("Oslash", "\u{00D8}"),
512    ("Ugrave", "\u{00D9}"),
513    ("Uacute", "\u{00DA}"),
514    ("Ucircumflex", "\u{00DB}"),
515    ("Udieresis", "\u{00DC}"),
516    ("Yacute", "\u{00DD}"),
517    ("Thorn", "\u{00DE}"),
518    ("germandbls", "\u{00DF}"),
519    ("agrave", "\u{00E0}"),
520    ("aacute", "\u{00E1}"),
521    ("acircumflex", "\u{00E2}"),
522    ("atilde", "\u{00E3}"),
523    ("adieresis", "\u{00E4}"),
524    ("aring", "\u{00E5}"),
525    ("ae", "\u{00E6}"),
526    ("ccedilla", "\u{00E7}"),
527    ("egrave", "\u{00E8}"),
528    ("eacute", "\u{00E9}"),
529    ("ecircumflex", "\u{00EA}"),
530    ("edieresis", "\u{00EB}"),
531    ("igrave", "\u{00EC}"),
532    ("iacute", "\u{00ED}"),
533    ("icircumflex", "\u{00EE}"),
534    ("idieresis", "\u{00EF}"),
535    ("eth", "\u{00F0}"),
536    ("ntilde", "\u{00F1}"),
537    ("ograve", "\u{00F2}"),
538    ("oacute", "\u{00F3}"),
539    ("ocircumflex", "\u{00F4}"),
540    ("otilde", "\u{00F5}"),
541    ("odieresis", "\u{00F6}"),
542    ("divide", "\u{00F7}"),
543    ("oslash", "\u{00F8}"),
544    ("ugrave", "\u{00F9}"),
545    ("uacute", "\u{00FA}"),
546    ("ucircumflex", "\u{00FB}"),
547    ("udieresis", "\u{00FC}"),
548    ("yacute", "\u{00FD}"),
549    ("thorn", "\u{00FE}"),
550    ("ydieresis", "\u{00FF}"),
551    // ── European Latin extensions ──
552    ("Lslash", "\u{0141}"),
553    ("lslash", "\u{0142}"),
554    ("Scaron", "\u{0160}"),
555    ("scaron", "\u{0161}"),
556    ("OE", "\u{0152}"),
557    ("oe", "\u{0153}"),
558    ("Ydieresis", "\u{0178}"),
559    ("Zcaron", "\u{017D}"),
560    ("zcaron", "\u{017E}"),
561    ("florin", "\u{0192}"),
562    ("circumflex", "\u{02C6}"),
563    ("tilde", "\u{02DC}"),
564    ("caron", "\u{02C7}"),
565    ("breve", "\u{02D8}"),
566    ("dotaccent", "\u{02D9}"),
567    ("ring", "\u{02DA}"),
568    ("ogonek", "\u{02DB}"),
569    ("hungarumlaut", "\u{02DD}"),
570    // ── Greek (math text) ──
571    ("Alpha", "\u{0391}"),
572    ("Beta", "\u{0392}"),
573    ("Gamma", "\u{0393}"),
574    ("Delta", "\u{0394}"),
575    ("Epsilon", "\u{0395}"),
576    ("Zeta", "\u{0396}"),
577    ("Eta", "\u{0397}"),
578    ("Theta", "\u{0398}"),
579    ("Iota", "\u{0399}"),
580    ("Kappa", "\u{039A}"),
581    ("Lambda", "\u{039B}"),
582    ("Mu", "\u{039C}"),
583    ("Nu", "\u{039D}"),
584    ("Xi", "\u{039E}"),
585    ("Omicron", "\u{039F}"),
586    ("Pi", "\u{03A0}"),
587    ("Rho", "\u{03A1}"),
588    ("Sigma", "\u{03A3}"),
589    ("Tau", "\u{03A4}"),
590    ("Upsilon", "\u{03A5}"),
591    ("Phi", "\u{03A6}"),
592    ("Chi", "\u{03A7}"),
593    ("Psi", "\u{03A8}"),
594    ("Omega", "\u{03A9}"),
595    ("alpha", "\u{03B1}"),
596    ("beta", "\u{03B2}"),
597    ("gamma", "\u{03B3}"),
598    ("delta", "\u{03B4}"),
599    ("epsilon", "\u{03B5}"),
600    ("zeta", "\u{03B6}"),
601    ("eta", "\u{03B7}"),
602    ("theta", "\u{03B8}"),
603    ("iota", "\u{03B9}"),
604    ("kappa", "\u{03BA}"),
605    ("lambda", "\u{03BB}"),
606    ("nu", "\u{03BD}"),
607    ("xi", "\u{03BE}"),
608    ("omicron", "\u{03BF}"),
609    ("pi", "\u{03C0}"),
610    ("rho", "\u{03C1}"),
611    ("sigma", "\u{03C3}"),
612    ("tau", "\u{03C4}"),
613    ("upsilon", "\u{03C5}"),
614    ("phi", "\u{03C6}"),
615    ("chi", "\u{03C7}"),
616    ("psi", "\u{03C8}"),
617    ("omega", "\u{03C9}"),
618    // ── Punctuation / dashes / quotes ──
619    ("endash", "\u{2013}"),
620    ("emdash", "\u{2014}"),
621    ("quotesinglbase", "\u{201A}"),
622    ("quotedblbase", "\u{201E}"),
623    ("quotedblleft", "\u{201C}"),
624    ("quotedblright", "\u{201D}"),
625    ("dagger", "\u{2020}"),
626    ("daggerdbl", "\u{2021}"),
627    ("bullet", "\u{2022}"),
628    ("ellipsis", "\u{2026}"),
629    ("perthousand", "\u{2030}"),
630    ("guilsinglleft", "\u{2039}"),
631    ("guilsinglright", "\u{203A}"),
632    ("Euro", "\u{20AC}"),
633    ("trademark", "\u{2122}"),
634    // ── Fractions ──
635    ("onethird", "\u{2153}"),
636    ("twothirds", "\u{2154}"),
637    ("oneeighth", "\u{215B}"),
638    ("threeeighths", "\u{215C}"),
639    ("fiveeighths", "\u{215D}"),
640    ("seveneighths", "\u{215E}"),
641    // ── Math + arrows ──
642    ("minus", "\u{2212}"),
643    ("fraction", "\u{2044}"),
644    ("infinity", "\u{221E}"),
645    ("notequal", "\u{2260}"),
646    ("lessequal", "\u{2264}"),
647    ("greaterequal", "\u{2265}"),
648    ("arrowleft", "\u{2190}"),
649    ("arrowup", "\u{2191}"),
650    ("arrowright", "\u{2192}"),
651    ("arrowdown", "\u{2193}"),
652    ("arrowboth", "\u{2194}"),
653    ("arrowdblleft", "\u{21D0}"),
654    ("arrowdblup", "\u{21D1}"),
655    ("arrowdblright", "\u{21D2}"),
656    ("arrowdbldown", "\u{21D3}"),
657    ("arrowdblboth", "\u{21D4}"),
658    ("partialdiff", "\u{2202}"),
659    ("gradient", "\u{2207}"),
660    ("product", "\u{220F}"),
661    ("summation", "\u{2211}"),
662    ("radical", "\u{221A}"),
663    ("proportional", "\u{221D}"),
664    ("integral", "\u{222B}"),
665    ("approxequal", "\u{2248}"),
666    ("equivalence", "\u{2261}"),
667    ("lozenge", "\u{25CA}"),
668    ("notelement", "\u{2209}"),
669    ("element", "\u{2208}"),
670    ("emptyset", "\u{2205}"),
671    ("intersection", "\u{2229}"),
672    ("union", "\u{222A}"),
673    ("logicalor", "\u{2228}"),
674    ("logicaland", "\u{2227}"),
675    ("universal", "\u{2200}"),
676    ("existential", "\u{2203}"),
677    // ── Ligatures ──
678    ("fi", "fi"),
679    ("fl", "fl"),
680    // ── Bullets / shapes ──
681    ("filledbox", "\u{25A0}"),
682    ("emptybox", "\u{25A1}"),
683    ("filledrect", "\u{25AC}"),
684    ("triagup", "\u{25B2}"),
685    ("triagrt", "\u{25BA}"),
686    ("triagdn", "\u{25BC}"),
687    ("triaglf", "\u{25C4}"),
688    ("circle", "\u{25CB}"),
689    ("filledcircle", "\u{25CF}"),
690    ("heart", "\u{2665}"),
691    ("musicalnote", "\u{266A}"),
692    ("musicalnotedbl", "\u{266B}"),
693];
694
695// ────────────────────────── base-encoding tables ──────────────────────────
696
697/// WinAnsiEncoding (CP1252) — ISO 32000-1 Annex D.2 Table D.2 "Latin
698/// Character Set". The 32 control bytes are unassigned; we leave them
699/// empty so the decoder emits U+FFFD when it sees one. Bytes 0x20..0x7E
700/// are plain ASCII; 0x80..0x9F are the Microsoft Windows code-page-1252
701/// punctuation overlay; 0xA0..0xFF round-trip 1:1 to Latin-1 with a few
702/// CP1252-specific swaps (the spec table is canonical).
703fn fill_winansi(m: &mut EncodingMap) {
704    // ASCII printable.
705    for b in 0x20u8..=0x7E {
706        m.set_char(b, b as char);
707    }
708    // CP1252 punctuation overlay (0x80..0x9F).
709    let overlay: &[(u8, char)] = &[
710        (0x80, '\u{20AC}'),
711        (0x82, '\u{201A}'),
712        (0x83, '\u{0192}'),
713        (0x84, '\u{201E}'),
714        (0x85, '\u{2026}'),
715        (0x86, '\u{2020}'),
716        (0x87, '\u{2021}'),
717        (0x88, '\u{02C6}'),
718        (0x89, '\u{2030}'),
719        (0x8A, '\u{0160}'),
720        (0x8B, '\u{2039}'),
721        (0x8C, '\u{0152}'),
722        (0x8E, '\u{017D}'),
723        (0x91, '\u{2018}'),
724        (0x92, '\u{2019}'),
725        (0x93, '\u{201C}'),
726        (0x94, '\u{201D}'),
727        (0x95, '\u{2022}'),
728        (0x96, '\u{2013}'),
729        (0x97, '\u{2014}'),
730        (0x98, '\u{02DC}'),
731        (0x99, '\u{2122}'),
732        (0x9A, '\u{0161}'),
733        (0x9B, '\u{203A}'),
734        (0x9C, '\u{0153}'),
735        (0x9E, '\u{017E}'),
736        (0x9F, '\u{0178}'),
737    ];
738    for (b, c) in overlay {
739        m.set_char(*b, *c);
740    }
741    // Latin-1 supplement (0xA0..0xFF).
742    for b in 0xA0u8..=0xFF {
743        m.set_char(b, b as char);
744    }
745}
746
747/// MacRomanEncoding — ISO 32000-1 Annex D.2 Table D.2 (the "Mac" column).
748fn fill_macroman(m: &mut EncodingMap) {
749    // ASCII printable.
750    for b in 0x20u8..=0x7E {
751        m.set_char(b, b as char);
752    }
753    // Bytes 0x80..0xFF — the canonical Mac Roman table.
754    let table: &[(u8, char)] = &[
755        (0x80, '\u{00C4}'),
756        (0x81, '\u{00C5}'),
757        (0x82, '\u{00C7}'),
758        (0x83, '\u{00C9}'),
759        (0x84, '\u{00D1}'),
760        (0x85, '\u{00D6}'),
761        (0x86, '\u{00DC}'),
762        (0x87, '\u{00E1}'),
763        (0x88, '\u{00E0}'),
764        (0x89, '\u{00E2}'),
765        (0x8A, '\u{00E4}'),
766        (0x8B, '\u{00E3}'),
767        (0x8C, '\u{00E5}'),
768        (0x8D, '\u{00E7}'),
769        (0x8E, '\u{00E9}'),
770        (0x8F, '\u{00E8}'),
771        (0x90, '\u{00EA}'),
772        (0x91, '\u{00EB}'),
773        (0x92, '\u{00ED}'),
774        (0x93, '\u{00EC}'),
775        (0x94, '\u{00EE}'),
776        (0x95, '\u{00EF}'),
777        (0x96, '\u{00F1}'),
778        (0x97, '\u{00F3}'),
779        (0x98, '\u{00F2}'),
780        (0x99, '\u{00F4}'),
781        (0x9A, '\u{00F6}'),
782        (0x9B, '\u{00F5}'),
783        (0x9C, '\u{00FA}'),
784        (0x9D, '\u{00F9}'),
785        (0x9E, '\u{00FB}'),
786        (0x9F, '\u{00FC}'),
787        (0xA0, '\u{2020}'),
788        (0xA1, '\u{00B0}'),
789        (0xA2, '\u{00A2}'),
790        (0xA3, '\u{00A3}'),
791        (0xA4, '\u{00A7}'),
792        (0xA5, '\u{2022}'),
793        (0xA6, '\u{00B6}'),
794        (0xA7, '\u{00DF}'),
795        (0xA8, '\u{00AE}'),
796        (0xA9, '\u{00A9}'),
797        (0xAA, '\u{2122}'),
798        (0xAB, '\u{00B4}'),
799        (0xAC, '\u{00A8}'),
800        (0xAD, '\u{2260}'),
801        (0xAE, '\u{00C6}'),
802        (0xAF, '\u{00D8}'),
803        (0xB0, '\u{221E}'),
804        (0xB1, '\u{00B1}'),
805        (0xB2, '\u{2264}'),
806        (0xB3, '\u{2265}'),
807        (0xB4, '\u{00A5}'),
808        (0xB5, '\u{00B5}'),
809        (0xB6, '\u{2202}'),
810        (0xB7, '\u{2211}'),
811        (0xB8, '\u{220F}'),
812        (0xB9, '\u{03C0}'),
813        (0xBA, '\u{222B}'),
814        (0xBB, '\u{00AA}'),
815        (0xBC, '\u{00BA}'),
816        (0xBD, '\u{03A9}'),
817        (0xBE, '\u{00E6}'),
818        (0xBF, '\u{00F8}'),
819        (0xC0, '\u{00BF}'),
820        (0xC1, '\u{00A1}'),
821        (0xC2, '\u{00AC}'),
822        (0xC3, '\u{221A}'),
823        (0xC4, '\u{0192}'),
824        (0xC5, '\u{2248}'),
825        (0xC6, '\u{2206}'),
826        (0xC7, '\u{00AB}'),
827        (0xC8, '\u{00BB}'),
828        (0xC9, '\u{2026}'),
829        (0xCA, '\u{00A0}'),
830        (0xCB, '\u{00C0}'),
831        (0xCC, '\u{00C3}'),
832        (0xCD, '\u{00D5}'),
833        (0xCE, '\u{0152}'),
834        (0xCF, '\u{0153}'),
835        (0xD0, '\u{2013}'),
836        (0xD1, '\u{2014}'),
837        (0xD2, '\u{201C}'),
838        (0xD3, '\u{201D}'),
839        (0xD4, '\u{2018}'),
840        (0xD5, '\u{2019}'),
841        (0xD6, '\u{00F7}'),
842        (0xD7, '\u{25CA}'),
843        (0xD8, '\u{00FF}'),
844        (0xD9, '\u{0178}'),
845        (0xDA, '\u{2044}'),
846        (0xDB, '\u{20AC}'),
847        (0xDC, '\u{2039}'),
848        (0xDD, '\u{203A}'),
849        (0xDE, '\u{FB01}'),
850        (0xDF, '\u{FB02}'),
851        (0xE0, '\u{2021}'),
852        (0xE1, '\u{00B7}'),
853        (0xE2, '\u{201A}'),
854        (0xE3, '\u{201E}'),
855        (0xE4, '\u{2030}'),
856        (0xE5, '\u{00C2}'),
857        (0xE6, '\u{00CA}'),
858        (0xE7, '\u{00C1}'),
859        (0xE8, '\u{00CB}'),
860        (0xE9, '\u{00C8}'),
861        (0xEA, '\u{00CD}'),
862        (0xEB, '\u{00CE}'),
863        (0xEC, '\u{00CF}'),
864        (0xED, '\u{00CC}'),
865        (0xEE, '\u{00D3}'),
866        (0xEF, '\u{00D4}'),
867        (0xF1, '\u{00D2}'),
868        (0xF2, '\u{00DA}'),
869        (0xF3, '\u{00DB}'),
870        (0xF4, '\u{00D9}'),
871        (0xF5, '\u{0131}'),
872        (0xF6, '\u{02C6}'),
873        (0xF7, '\u{02DC}'),
874        (0xF8, '\u{00AF}'),
875        (0xF9, '\u{02D8}'),
876        (0xFA, '\u{02D9}'),
877        (0xFB, '\u{02DA}'),
878        (0xFC, '\u{00B8}'),
879        (0xFD, '\u{02DD}'),
880        (0xFE, '\u{02DB}'),
881        (0xFF, '\u{02C7}'),
882    ];
883    for (b, c) in table {
884        m.set_char(*b, *c);
885    }
886}
887
888/// MacExpertEncoding — ISO 32000-1 Annex D.4. Only the slots that have a
889/// commonly-recognised Unicode equivalent are populated; the rest stay
890/// unassigned. This is the rarest encoding in practice — text-extraction
891/// almost never sees it.
892fn fill_macexpert(m: &mut EncodingMap) {
893    // The spec table is very sparse — we transcribe just the entries
894    // whose AGL name has a Unicode glyph today.
895    let table: &[(u8, char)] = &[
896        (0x20, ' '),
897        (0x21, '\u{F721}'),
898        (0x22, '\u{F6F8}'),
899        (0x23, '\u{F7A2}'),
900        (0x24, '\u{F724}'),
901        (0x25, '\u{F6E4}'),
902        (0x26, '\u{F726}'),
903        (0x27, '\u{F7B4}'),
904        (0x28, '\u{207D}'),
905        (0x29, '\u{207E}'),
906        (0x2A, '\u{2022}'),
907        (0x2B, '\u{2024}'),
908        (0x2C, ','),
909        (0x2D, '-'),
910        (0x2E, '.'),
911        (0x2F, '\u{2044}'),
912        (0x30, '\u{F730}'),
913        (0x31, '\u{F731}'),
914        (0x32, '\u{F732}'),
915        (0x33, '\u{F733}'),
916        (0x34, '\u{F734}'),
917        (0x35, '\u{F735}'),
918        (0x36, '\u{F736}'),
919        (0x37, '\u{F737}'),
920        (0x38, '\u{F738}'),
921        (0x39, '\u{F739}'),
922        (0x3A, ':'),
923        (0x3B, ';'),
924        (0x3D, '\u{F6DE}'),
925        (0x3F, '\u{F73F}'),
926    ];
927    for (b, c) in table {
928        m.set_char(*b, *c);
929    }
930}
931
932/// StandardEncoding — ISO 32000-1 Annex D.2 (Adobe Type 1 Standard
933/// encoding, the implicit default when a Type 1 font omits `/Encoding`).
934/// Bytes 0x20..0x7E + a small upper-byte set (0xA1..0xFA).
935fn fill_standard(m: &mut EncodingMap) {
936    // Most of 0x20..0x7E matches ASCII.
937    for b in 0x20u8..=0x7E {
938        m.set_char(b, b as char);
939    }
940    // Per Annex D.2, a few code points in the printable ASCII range
941    // overlap the AGL names that are NOT plain ASCII (e.g. 0x27 in
942    // StandardEncoding is `quoteright` → U+2019, not U+0027).
943    m.set_char(0x27, '\u{2019}'); // quoteright
944    m.set_char(0x60, '\u{2018}'); // quoteleft
945    m.set_char(0x22, '"'); // quotedbl
946                           // Upper bytes — the "Standard Latin" extras.
947    let upper: &[(u8, char)] = &[
948        (0xA1, '\u{00A1}'),
949        (0xA2, '\u{00A2}'),
950        (0xA3, '\u{00A3}'),
951        (0xA4, '\u{2044}'),
952        (0xA5, '\u{00A5}'),
953        (0xA6, '\u{0192}'),
954        (0xA7, '\u{00A7}'),
955        (0xA8, '\u{00A4}'),
956        (0xA9, '\''),
957        (0xAA, '\u{201C}'),
958        (0xAB, '\u{00AB}'),
959        (0xAC, '\u{2039}'),
960        (0xAD, '\u{203A}'),
961        (0xAE, '\u{FB01}'),
962        (0xAF, '\u{FB02}'),
963        (0xB1, '\u{2013}'),
964        (0xB2, '\u{2020}'),
965        (0xB3, '\u{2021}'),
966        (0xB4, '\u{00B7}'),
967        (0xB6, '\u{00B6}'),
968        (0xB7, '\u{2022}'),
969        (0xB8, '\u{201A}'),
970        (0xB9, '\u{201E}'),
971        (0xBA, '\u{201D}'),
972        (0xBB, '\u{00BB}'),
973        (0xBC, '\u{2026}'),
974        (0xBD, '\u{2030}'),
975        (0xBF, '\u{00BF}'),
976        (0xC1, '\u{0060}'),
977        (0xC2, '\u{00B4}'),
978        (0xC3, '\u{02C6}'),
979        (0xC4, '\u{02DC}'),
980        (0xC5, '\u{00AF}'),
981        (0xC6, '\u{02D8}'),
982        (0xC7, '\u{02D9}'),
983        (0xC8, '\u{00A8}'),
984        (0xCA, '\u{02DA}'),
985        (0xCB, '\u{00B8}'),
986        (0xCD, '\u{02DD}'),
987        (0xCE, '\u{02DB}'),
988        (0xCF, '\u{02C7}'),
989        (0xD0, '\u{2014}'),
990        (0xE1, '\u{00C6}'),
991        (0xE3, '\u{00AA}'),
992        (0xE8, '\u{0141}'),
993        (0xE9, '\u{00D8}'),
994        (0xEA, '\u{0152}'),
995        (0xEB, '\u{00BA}'),
996        (0xF1, '\u{00E6}'),
997        (0xF5, '\u{0131}'),
998        (0xF8, '\u{0142}'),
999        (0xF9, '\u{00F8}'),
1000        (0xFA, '\u{0153}'),
1001        (0xFB, '\u{00DF}'),
1002    ];
1003    for (b, c) in upper {
1004        m.set_char(*b, *c);
1005    }
1006}
1007
1008/// SymbolEncoding — ISO 32000-1 Annex D.5 (Symbol font's built-in
1009/// encoding — Greek + math). Sparse; only the spec entries.
1010fn fill_symbol(m: &mut EncodingMap) {
1011    let table: &[(u8, char)] = &[
1012        (0x20, ' '),
1013        (0x21, '!'),
1014        (0x22, '\u{2200}'), // universal
1015        (0x23, '#'),
1016        (0x24, '\u{2203}'), // existential
1017        (0x25, '%'),
1018        (0x26, '&'),
1019        (0x27, '\u{220B}'),
1020        (0x28, '('),
1021        (0x29, ')'),
1022        (0x2A, '\u{2217}'),
1023        (0x2B, '+'),
1024        (0x2C, ','),
1025        (0x2D, '\u{2212}'),
1026        (0x2E, '.'),
1027        (0x2F, '/'),
1028        (0x30, '0'),
1029        (0x31, '1'),
1030        (0x32, '2'),
1031        (0x33, '3'),
1032        (0x34, '4'),
1033        (0x35, '5'),
1034        (0x36, '6'),
1035        (0x37, '7'),
1036        (0x38, '8'),
1037        (0x39, '9'),
1038        (0x3A, ':'),
1039        (0x3B, ';'),
1040        (0x3C, '<'),
1041        (0x3D, '='),
1042        (0x3E, '>'),
1043        (0x3F, '?'),
1044        (0x40, '\u{2245}'),
1045        (0x41, '\u{0391}'),
1046        (0x42, '\u{0392}'),
1047        (0x43, '\u{03A7}'),
1048        (0x44, '\u{0394}'),
1049        (0x45, '\u{0395}'),
1050        (0x46, '\u{03A6}'),
1051        (0x47, '\u{0393}'),
1052        (0x48, '\u{0397}'),
1053        (0x49, '\u{0399}'),
1054        (0x4A, '\u{03D1}'),
1055        (0x4B, '\u{039A}'),
1056        (0x4C, '\u{039B}'),
1057        (0x4D, '\u{039C}'),
1058        (0x4E, '\u{039D}'),
1059        (0x4F, '\u{039F}'),
1060        (0x50, '\u{03A0}'),
1061        (0x51, '\u{0398}'),
1062        (0x52, '\u{03A1}'),
1063        (0x53, '\u{03A3}'),
1064        (0x54, '\u{03A4}'),
1065        (0x55, '\u{03A5}'),
1066        (0x56, '\u{03C2}'),
1067        (0x57, '\u{03A9}'),
1068        (0x58, '\u{039E}'),
1069        (0x59, '\u{03A8}'),
1070        (0x5A, '\u{0396}'),
1071        (0x61, '\u{03B1}'),
1072        (0x62, '\u{03B2}'),
1073        (0x63, '\u{03C7}'),
1074        (0x64, '\u{03B4}'),
1075        (0x65, '\u{03B5}'),
1076        (0x66, '\u{03C6}'),
1077        (0x67, '\u{03B3}'),
1078        (0x68, '\u{03B7}'),
1079        (0x69, '\u{03B9}'),
1080        (0x6A, '\u{03D5}'),
1081        (0x6B, '\u{03BA}'),
1082        (0x6C, '\u{03BB}'),
1083        (0x6D, '\u{03BC}'),
1084        (0x6E, '\u{03BD}'),
1085        (0x6F, '\u{03BF}'),
1086        (0x70, '\u{03C0}'),
1087        (0x71, '\u{03B8}'),
1088        (0x72, '\u{03C1}'),
1089        (0x73, '\u{03C3}'),
1090        (0x74, '\u{03C4}'),
1091        (0x75, '\u{03C5}'),
1092        (0x76, '\u{03D6}'),
1093        (0x77, '\u{03C9}'),
1094        (0x78, '\u{03BE}'),
1095        (0x79, '\u{03C8}'),
1096        (0x7A, '\u{03B6}'),
1097    ];
1098    for (b, c) in table {
1099        m.set_char(*b, *c);
1100    }
1101}
1102
1103/// ZapfDingbatsEncoding — ISO 32000-1 Annex D.6 (the bundled dingbats
1104/// font's built-in encoding). Sparse subset; full table is round-29+.
1105fn fill_zapfdingbats(m: &mut EncodingMap) {
1106    let table: &[(u8, char)] = &[
1107        (0x20, ' '),
1108        (0x21, '\u{2701}'),
1109        (0x22, '\u{2702}'),
1110        (0x23, '\u{2703}'),
1111        (0x24, '\u{2704}'),
1112        (0x25, '\u{260E}'),
1113        (0x26, '\u{2706}'),
1114        (0x27, '\u{2707}'),
1115        (0x28, '\u{2708}'),
1116        (0x29, '\u{2709}'),
1117        (0x2A, '\u{261B}'),
1118        (0x2B, '\u{261E}'),
1119        (0x2C, '\u{270C}'),
1120        (0x2D, '\u{270D}'),
1121        (0x2E, '\u{270E}'),
1122        (0x2F, '\u{270F}'),
1123        (0x30, '\u{2710}'),
1124        (0x31, '\u{2711}'),
1125        (0x32, '\u{2712}'),
1126        (0x33, '\u{2713}'),
1127        (0x34, '\u{2714}'),
1128        (0x35, '\u{2715}'),
1129        (0x36, '\u{2716}'),
1130        (0x37, '\u{2717}'),
1131        (0x38, '\u{2718}'),
1132        (0x39, '\u{2719}'),
1133        (0x3A, '\u{271A}'),
1134        (0x3B, '\u{271B}'),
1135        (0x3C, '\u{271C}'),
1136        (0x3D, '\u{271D}'),
1137        (0x3E, '\u{271E}'),
1138        (0x3F, '\u{271F}'),
1139        (0x40, '\u{2720}'),
1140        (0x41, '\u{2721}'),
1141        (0x42, '\u{2722}'),
1142        (0x43, '\u{2723}'),
1143    ];
1144    for (b, c) in table {
1145        m.set_char(*b, *c);
1146    }
1147}
1148
1149// ────────────────────────── tests ──────────────────────────
1150
1151#[cfg(test)]
1152mod tests {
1153    use super::*;
1154
1155    fn name(s: &str) -> Object {
1156        Object::Name(s.to_string())
1157    }
1158    fn int(n: i64) -> Object {
1159        Object::Integer(n)
1160    }
1161
1162    #[test]
1163    fn parse_simple_differences_array() {
1164        // [24 /breve /caron /circumflex 32 /space]
1165        let arr = Object::Array(vec![
1166            int(24),
1167            name("breve"),
1168            name("caron"),
1169            name("circumflex"),
1170            int(32),
1171            name("space"),
1172        ]);
1173        let d = parse_encoding_differences(&arr).unwrap();
1174        assert_eq!(d.overrides.len(), 4);
1175        assert_eq!(d.overrides[0].code, 24);
1176        assert_eq!(d.overrides[0].glyph_name, "breve");
1177        assert_eq!(d.overrides[1].code, 25);
1178        assert_eq!(d.overrides[1].glyph_name, "caron");
1179        assert_eq!(d.overrides[2].code, 26);
1180        assert_eq!(d.overrides[2].glyph_name, "circumflex");
1181        assert_eq!(d.overrides[3].code, 32);
1182        assert_eq!(d.overrides[3].glyph_name, "space");
1183    }
1184
1185    #[test]
1186    fn parse_differences_skips_unknown_tokens() {
1187        // [24 /breve null /caron]  → null is skipped, caron lands at 25.
1188        let arr = Object::Array(vec![int(24), name("breve"), Object::Null, name("caron")]);
1189        let d = parse_encoding_differences(&arr).unwrap();
1190        assert_eq!(d.overrides.len(), 2);
1191        assert_eq!(d.overrides[0].code, 24);
1192        assert_eq!(d.overrides[1].code, 25);
1193        assert_eq!(d.overrides[1].glyph_name, "caron");
1194    }
1195
1196    #[test]
1197    fn parse_differences_real_coerced() {
1198        // [24.0 /breve]
1199        let arr = Object::Array(vec![Object::Real(24.0), name("breve")]);
1200        let d = parse_encoding_differences(&arr).unwrap();
1201        assert_eq!(d.overrides.len(), 1);
1202        assert_eq!(d.overrides[0].code, 24);
1203    }
1204
1205    #[test]
1206    fn parse_differences_rejects_non_array() {
1207        let r = parse_encoding_differences(&Object::Null);
1208        assert!(r.is_err());
1209    }
1210
1211    #[test]
1212    fn agl_lookup_basic_latin() {
1213        assert_eq!(glyph_name_to_unicode("A").as_deref(), Some("A"));
1214        assert_eq!(glyph_name_to_unicode("space").as_deref(), Some(" "));
1215        assert_eq!(glyph_name_to_unicode("zero").as_deref(), Some("0"));
1216    }
1217
1218    #[test]
1219    fn agl_lookup_smart_quotes() {
1220        assert_eq!(
1221            glyph_name_to_unicode("quoteright").as_deref(),
1222            Some("\u{2019}")
1223        );
1224        assert_eq!(
1225            glyph_name_to_unicode("quotedblleft").as_deref(),
1226            Some("\u{201C}")
1227        );
1228    }
1229
1230    #[test]
1231    fn agl_lookup_ligature() {
1232        assert_eq!(glyph_name_to_unicode("fi").as_deref(), Some("fi"));
1233        assert_eq!(glyph_name_to_unicode("fl").as_deref(), Some("fl"));
1234    }
1235
1236    #[test]
1237    fn agl_lookup_notdef_is_empty() {
1238        assert_eq!(glyph_name_to_unicode(".notdef").as_deref(), Some(""));
1239    }
1240
1241    #[test]
1242    fn agl_lookup_unknown() {
1243        assert!(glyph_name_to_unicode("notaglyphname").is_none());
1244    }
1245
1246    #[test]
1247    fn agl_uni_bmp_single_group() {
1248        // AGL PIN §3 — `uniXXXX` for one BMP codepoint.
1249        assert_eq!(
1250            glyph_name_to_unicode("uni201C").as_deref(),
1251            Some("\u{201C}")
1252        );
1253        assert_eq!(
1254            glyph_name_to_unicode("uni2019").as_deref(),
1255            Some("\u{2019}")
1256        );
1257        // BMP edge — U+0041 ('A'). Static table preferred when `/A` is
1258        // emitted, but the escape resolves through this path too.
1259        assert_eq!(glyph_name_to_unicode("uni0041").as_deref(), Some("A"));
1260    }
1261
1262    #[test]
1263    fn agl_uni_bmp_multi_group() {
1264        // Multi-group concatenation per AGL PIN §3 — two codepoints
1265        // glued into one glyph name.
1266        assert_eq!(
1267            glyph_name_to_unicode("uni20142019").as_deref(),
1268            Some("\u{2014}\u{2019}")
1269        );
1270    }
1271
1272    #[test]
1273    fn agl_uni_supplementary_plane() {
1274        // `uXXXXXXXX` form for a supplementary-plane codepoint.
1275        // U+1F600 GRINNING FACE — encoded as 5 hex chars.
1276        assert_eq!(
1277            glyph_name_to_unicode("u1F600").as_deref(),
1278            Some("\u{1F600}")
1279        );
1280        // Full 6-char form for the highest valid Unicode (U+10FFFF).
1281        assert_eq!(
1282            glyph_name_to_unicode("u10FFFF").as_deref(),
1283            Some("\u{10FFFF}")
1284        );
1285        // 4-digit `u` form is also valid per AGL PIN §3.
1286        assert_eq!(glyph_name_to_unicode("u00A9").as_deref(), Some("\u{00A9}"));
1287    }
1288
1289    #[test]
1290    fn agl_uni_rejects_surrogate_halves() {
1291        // U+D800 is the start of the surrogate range — must not decode.
1292        assert!(glyph_name_to_unicode("uniD800").is_none());
1293        assert!(glyph_name_to_unicode("uniDFFF").is_none());
1294        assert!(glyph_name_to_unicode("uD800").is_none());
1295    }
1296
1297    #[test]
1298    fn agl_uni_rejects_ffff_noncharacter() {
1299        // AGL PIN §3 carves out U+FFFF.
1300        assert!(glyph_name_to_unicode("uniFFFF").is_none());
1301        assert!(glyph_name_to_unicode("uFFFF").is_none());
1302    }
1303
1304    #[test]
1305    fn agl_uni_rejects_misshapen_input() {
1306        // `uni` with a remainder not divisible by 4 — reject.
1307        assert!(glyph_name_to_unicode("uni20").is_none());
1308        assert!(glyph_name_to_unicode("uni20142").is_none());
1309        // `uni` with no remainder — reject.
1310        assert!(glyph_name_to_unicode("uni").is_none());
1311        // `u` with too-few or too-many hex digits — reject.
1312        assert!(glyph_name_to_unicode("u041").is_none());
1313        assert!(glyph_name_to_unicode("u1234567").is_none());
1314        // `u` over U+10FFFF — reject (char::from_u32 returns None).
1315        assert!(glyph_name_to_unicode("u110000").is_none());
1316        // Lowercase hex — AGL canon is uppercase, reject so the
1317        // ambiguity doesn't propagate into the encoding table.
1318        assert!(glyph_name_to_unicode("uni201c").is_none());
1319        assert!(glyph_name_to_unicode("u1f600").is_none());
1320        // Non-hex bytes in the suffix — reject.
1321        assert!(glyph_name_to_unicode("uniZZZZ").is_none());
1322        // The bare `u` / `uni` prefix on a real AGL name (e.g.
1323        // `university` — not in the AGL subset) must not be mistaken
1324        // for the escape form.
1325        assert!(glyph_name_to_unicode("university").is_none());
1326    }
1327
1328    #[test]
1329    fn agl_uni_escape_in_differences() {
1330        // End-to-end: a `/Differences` override that uses the
1331        // `uniXXXX` escape decodes to the correct Unicode payload.
1332        let base = EncodingMap::from_base(BaseEncoding::WinAnsi);
1333        let diffs = EncodingDifferences {
1334            overrides: vec![
1335                EncodingOverride {
1336                    code: 0x80,
1337                    glyph_name: "uni201C".to_string(),
1338                },
1339                EncodingOverride {
1340                    code: 0x81,
1341                    glyph_name: "u1F600".to_string(),
1342                },
1343            ],
1344        };
1345        let out = apply_encoding_differences(&base, &diffs);
1346        assert_eq!(out.decode(&[0x80]), "\u{201C}");
1347        assert_eq!(out.decode(&[0x81]), "\u{1F600}");
1348    }
1349
1350    #[test]
1351    fn winansi_base_map_decodes_ascii_and_smart_quote() {
1352        let m = EncodingMap::from_base(BaseEncoding::WinAnsi);
1353        assert_eq!(m.decode(b"Hello"), "Hello");
1354        // 0x93 = U+201C in CP1252.
1355        assert_eq!(m.decode(&[0x93]), "\u{201C}");
1356    }
1357
1358    #[test]
1359    fn apply_differences_overrides_base_map() {
1360        // Start with WinAnsi (0x41 = 'A'). Override 0x41 → /Omega.
1361        let base = EncodingMap::from_base(BaseEncoding::WinAnsi);
1362        let diffs = EncodingDifferences {
1363            overrides: vec![EncodingOverride {
1364                code: 0x41,
1365                glyph_name: "Omega".to_string(),
1366            }],
1367        };
1368        let out = apply_encoding_differences(&base, &diffs);
1369        assert_eq!(out.decode(&[0x41]), "\u{03A9}"); // Greek capital Omega
1370                                                     // Sibling codes unchanged.
1371        assert_eq!(out.decode(&[0x42]), "B");
1372    }
1373
1374    #[test]
1375    fn apply_differences_unknown_glyph_becomes_replacement() {
1376        let base = EncodingMap::from_base(BaseEncoding::WinAnsi);
1377        let diffs = EncodingDifferences {
1378            overrides: vec![EncodingOverride {
1379                code: 0x41,
1380                glyph_name: "not-a-real-glyph-name".to_string(),
1381            }],
1382        };
1383        let out = apply_encoding_differences(&base, &diffs);
1384        // 0x41 slot is now empty → decode emits U+FFFD.
1385        assert_eq!(out.decode(&[0x41]), "\u{FFFD}");
1386    }
1387
1388    #[test]
1389    fn apply_differences_ligature_expands() {
1390        let base = EncodingMap::from_base(BaseEncoding::WinAnsi);
1391        let diffs = EncodingDifferences {
1392            overrides: vec![EncodingOverride {
1393                code: 0xFD,
1394                glyph_name: "fi".to_string(),
1395            }],
1396        };
1397        let out = apply_encoding_differences(&base, &diffs);
1398        assert_eq!(out.decode(&[0xFD]), "fi");
1399    }
1400
1401    #[test]
1402    fn macroman_base_map_smart_quotes() {
1403        let m = EncodingMap::from_base(BaseEncoding::MacRoman);
1404        // MacRoman 0xD2 = U+201C left double smart quote.
1405        assert_eq!(m.decode(&[0xD2]), "\u{201C}");
1406    }
1407
1408    #[test]
1409    fn standard_base_map_quotes() {
1410        let m = EncodingMap::from_base(BaseEncoding::Standard);
1411        // Standard 0x27 = quoteright = U+2019, not ASCII apostrophe.
1412        assert_eq!(m.decode(&[0x27]), "\u{2019}");
1413    }
1414
1415    #[test]
1416    fn symbol_base_map_alpha() {
1417        let m = EncodingMap::from_base(BaseEncoding::Symbol);
1418        // Symbol 0x41 = uppercase Alpha = U+0391.
1419        assert_eq!(m.decode(&[0x41]), "\u{0391}");
1420        // Symbol 0x70 = lowercase pi = U+03C0.
1421        assert_eq!(m.decode(&[0x70]), "\u{03C0}");
1422    }
1423
1424    #[test]
1425    fn base_encoding_name_recognition() {
1426        assert_eq!(
1427            BaseEncoding::from_name("WinAnsiEncoding"),
1428            Some(BaseEncoding::WinAnsi)
1429        );
1430        assert_eq!(
1431            BaseEncoding::from_name("MacRomanEncoding"),
1432            Some(BaseEncoding::MacRoman)
1433        );
1434        assert_eq!(BaseEncoding::from_name("not-a-real-name"), None);
1435    }
1436
1437    #[test]
1438    fn unassigned_code_decodes_to_replacement() {
1439        let m = EncodingMap::from_base(BaseEncoding::Standard);
1440        // StandardEncoding has nothing at 0x00 — must come back as FFFD.
1441        assert_eq!(m.decode(&[0x00]), "\u{FFFD}");
1442    }
1443}