PDF-Inspector OCR Reasons: Complete Guide to When and Why OCR Triggers

PDF-Inspector triggers OCR when native text extraction produces unreliable or unreadable characters, with seven specific OCR reasons defined in src/vision/pipeline.rs that determine per-page OCR necessity.

The pdf-inspector repository by Firecrawl provides intelligent PDF text extraction that automatically detects when Optical Character Recognition (OCR) is required. Understanding these OCR reasons helps developers debug extraction failures and optimize processing pipelines.

What Are OCR Reasons in PDF-Inspector?

OCR reasons are categorical flags that explain why a specific PDF page cannot be reliably processed through native text extraction alone. These reasons are collected in a Vec<PageOcrReasons> structure and exposed through both the Rust API and language bindings (Python, Node.js, WASM).

The decision logic resides in src/vision/pipeline.rs, while the public data structures are defined in src/lib.rs (PageOcrReasons at lines 128–149).

The Seven OCR Reasons Explained

pdf-inspector defines seven distinct OCR reasons as constants in the vision pipeline. Each reason corresponds to a specific failure mode in native PDF text extraction.

OCR_REASON_SUSPECTED_GARBLED_TEXT

Trigger: The decoded text contains a very low proportion of alphanumeric characters or excessive replacement symbols.

Cause: Custom CMaps, shifted-cipher fonts, or corrupted font encodings produce unreadable glyphs that look like text but aren't meaningful.

Example: A PDF using non-standard encoding where "Hello" appears as random Unicode private-use characters.

OCR_REASON_IDENTITY_H_NO_TOUU

Trigger: A font uses the Identity-H cmap without a ToUnicode CMap entry.

Cause: Identity-H maps glyph IDs directly to 16-bit Unicode values, but without ToUnicode, the extractor cannot reverse this mapping to obtain actual text.

Impact: Zero extractable text from affected regions, forcing OCR fallback.

OCR_REASON_SHIFTED_CIPHER

Trigger: Font character codes show systematic offset patterns (constant shift) producing nonsensical text.

Cause: Intentional or accidental glyph ID shifting in the font's encoding vector.

Detection: The extractor identifies systematic mismatch between expected and actual character values.

OCR_REASON_ZERO_AREA

Trigger: A detected text region has zero width or height.

Cause: Placeholder regions, malformed bounding boxes, or image-mounted text layers.

Behavior: Pipeline treats these as OCR candidates since they cannot contain genuine extractable text.

OCR_REASON_NON_EXISTENT_PAGE

Trigger: A region references a page number not present in the PDF's page tree.

Cause: Corrupted page tree structures or malformed PDFs with dangling references.

Response: OCR runs on the entire page to ensure no content is missed.

OCR_REASON_LOW_ALPHANUMERIC_RATIO

Trigger: Fewer than 50% of characters on a page are alphanumeric after initial scan.

Heuristic: Flags pages likely to be scanned images, diagrams, or heavily encoded content.

Threshold: The 50% ratio is configurable in the pipeline logic.

OCR_REASON_WATERMARK_ONLY

Trigger: Page consists solely of a raster watermark without extractable text elements.

Example: A "CONFIDENTIAL" stamp across an otherwise empty or image-based page.

Pipeline behavior: OCR is offered to recover any hidden or embed text beneath the watermark.

Accessing OCR Reasons in Code

Rust API

use pdf_inspector::{process_pdf_with_options, PdfOptions, OcrOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pdf = std::fs::read("document.pdf")?;
    let opts = PdfOptions::default()
        .ocr(OcrOptions::new().auto());  // Enable auto-detect
    
    let result = process_pdf_with_options(&pdf, opts)?;
    
    println!("Pages needing OCR: {:?}", result.pages_needing_ocr);
    
    for reason in &result.ocr_reasons_by_page {
        println!("Page {}: {:?}", reason.page, reason.reasons);
    }
    
    Ok(())
}

Python Binding

from pdf_inspector import PdfOptions, OcrOptions, process_pdf

with open("scanned_document.pdf", "rb") as f:
    pdf_bytes = f.read()

options = PdfOptions().ocr(OcrOptions().auto())
result = process_pdf(pdf_bytes, options)

print(f"Pages flagged: {result.pages_needing_ocr}")

for page_reasons in result.ocr_reasons_by_page:
    print(f"Page {page_reasons.page}: {page_reasons.reasons}")

CLI Output

The pdf2md CLI tool (in src/bin/pdf2md.rs) formats OCR reasons through the format_ocr_reasons_by_page function:

$ pdf2md --ocr auto input.pdf
Processing page 1... OK
Processing page 2... OCR triggered [Suspected garbled native text, Missing ToUnicode]
Processing page 3... OK

Key Source Files

File Purpose Key Elements
src/vision/pipeline.rs OCR decision engine Reason constants, detection logic
src/lib.rs Public API structures PageOcrReasons, PdfProcessResult
src/bin/pdf2md.rs Command-line interface format_ocr_reasons_by_page
docs/ocr-runtime.md Runtime behavior docs Configuration, thresholds

How OCR Reasons Flow Through the Pipeline

  1. Extraction attempt: Native text extraction runs on each page
  2. Quality analysis: Pipeline evaluates character ratios, font mappings, and region geometry
  3. Reason accumulation: Failing checks add corresponding OCR reasons to PageOcrReasons
  4. Decision point: Pages with any reasons are queued for OCR processing
  5. Result assembly: PdfProcessResult contains both extracted text and ocr_reasons_by_page

Summary

  • pdf-inspector defines seven OCR reasons in src/vision/pipeline.rs that explain why native extraction fails
  • Reasons range from encoding problems (OCR_REASON_IDENTITY_H_NO_TOUU, OCR_REASON_SHIFTED_CIPHER) to content heuristics (OCR_REASON_LOW_ALPHANUMERIC_RATIO, OCR_REASON_WATERMARK_ONLY)
  • The PageOcrReasons struct in src/lib.rs exposes these through multiple API layers
  • Auto-detect mode uses these reasons to minimize unnecessary OCR while ensuring text recovery
  • Understanding these reasons helps optimize PDF processing pipelines and debug extraction quality issues

Frequently Asked Questions

How do I disable OCR entirely in pdf-inspector?

Pass OcrOptions::new().disabled() or equivalent in your language binding. The pipeline will skip OCR even if reasons are detected, though garbled text may result.

Can I customize the 50% alphanumeric threshold?

Not directly through the public API. The threshold is hardcoded in src/vision/pipeline.rs. Forking and modifying the constant requires recompilation.

What's the difference between OCR_REASON_SUSPECTED_GARBLED_TEXT and OCR_REASON_SHIFTED_CIPHER?

SUSPECTED_GARBLED_TEXT detects general low-quality output with many replacement characters, while SHIFTED_CIPHER specifically identifies systematic offset patterns in character codes. Both trigger OCR, but the latter indicates a specific encoding strategy.

Do OCR reasons appear in the final Markdown output?

No. OCR reasons are metadata in PdfProcessResult. They inform processing decisions but don't appear in extracted text. Log them separately for debugging or pipeline monitoring.

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 →