Skip to main content

pdfrum_cmap/
lib.rs

1#![doc = include_str!("../README.md")]
2// Inheritance runs through three mechanisms, all live:
3//
4// - the built-in tables' own chaining, which is what makes `GB-EUC-V` a thin
5//   override of `GB-EUC-H`;
6// - `usecmap` inside an embedded program, resolved by `parse_embedded`;
7// - the `/UseCMap` key of an `/Encoding` stream's dictionary, attached by
8//   `inherit_from`, which supersedes the operator.
9//
10// The last two are ISO 32000-1 §9.7.5.3's two channels, and both are
11// child-wins: a code the child maps is the child's answer, and only a code it
12// maps to nothing reaches the parent. The oracle implements neither — see the
13// marked site in `parser.rs`.
14#![forbid(unsafe_code)]
15#![cfg_attr(docsrs, feature(doc_cfg))]
16// Every byte reaching this crate came from an untrusted file or a generated
17// blob: index with `get()`.
18#![warn(clippy::indexing_slicing)]
19// Narrowing casts are *behavior* in this crate, not accidents. A CID is a
20// 16-bit value and a CMap program declaring `<10000>` means CID 0; a character
21// code is written out one byte at a time, each one the low bits of a wider
22// value. Every such cast below is deliberate and pinned by a test, so the
23// lint is off crate-wide rather than annotated a few dozen times.
24#![allow(clippy::cast_possible_truncation)]
25
26mod blob;
27mod cid2unicode;
28mod decode;
29mod error;
30mod ids;
31mod lexer;
32mod parser;
33mod predefined;
34mod static_lookup;
35
36pub use error::Error;
37pub use ids::{CharCode, Cid, CidCoding, CidSet, CodingScheme};
38pub use lexer::Words;
39
40use decode::Decoder;
41use parser::{CidRange, DirectTable};
42use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
43use pdfrum_object::Name;
44
45/// Where a CMap's CIDs come from.
46#[derive(Debug, Clone, PartialEq, Eq)]
47enum CidMap {
48    /// The character code *is* the CID. Both `Identity-H`/`-V` and the
49    /// fallback for a name that resolved to nothing behave this way, and they
50    /// are indistinguishable once built.
51    Identity,
52    /// One entry of a built-in registry's table, plus whatever its chain
53    /// defers to.
54    Static { registry: usize, index: usize },
55    /// A CMap program's own tables: a dense array for codes below `0x1_0000`
56    /// and a sorted list for the rest.
57    Embedded {
58        direct: DirectTable,
59        additional: Vec<CidRange>,
60    },
61}
62
63/// A CMap: a byte decoder plus a charcode→CID map (ISO 32000-1 §9.7.5).
64///
65/// Build one with [`from_encoding_name`], [`predefined`] or
66/// [`parse_embedded`].
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CMap {
69    decoder: Decoder,
70    map: CidMap,
71    vertical: bool,
72    loaded: bool,
73    charset: CidSet,
74    coding: CidCoding,
75    /// The CMap this one inherits from, named by a `usecmap` operator inside
76    /// the program or by the `/UseCMap` key of its stream dictionary
77    /// (ISO 32000-1 §9.7.5.3). Consulted only where this CMap maps nothing,
78    /// which is what makes inheritance child-wins.
79    inherited: Option<Box<CMap>>,
80}
81
82impl CMap {
83    /// The CMap a file gets when its `/Encoding` names nothing recognisable:
84    /// fixed two-byte codes, every code its own CID, no character collection.
85    fn unrecognized(vertical: bool) -> Self {
86        Self {
87            decoder: Decoder::TwoBytes,
88            map: CidMap::Identity,
89            vertical,
90            loaded: false,
91            charset: CidSet::Unknown,
92            coding: CidCoding::Unknown,
93            inherited: None,
94        }
95    }
96
97    /// Decode a string into `(character code, CID)` pairs — the entry point
98    /// every text path shares.
99    ///
100    /// Iteration ends when the string is exhausted. Damage never stops it
101    /// early: a code no codespace range accepts comes back as `CharCode(0)`,
102    /// and a code cut off by the end of the string does too.
103    ///
104    /// ```
105    /// use pdfrum_cmap::{CharCode, Cid, from_encoding_name};
106    /// use pdfrum_common::Diagnostics;
107    /// use pdfrum_object::Name;
108    ///
109    /// let mut diags = Diagnostics::default();
110    /// let cmap = from_encoding_name(&Name::from("GB-EUC-H"), &mut diags);
111    ///
112    /// // 0x41 is not a lead byte, so it is a one-byte code; 0xA1 0xA1 is a pair.
113    /// let codes: Vec<CharCode> = cmap.decode(&[0x41, 0xA1, 0xA1]).map(|(c, _)| c).collect();
114    /// assert_eq!(codes, vec![CharCode(0x41), CharCode(0xA1A1)]);
115    /// assert_eq!(cmap.cid(CharCode(0xA1A1)), Cid(0x0060));
116    /// ```
117    pub fn decode<'a>(&'a self, bytes: &'a [u8]) -> impl Iterator<Item = (CharCode, Cid)> + 'a {
118        let mut offset = 0usize;
119        std::iter::from_fn(move || {
120            if offset >= bytes.len() {
121                return None;
122            }
123            let before = offset;
124            let code = self.next_char(bytes, &mut offset);
125            // A decoder that consumed nothing would loop forever; no scheme
126            // does, but the guard makes that a property of this loop rather
127            // than of every decoder arm.
128            if offset == before {
129                return None;
130            }
131            Some((code, self.cid(code)))
132        })
133    }
134
135    /// Decode one character code, advancing `offset`.
136    ///
137    /// Returns `CharCode(0)` rather than failing when the bytes are damaged.
138    /// `offset` may land past a truncated code's start without reaching the
139    /// end of the string, which is how iteration terminates.
140    pub fn next_char(&self, bytes: &[u8], offset: &mut usize) -> CharCode {
141        decode::next_char(&self.decoder, bytes, offset)
142    }
143
144    /// The CID a character code maps to. Unmapped codes give [`Cid(0)`](Cid),
145    /// which is `.notdef`.
146    ///
147    /// A CMap that inherits another through `usecmap` or `/UseCMap` answers
148    /// from its own tables first and asks its parent only where it maps
149    /// nothing — the child-wins rule of ISO 32000-1 §9.7.5.3.
150    #[must_use]
151    pub fn cid(&self, code: CharCode) -> Cid {
152        let charcode = code.0;
153        let own = match &self.map {
154            CidMap::Identity => charcode as u16,
155            CidMap::Static { registry, index } => {
156                static_lookup::cid_from_charcode(*registry, *index, charcode)
157            }
158            CidMap::Embedded { direct, additional } => direct
159                .get(charcode)
160                .unwrap_or_else(|| lookup_additional(additional, charcode)),
161        };
162        // CID 0 is `.notdef` and is the crate's single "maps nothing" answer,
163        // so it is also the point at which the parent is asked. A child that
164        // wants a code to *be* `.notdef` cannot say so — the same limitation
165        // pdf.js has, whose `contains` is likewise a presence test over a map
166        // that never stores a zero.
167        if own != 0 {
168            return Cid(own);
169        }
170        match &self.inherited {
171            Some(parent) => parent.cid(code),
172            None => Cid(0),
173        }
174    }
175
176    /// The character code that maps to `cid`, or [`CharCode(0)`](CharCode)
177    /// when none does.
178    ///
179    /// Defined only for a predefined CMap backed by the built-in tables; every
180    /// other kind returns 0. Even for those it is partial: the scan does not
181    /// consult the four-byte tables, so a CID that exists only above
182    /// `0x1_0000` is unreachable in reverse. This is used by CID fonts to find
183    /// a code for a character they were asked to draw by Unicode.
184    #[must_use]
185    pub fn charcode_from_cid(&self, cid: Cid) -> CharCode {
186        CharCode(match &self.map {
187            CidMap::Static { registry, index } => {
188                static_lookup::charcode_from_cid(*registry, *index, cid.0)
189            }
190            CidMap::Identity | CidMap::Embedded { .. } => 0,
191        })
192    }
193
194    /// How many bytes a code of this value occupies.
195    ///
196    /// Derived from the value alone, not from the bytes it was decoded from
197    /// and not from the codespace ranges. That makes it disagree with
198    /// [`append_char`](CMap::append_char) in exactly one case: under a
199    /// mixed-two-byte scheme a code below `0x100` whose value is itself a
200    /// lead byte reports width 1 but is *written* as two bytes. Code that
201    /// needs the encoded width should measure what `append_char` produced.
202    #[must_use]
203    pub fn char_size(&self, code: CharCode) -> u8 {
204        decode::char_size(&self.decoder, code)
205    }
206
207    /// How many character codes a byte string holds. Always equal to the
208    /// number of pairs [`decode`](CMap::decode) yields for the same string.
209    #[must_use]
210    pub fn count_chars(&self, bytes: &[u8]) -> usize {
211        decode::count_chars(&self.decoder, bytes)
212    }
213
214    /// Append a character code to a byte string in this CMap's encoding — the
215    /// inverse of [`next_char`](CMap::next_char).
216    pub fn append_char(&self, out: &mut Vec<u8>, code: CharCode) {
217        decode::append_char(&self.decoder, out, code);
218    }
219
220    /// Whether this CMap selects vertical writing mode (ISO 32000-1 §9.7.4.3).
221    ///
222    /// Decided by the last byte of the name as written, before any suffix
223    /// handling, so `Identity-V` is vertical and so is any name ending in `V`.
224    #[must_use]
225    pub fn is_vertical(&self) -> bool {
226        self.vertical
227    }
228
229    /// Whether the name resolved to real tables.
230    ///
231    /// `false` covers two different failures that behave the same way: a name
232    /// matching no row at all, and a name whose row was found but whose
233    /// built-in table was not. Either way the CMap still decodes and still
234    /// maps codes; it just maps them to themselves.
235    #[must_use]
236    pub fn is_loaded(&self) -> bool {
237        self.loaded
238    }
239
240    /// The legacy encoding family this CMap's codes belong to.
241    #[must_use]
242    pub fn coding(&self) -> CidCoding {
243        self.coding
244    }
245
246    /// The character collection its CIDs index.
247    #[must_use]
248    pub fn charset(&self) -> CidSet {
249        self.charset
250    }
251
252    /// How its bytes split into character codes.
253    #[must_use]
254    pub fn coding_scheme(&self) -> CodingScheme {
255        self.decoder.scheme()
256    }
257
258    /// Whether this CMap has no dense charcode→CID table — true for every
259    /// predefined CMap and false for every embedded one, whatever the program
260    /// contained. CID fonts branch on it when choosing how to find a glyph.
261    #[must_use]
262    pub fn has_no_direct_table(&self) -> bool {
263        !matches!(self.map, CidMap::Embedded { .. })
264    }
265
266    /// Whether this CMap resolved to one of the built-in static tables.
267    #[must_use]
268    pub fn has_static_map(&self) -> bool {
269        matches!(self.map, CidMap::Static { .. })
270    }
271}
272
273/// Binary search of the wide-code ranges an embedded CMap declared.
274fn lookup_additional(ranges: &[CidRange], charcode: u32) -> u16 {
275    let at = ranges.partition_point(|r| r.end_code < charcode);
276    match ranges.get(at) {
277        Some(r) if r.start_code <= charcode => {
278            (u32::from(r.start_cid) + charcode - r.start_code) as u16
279        }
280        _ => 0,
281    }
282}
283
284/// Look up one of the built-in CMap names.
285///
286/// Returns `None` for a name that matches no row and is not `Identity-H` or
287/// `Identity-V`. A caller resolving a font's `/Encoding` almost always wants
288/// [`from_encoding_name`] instead, which builds the same fallback the oracle
289/// does rather than reporting the miss.
290///
291/// ```
292/// use pdfrum_cmap::{CidSet, CodingScheme, predefined};
293/// use pdfrum_object::Name;
294///
295/// let gb = predefined(&Name::from("GB-EUC-H")).unwrap();
296/// assert_eq!(gb.charset(), CidSet::Gb1);
297/// assert_eq!(gb.coding_scheme(), CodingScheme::MixedTwoBytes);
298/// assert!(gb.is_loaded());
299///
300/// assert!(predefined(&Name::from("Nonsense")).is_none());
301/// ```
302#[must_use]
303pub fn predefined(name: &Name) -> Option<CMap> {
304    let raw = predefined::strip_slash(name.as_bytes());
305    let vertical = predefined::is_vertical(raw);
306    if predefined::is_identity(raw) {
307        return Some(CMap {
308            decoder: Decoder::TwoBytes,
309            map: CidMap::Identity,
310            vertical,
311            loaded: true,
312            charset: CidSet::Unknown,
313            coding: CidCoding::Cid,
314            inherited: None,
315        });
316    }
317    let row = predefined::resolve(raw)?;
318    let decoder = match (row.scheme, row.leading) {
319        (CodingScheme::MixedTwoBytes, Some(leading)) => Decoder::MixedTwoBytes { leading },
320        (CodingScheme::OneByte, _) => Decoder::OneByte,
321        (CodingScheme::MixedFourBytes, _) => Decoder::MixedFourBytes { ranges: Vec::new() },
322        _ => Decoder::TwoBytes,
323    };
324    // The static table is keyed by the *full* name, not the truncated stem, so
325    // a name that found a decoder row can still find no table — a real,
326    // reachable state with a correct decoder and no CID map.
327    let table = row
328        .charset
329        .registry_index()
330        .and_then(|reg| static_lookup::find(reg, raw).map(|index| (reg, index)));
331    let (map, loaded) = match table {
332        Some((registry, index)) => (CidMap::Static { registry, index }, true),
333        None => (CidMap::Identity, false),
334    };
335    Some(CMap {
336        decoder,
337        map,
338        vertical,
339        loaded,
340        charset: row.charset,
341        coding: row.coding,
342        inherited: None,
343    })
344}
345
346/// Resolve a font's `/Encoding` name to a CMap, failures included.
347///
348/// Never fails. A name that resolves to nothing produces a CMap that decodes
349/// fixed two-byte codes and maps every code to itself, and records a
350/// diagnostic — that is what the oracle does, and a file with
351/// `/Encoding /Nonsense` renders because of it.
352///
353/// ```
354/// use pdfrum_cmap::{CharCode, Cid, CidSet, CodingScheme, from_encoding_name};
355/// use pdfrum_common::Diagnostics;
356/// use pdfrum_object::Name;
357///
358/// let mut diags = Diagnostics::default();
359/// let cmap = from_encoding_name(&Name::from("Nonsense"), &mut diags);
360///
361/// assert!(!cmap.is_loaded());
362/// assert_eq!(cmap.charset(), CidSet::Unknown);
363/// assert_eq!(cmap.coding_scheme(), CodingScheme::TwoBytes);
364/// assert_eq!(cmap.cid(CharCode(0x1234)), Cid(0x1234));  // identity fallback
365/// assert_eq!(diags.len(), 1);
366/// ```
367#[must_use]
368pub fn from_encoding_name(name: &Name, diags: &mut Diagnostics) -> CMap {
369    let raw = predefined::strip_slash(name.as_bytes());
370    let Some(cmap) = predefined(name) else {
371        diags.record(Severity::Suspicious, DiagKind::CMapNameUnknown, None);
372        return CMap::unrecognized(predefined::is_vertical(raw));
373    };
374    if !cmap.is_loaded() {
375        diags.record(Severity::Suspicious, DiagKind::CMapTableMissing, None);
376    }
377    cmap
378}
379
380/// Read an embedded CMap program — the decoded bytes of an `/Encoding` stream.
381///
382/// Never fails; damage is recorded on `diags`. `limits` caps how many
383/// codespace and wide-code ranges one program may declare, which the oracle
384/// leaves unbounded.
385///
386/// A `usecmap` operator inside the program names a parent, which is resolved
387/// against the built-in CMaps and consulted for every code this program does
388/// not map itself (ISO 32000-1 §9.7.5.3). For the other inheritance channel —
389/// the stream dictionary's `/UseCMap` key, which may name a stream rather
390/// than a built-in — use [`inherit_from`], which takes precedence.
391///
392/// ```
393/// use pdfrum_cmap::{CharCode, Cid, CodingScheme, parse_embedded};
394/// use pdfrum_common::{Diagnostics, Limits};
395///
396/// let program = b"
397///     begincodespacerange <0000> <ffff> endcodespacerange
398///     1 begincidrange <0020> <007e> <0001> endcidrange
399/// ";
400/// let mut diags = Diagnostics::default();
401/// let cmap = parse_embedded(program, &Limits::default(), &mut diags);
402///
403/// assert_eq!(cmap.coding_scheme(), CodingScheme::TwoBytes);
404/// assert_eq!(cmap.cid(CharCode(0x20)), Cid(1));
405/// assert_eq!(cmap.cid(CharCode(0x7e)), Cid(0x5F));
406/// assert_eq!(cmap.cid(CharCode(0x7f)), Cid(0));   // outside the range
407/// ```
408#[must_use]
409pub fn parse_embedded(bytes: &[u8], limits: &Limits, diags: &mut Diagnostics) -> CMap {
410    let parsed = parser::parse(bytes, limits, diags);
411    // The `usecmap` operand names a built-in CMap. That is the whole reachable
412    // set: a program can only name what the consumer can find without the
413    // document, and pdf.js draws the same line — `createBuiltInCMap`
414    // (cmap.js:672-680) throws on a name outside `BUILT_IN_CMAPS`. So the
415    // parent chain is the built-in tables' own `use_offset` chain, which
416    // `static_lookup::MAX_CHAIN` already bounds; there is no unbounded
417    // recursion to guard on this channel. The `/UseCMap` *dictionary* key can
418    // name a stream, and `inherit_from` carries the depth guard for it.
419    let inherited = parsed.use_cmap.as_deref().and_then(|name| {
420        let cmap = predefined(&Name::from(predefined::strip_slash(name)))?;
421        Some(Box::new(cmap))
422    });
423    if parsed.use_cmap.is_some() && inherited.is_none() {
424        diags.record(Severity::Suspicious, DiagKind::CMapUsecmapUnknown, None);
425    }
426    // §9.7.5.3: a program that declares no codespace range of its own reads
427    // codes the way its parent does. pdf.js copies the parent's ranges under
428    // exactly this condition (`extendCMap`, cmap.js:653-659).
429    let decoder = match &inherited {
430        Some(parent) if !parsed.declared_codespace => parent.decoder.clone(),
431        _ => parsed.decoder,
432    };
433    CMap {
434        decoder,
435        map: CidMap::Embedded {
436            direct: parsed.direct,
437            additional: parsed.additional,
438        },
439        vertical: parsed.vertical,
440        loaded: true,
441        charset: parsed.charset,
442        coding: CidCoding::Unknown,
443        inherited,
444    }
445}
446
447/// Attach the parent a CMap stream's `/UseCMap` key names (ISO 32000-1
448/// §9.7.5.3), superseding whatever a `usecmap` operator inside the program
449/// named.
450///
451/// Takes an already-built parent rather than a name because the key may name a
452/// stream and only the caller has the resolver. `depth` is the caller's
453/// recursion depth: at or past [`Limits::max_name_tree_depth`] the parent is
454/// dropped and a diagnostic recorded, so a `/UseCMap` naming its own stream
455/// terminates. Codespace ranges are not re-inherited here — `parse_embedded`
456/// has already settled the decoder.
457///
458/// ```
459/// use pdfrum_cmap::{CharCode, Cid, inherit_from, parse_embedded, predefined};
460/// use pdfrum_common::{Diagnostics, Limits};
461/// use pdfrum_object::Name;
462///
463/// let limits = Limits::default();
464/// let mut diags = Diagnostics::default();
465/// let child = parse_embedded(
466///     b"begincodespacerange <00> <ff> endcodespacerange
467///       1 begincidchar <41> 7 endcidchar",
468///     &limits,
469///     &mut diags,
470/// );
471/// let parent = predefined(&Name::from("Identity-H")).unwrap();
472/// let child = inherit_from(child, parent, 0, &limits, &mut diags);
473///
474/// assert_eq!(child.cid(CharCode(0x41)), Cid(7));      // the child's own
475/// assert_eq!(child.cid(CharCode(0x42)), Cid(0x42));   // inherited
476/// ```
477#[must_use]
478pub fn inherit_from(
479    mut cmap: CMap,
480    parent: CMap,
481    depth: u32,
482    limits: &Limits,
483    diags: &mut Diagnostics,
484) -> CMap {
485    if depth >= limits.max_name_tree_depth {
486        diags.record(Severity::Suspicious, DiagKind::CMapUsecmapDepth, None);
487        return cmap;
488    }
489    cmap.inherited = Some(Box::new(parent));
490    cmap
491}
492
493/// The Unicode scalar a character collection assigns to a CID
494/// (ISO 32000-1 §9.10.2), or `None` when it assigns none.
495///
496/// [`CidSet::Unicode`] is the identity map. CID 0 is U+FFFD in every built-in
497/// table, so an unmapped character code extracts as the replacement character.
498///
499/// ```
500/// use pdfrum_cmap::{Cid, CidSet, unicode_from_cid};
501///
502/// assert_eq!(unicode_from_cid(CidSet::Gb1, Cid(0)), Some('\u{FFFD}'));
503/// assert_eq!(unicode_from_cid(CidSet::Gb1, Cid(34)), Some('A'));
504/// assert_eq!(unicode_from_cid(CidSet::Unicode, Cid(0x41)), Some('A'));
505/// assert_eq!(unicode_from_cid(CidSet::Unknown, Cid(1)), None);
506/// ```
507#[must_use]
508pub fn unicode_from_cid(set: CidSet, cid: Cid) -> Option<char> {
509    cid2unicode::unicode_from_cid(set, cid)
510}
511
512/// Whether a character collection has a built-in CID→Unicode table. Only the
513/// four CJK collections do.
514#[must_use]
515pub fn has_cid2unicode(set: CidSet) -> bool {
516    cid2unicode::has_table(set)
517}
518
519/// The character code that would draw `unicode` through this CMap, or
520/// [`CharCode(0)`](CharCode) when none would.
521///
522/// A linear scan of the collection's whole CID→Unicode table looking for a
523/// match, then a reverse table lookup — expensive, and only used when a CID
524/// font is asked to draw a character it can only identify by Unicode.
525///
526/// ```
527/// use pdfrum_cmap::{CharCode, charcode_from_unicode, predefined};
528/// use pdfrum_object::Name;
529///
530/// let gb = predefined(&Name::from("GB-EUC-H")).unwrap();
531/// // U+0020 is CID 0x1E24 in Adobe-GB1, which GB-EUC-H reaches from code 0x20.
532/// assert_eq!(charcode_from_unicode(&gb, ' '), CharCode(0x20));
533/// ```
534#[must_use]
535pub fn charcode_from_unicode(cmap: &CMap, unicode: char) -> CharCode {
536    let Some(reg) = cmap.charset.registry_index() else {
537        return CharCode(0);
538    };
539    let want = u32::from(unicode);
540    let len = blob::cid2unicode_len(reg);
541    for cid in 0..len {
542        let Ok(cid) = u16::try_from(cid) else { break };
543        if blob::cid2unicode(reg, cid).map(u32::from) == Some(want) {
544            let code = cmap.charcode_from_cid(Cid(cid));
545            if code.0 != 0 {
546                return code;
547            }
548        }
549    }
550    CharCode(0)
551}
552
553/// The character collection a `/CIDSystemInfo`'s `/Ordering` names
554/// (ISO 32000-1 §9.7.3).
555///
556/// The five recognised spellings are `GB1`, `CNS1`, `Japan1`, `Korea1` and
557/// `UCS` — note `UCS`, not `UCS2` and not `Identity`. Anything else is
558/// [`CidSet::Unknown`].
559///
560/// ```
561/// use pdfrum_cmap::{CidSet, charset_from_ordering};
562///
563/// assert_eq!(charset_from_ordering(b"Korea1"), CidSet::Korea1);
564/// assert_eq!(charset_from_ordering(b"UCS"), CidSet::Unicode);
565/// assert_eq!(charset_from_ordering(b"UCS2"), CidSet::Unknown);
566/// assert_eq!(charset_from_ordering(b"Identity"), CidSet::Unknown);
567/// ```
568#[must_use]
569pub fn charset_from_ordering(ordering: &[u8]) -> CidSet {
570    match ordering {
571        b"GB1" => CidSet::Gb1,
572        b"CNS1" => CidSet::Cns1,
573        b"Japan1" => CidSet::Japan1,
574        b"Korea1" => CidSet::Korea1,
575        b"UCS" => CidSet::Unicode,
576        _ => CidSet::Unknown,
577    }
578}
579
580#[cfg(test)]
581mod tests;