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;