Handling CID Fonts with ToUnicode CMap Decoding in pdf-inspector: A Complete Guide
pdf-inspector extracts Unicode text from CID-encoded PDF fonts by parsing /ToUnicode streams with automatic fallback to TrueType cmap tables, glyph names, and built-in CMaps when mapping data is missing or incomplete.
The firecrawl/pdf-inspector repository implements a robust multi-layer strategy for decoding CID fonts (Character Identifier fonts) commonly found in PDF documents. CID fonts use numeric identifiers rather than direct Unicode codepoints, making ToUnicode CMap decoding essential for accurate text extraction. This article explains the internal pipeline, fallback mechanisms, and practical implementation details drawn directly from the source code.
What Are CID Fonts and ToUnicode CMaps?
CID fonts are font formats where each glyph is referenced by a Character Identifier (CID)—a 16-bit number—rather than a character name or Unicode value. When you open a PDF containing Chinese, Japanese, or Korean text, or certain composite Latin fonts, you're almost certainly dealing with CID fonts.
The ToUnicode CMap is a PDF object that maps these CIDs to actual Unicode strings. Without it, raw CID values are meaningless to text extraction tools. According to the pdf-inspector source, the core decoding logic resides in src/tounicode.rs, orchestrated by the font extraction layer in src/extractor/fonts.rs.
The Six-Stage ToUnicode Pipeline
The pdf-inspector crate processes CID font decoding through a carefully ordered pipeline. Each stage attempts to build a valid CID-to-Unicode mapping, with subsequent stages serving as fallbacks.
| Stage | Function | Purpose | Trigger Condition |
|---|---|---|---|
| 1 | ToUnicodeCMap::parse |
Parse explicit /ToUnicode stream |
/ToUnicode key present in font dictionary |
| 2 | build_cmap_from_truetype |
Extract cmap table from embedded TrueType font | ToUnicode sparse or missing; TrueType data available |
| 3 | build_cmap_from_glyph_names |
Convert glyph names via Adobe Glyph List | TrueType cmap unavailable; post table present |
| 4 | build_cmap_from_builtin_cmap |
Load legacy CID→Unicode binary tables | CIDSystemInfo matches known ordering (e.g., Adobe-Identity) |
| 5 | merge_cmaps |
Combine primary map with richer fallback | Overlay contains mappings missing from base |
| 6 | remap_to_sequential |
Reorder CIDs to sequential space | Simplifies downstream layout processing |
Stage 1: Parsing Raw ToUnicode Streams
The entry point for explicit ToUnicode data is ToUnicodeCMap::parse at lines 103-110 of src/tounicode.rs:
pub fn parse(data: &[u8]) -> Result<ToUnicodeCMap, String> {
let mut cmap = ToUnicodeCMap::new();
// … parsing logic …
Ok(cmap)
}
This function decompresses the stream, handles CMap syntax (begincidchar, beginbfchar, etc.), and populates a ToUnicodeCMap struct containing the CID→Unicode mappings.
Stage 2: TrueType cmap Fallback
When the ToUnicode stream is missing or incomplete, pdf-inspector examines the embedded font program. The build_cmap_from_truetype function (lines 944-952) uses the ttf_parser crate to read the TrueType cmap table:
pub fn build_cmap_from_truetype(font_data: &[u8]) -> Option<ToUnicodeCMap> {
let mut cmap = ToUnicodeCMap::new();
// read the TrueType `cmap` table via `ttf_parser`
// … populate `cmap` …
Some(cmap)
}
This often succeeds where PDF ToUnicode fails, since font embedding is mandatory for subset CID fonts.
Stage 3: Glyph Name Resolution
For fonts with neither ToUnicode nor usable TrueType cmap data, the extractor falls back to glyph names from the TrueType post table, converting them via the Adobe Glyph List:
fn build_cmap_from_glyph_names(face: &ttf_parser::Face<'_>) -> Option<ToUnicodeCMap> {
let mut cmap = ToUnicodeCMap::new();
// map glyph names → Unicode using `adobe_glyphlist`
// …
Some(cmap)
}
This handles older PDFs and certain technical drawing fonts where glyph naming conventions follow Adobe standards.
Stage 4: Built-in Binary CMaps
Legacy CID fonts reference CIDSystemInfo dictionaries with /Ordering values like Adobe-Japan1 or Adobe-Identity. The build_cmap_from_builtin_cmap function loads pre-compiled binary CMaps shipped with the crate for these known orderings.
Stage 5 & 6: Merging and Remapping
The merge_cmaps function combines a sparse primary CMap with a richer fallback:
fn merge_cmaps(base: ToUnicodeCMap, overlay: ToUnicodeCMap) -> ToUnicodeCMap
remap_to_sequential optionally reorders the CID space to eliminate gaps, simplifying the text layout engine's job.
How the Font Extractor Orchestrates CMap Resolution
The src/extractor/fonts.rs module coordinates all CMap building. At lines 340-380, the resolve_font_cmap function traverses each font dictionary:
pub fn resolve_font_cmap(font_dict: &Dict, resources: &Resources) -> CharMap {
if let Some(cmap) = font_dict.get(b"ToUnicode") {
// parse the stream
let parsed = ToUnicodeCMap::parse(&cmap_data)?;
// possibly merge with a fallback
let final_map = merge_cmaps(parsed, fallback);
char_map.insert(font_id, final_map);
} else {
// use TrueType or builtin fallback
let fallback = build_cmap_from_truetype(...).or_else(...);
}
// …
}
The returned CharMap is consumed by the text extraction engine to convert raw character codes to Unicode strings during page content parsing.
OCR Flagging for Unrecoverable Fonts
When no mapping can be constructed—common with Identity-H fonts lacking any usable cmap—the extractor marks the page as needing OCR. This logic appears around line 1594 in tounicode.rs, with downstream consumers in detector.rs surfacing the flag to users.
Handling Edge Cases and Broken PDFs
The pdf-inspector codebase contains specific mitigations for real-world PDF problems:
- Sparse ToUnicode streams: Logged warning at line 48; TrueType cmap preferred when richer
- Broken subset fonts: Detection and repair logic at line 851 ("detect and fix broken ToUnicode CMaps")
- Identity-H/V without mapping: Forced fallback through the three-stage cascade rather than emitting empty text
Practical Usage: Extracting Text from CID Font PDFs
Below is a complete Rust example using pdf-inspector's public API. The ToUnicode decoding happens automatically during process_pdf:
use pdf_inspector::{process_pdf, MarkdownProfile};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Path to a PDF that uses CID (Identity‑H) fonts
let pdf_path = "samples/cid-fonts.pdf";
// Process the PDF with the default profile (fidelity)
let result = process_pdf(pdf_path)?;
// `result` contains a vector of `TextLine` items already decoded via ToUnicode CMaps
for line in result.items.iter() {
println!("{}", line.text);
}
Ok(())
}
The process_pdf function delegates to the extractor, which runs the full CMap resolution pipeline described above. Unicode text extraction succeeds even when the original PDF lacks explicit ToUnicode data.
Key Source Files Reference
| File | Role | Key Symbols |
|---|---|---|
src/tounicode.rs |
Core parser, fallback implementations, merging | ToUnicodeCMap::parse, build_cmap_from_truetype, merge_cmaps |
src/extractor/fonts.rs |
Font dictionary traversal, CMap selection | resolve_font_cmap, CharMap |
src/lib.rs |
Public API entry points | process_pdf, extract_text |
tests/integration_tests.rs |
CID font validation, OCR flag testing | Integration test cases |
Summary
src/tounicode.rsimplements the complete ToUnicode CMap decoding pipeline with four fallback strategies for robust CID font handlingToUnicodeCMap::parseprocesses explicit/ToUnicodestreams;build_cmap_from_truetypeextracts mappings from embedded font programsresolve_font_cmapinsrc/extractor/fonts.rsorchestrates fallback selection and exposesCharMapto the extraction engine- Unrecoverable mappings trigger OCR flags rather than silent failure, ensuring users know when automated extraction failed
- The public
process_pdfAPI automatically runs all decoding logic—no manual CMap handling required for typical use
Frequently Asked Questions
What happens if a PDF has CID fonts but no ToUnicode entry?
The extractor attempts three fallback strategies in order: TrueType cmap table extraction, glyph name resolution via Adobe Glyph List, and built-in binary CMaps for known CIDSystemInfo orderings. If all fail, the font is flagged for OCR rather than returning empty or garbage text.
Why does Identity-H sometimes fail to extract text while other CID fonts work?
Identity-H fonts map CIDs directly to glyph IDs without a predefined character collection. Without an explicit ToUnicode CMap or embedded TrueType cmap, there's no semantic mapping available. The pdf-inspector detector specifically identifies this case (around line 1594 in tounicode.rs) and triggers OCR fallback.
Can I manually inspect the CMap selected for a specific font?
Currently, the CharMap resolution happens internally within resolve_font_cmap. For debugging, enable logging at the DEBUG level—pdf-inspector logs CMap source selection and fallback activation. The ToUnicodeCMap struct is not yet exposed in the public API, though internal unit tests in tests/integration_tests.rs demonstrate direct inspection patterns.
How does the crate handle corrupted or partial ToUnicode streams?
The parser detects sparse mappings and logs warnings (line 48). It then prefers TrueType cmap data when available, as embedded fonts typically contain more complete mappings than PDF ToUnicode streams. For broken subset fonts with mismatched GIDs, reconstruction logic at line 851 attempts to rebuild a valid mapping from available glyph data.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →