How Font Width Tables, CMapDecisionCache, and TrueType CMap Fallback Contribute to Font Decoding in PDF-Inspector

Font decoding in PDF-Inspector relies on three coordinated mechanisms: font width tables calculate glyph positioning, CMapDecisionCache persists font-to-character mapping decisions, and TrueType CMap fallback recovers Unicode mappings when primary CMaps are missing.

The firecrawl/pdf-inspector architecture handles PDF text extraction by resolving raw glyph identifiers into readable, properly spaced Unicode characters. Understanding how these three components interact is essential for debugging extraction failures or extending the library's font support.

Font Width Tables: Computing Visual Layout

PDF documents encode text as glyph IDs rather than characters, with positioning determined by advance widths stored in each font. The font width table—implemented in src/extractor/fonts.rs—maps each glyph ID to its horizontal advance measurement.

When the content stream interpreter encounters a text rendering operation, it:

  1. Reads the font object referenced by the current graphics state
  2. Builds a width table by parsing the /Widths array or embedded font program
  3. Uses these widths to compute cursor advances between glyphs

This enables accurate reconstruction of the original visual layout even when the PDF lacks explicit space characters. The extractor can infer word boundaries by detecting width gaps exceeding threshold values.

// src/extractor/fonts.rs – simplified width table construction
let width_table = font.build_width_table()?;   // Parses /Widths or font program

// During content stream interpretation
for gid in glyph_ids {
    let advance = width_table.width_of(gid);   // Points for horizontal movement
    cursor += advance;
    // Position-aware extraction enables table detection, column analysis
}

Without width tables, extracted text would collapse into unbroken character sequences, destroying all structural information.

CMapDecisionCache: Optimizing Character Mapping Decisions

Every font in a PDF may specify character-to-glyph mappings through multiple potential sources: an embedded ToUnicode CMap, a CIDSystemInfo dictionary, or the font's internal tables. Determining which source to use requires inspecting font properties that remain constant across the document.

The CMapDecisionCache—defined in src/extractor/fonts.rs—eliminates redundant computation by storing the resolved CMapSource for each unique font object:

// src/extractor/fonts.rs – CMapDecisionCache usage pattern
let mut cmap_decisions = CMapDecisionCache::new();

// First encounter: evaluates all mapping sources, stores decision
let cmap_src = cmap_decisions.decide(&font, |f| {
    evaluation_logic(f)  // Checks ToUnicode, encoding, font type
});

// Subsequent glyphs: O(1) cache retrieval
let cached_src = cmap_decisions.get(&font).unwrap();

Caching provides two critical benefits:

  • Performance: Complex font inspection (parsing CMap streams, analyzing TrueType tables) occurs once per font rather than once per glyph
  • Consistency: All glyphs from the same font use identical mapping logic, preventing mixed-encoding artifacts

The cache key typically incorporates the font object's indirect reference and generation number, ensuring distinct cached decisions for distinct font dictionaries even when they share underlying font programs.

TrueType CMap Fallback: Recovering Missing Mappings

PDFs frequently omit ToUnicode CMaps—particularly for embedded subsets or legacy documents—leaving glyph IDs without Unicode equivalents. When CMapDecisionCache detects an absent or incomplete ToUnicode mapping, it triggers the TrueType CMap fallback path.

The fallback mechanism extracts and parses the cmap table from embedded TrueType or OpenType font data:

// src/extractor/fonts.rs – TrueType CMap fallback integration
let cmap = cmap_decisions.get_or_compute(font, |f| {
    if let Some(to_unicode) = f.to_unicode_cmap() {
        CMapSource::ToUnicode(to_unicode)           // Preferred: PDF-provided mapping
    } else if let Some(tt_cmap) = f.true_type_cmap() {
        CMapSource::TrueType(tt_cmap)               // Fallback: font-internal cmap table
    } else {
        CMapSource::Heuristic(f.build_heuristic_map()) // Last resort: encoding guess
    }
});

The TrueType cmap table provides platform-independent Unicode mappings that are often more reliable than PDF encoding declarations. This fallback enables successful text extraction from:

  • PDF/A-1b documents with embedded font subsets lacking ToUnicode
  • Legacy PDFs using non-ASCII encodings without proper specification
  • Documents with incorrectly constructed ToUnicode CMaps

When even TrueType cmap is unavailable—such as with Type 1 fonts or corrupted font programs—the system degrades gracefully to heuristic encoding detection based on character frequency analysis and declared font encodings.

Integration in the Extraction Pipeline

These three mechanisms operate sequentially during content stream processing in src/extractor/content_stream.rs:

  1. Font selection: The graphics state identifies the current font object
  2. Decision retrieval: CMapDecisionCache provides (or computes) the appropriate CMapSource
  3. Glyph decoding: Each glyph ID maps to Unicode through the selected source
  4. Width application: Font width tables calculate positioning for layout preservation
// src/extractor/content_stream.rs – text extraction integration
fn extract_text_segment(&mut self, font_ref: FontRef, glyphs: &[GlyphId]) -> TextSegment {
    let font = self.resources.get_font(font_ref);
    let width_table = font.width_table();
    let cmap = self.cmap_cache.get_or_compute(font, evaluate_sources);
    
    let mut segment = TextSegment::new();
    let mut position = 0.0;
    
    for gid in glyphs {
        let unicode = cmap.decode(*gid)
            .unwrap_or_else(|| self.fallback_char(gid));
        let advance = width_table.width_of(*gid);
        
        segment.push_char(unicode, position);
        position += advance;
    }
    
    segment
}

The src/extractor/xobjects.rs module handles font object instantiation and passes fully constructed Font structures to this pipeline, while src/tounicode.rs implements the primary ToUnicode CMap parser used before fallback evaluation.

Summary

  • Font width tables enable accurate text positioning and layout reconstruction by providing per-glyph advance measurements parsed from /Widths arrays or embedded font programs in src/extractor/fonts.rs
  • CMapDecisionCache eliminates redundant font analysis by persisting mapping source decisions per font object, improving both performance and output consistency
  • TrueType CMap fallback recovers Unicode mappings when PDF-provided ToUnicode CMaps are absent, extracting the cmap table from embedded TrueType font data as a reliable secondary source

Frequently Asked Questions

How does PDF-Inspector handle fonts without any embedded CMap?

When neither ToUnicode nor TrueType cmap tables are available, the system falls back to heuristic encoding detection based on declared font encodings, character frequency analysis, and byte distribution patterns. This occurs in the final branch of the CMapSource selection logic within src/extractor/fonts.rs.

Why cache CMap decisions rather than parsing them per glyph?

Font CMap structures can span thousands of entries and require stream decompression. Parsing once per font versus once per glyph reduces processing time from O(N × M) to O(N + M) where N represents glyphs and M represents CMap complexity. The cache also prevents inconsistent mapping if multiple CMap sources would yield different results for the same glyph ID.

What limitations exist for TrueType CMap fallback?

TrueType cmap tables only provide mappings for glyphs actually present in the embedded font subset. If the PDF creator subsetted the font aggressively, uncommon characters may lack cmap entries despite appearing in the original typeface. Additionally, platform-specific cmap variants (Macintosh vs. Windows) may encode the same glyph ID to different Unicode code points, requiring platform detection heuristics.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →