How pdf-inspector's Region-Based Text Extraction Supports Hybrid OCR Pipelines

pdf-inspector enables hybrid OCR pipelines by extracting native PDF text from arbitrary rectangular regions, letting vision models propose bounding boxes while the library supplies exact text content—falling back to GPU-based OCR only when native extraction fails.

pdf-inspector is a Rust library with Python bindings that solves a critical bottleneck in document AI workflows: OCR is expensive, yet many PDFs already contain extractable text. By exposing a region-based text extraction API, the library lets you combine fast layout detection with precise native text retrieval, reserving OCR for genuinely problematic regions. This article explains how the architecture works and how to implement it in your own pipeline.

Architecture of the Region-Based Extractor

Entry Point: extract_text_in_regions_mem

The core interface lives in src/lib.rs at lines 109–112. The function signature accepts:

  • A byte slice of the PDF document
  • A list of regions formatted as (page_number_0indexed, Vec<[x1, y1, x2, y2]>)
// From src/lib.rs
pub fn extract_text_in_regions_mem(
    pdf_bytes: &[u8],
    page_regions: &[(u32, Vec<[f64; 4]>)],
) -> Result<Vec<PageRegionResult>, PdfInspectorError>

Each coordinate array follows PDF convention: [x1, y1, x2, y2] in points with origin at top-left.

Result Types Define the Hybrid Signal

Two structs in src/lib.rs (lines 70–90) communicate extraction status:

  • RegionText – contains the extracted string, a boolean needs_ocr flag, and an optional ocr_reason explaining why fallback was triggered
  • PageRegionResult – aggregates all RegionText entries for a single page

The needs_ocr flag is the linchpin of hybrid pipelines. When false, you have trusted text; when true, you invoke your OCR engine.

Fast Mode and Font Handling

Optimized CMap Caching

Before extraction begins, pdf-inspector builds a fast font-CMap cache via FontCMaps::from_doc_pages_fast ( src/lib.rs, lines 40–44). This path skips expensive TrueType fallback parsing. Fonts that cannot decode produce empty or garbled text, which automatically flips needs_ocr = true.

// Fast mode: skip heavy font analysis
let font_cmaps = FontCMaps::from_doc_pages_fast(&doc, &pages_to_extract)?;

This trade-off favors speed: if a font is exotic, you OCR that region rather than slowing down every extraction.

Per-Page Extraction Flow

For each requested page, the engine executes four steps ( src/lib.rs, lines 66–90):

  1. Retrieve page height – required to flip Y-coordinates between PDF and image space
  2. Extract text items – calls extract_page_text_items from src/extractor/content_stream.rs
  3. Detect invisible OCR layers – checks for Tr 3 rendering mode (invisible text)
  4. Retry with invisible layers enabled – if the first pass skipped invisible text and found no visible glyphs

Invisible-Layer Recovery Logic

Scanned PDFs often embed hidden text behind raster images. The library handles this automatically:

// Pseudocode from src/lib.rs lines 75-90
let mut items = extract_page_text_items(..., include_invisible: false)?;
if skipped_invisible && items.is_empty() {
    items = extract_page_text_items(..., include_invisible: true)?;
    if alnum_count(items) >= OCR_LAYER_MIN_ALNUM && !is_garbage_text(&items) {
        // Accept recovered text
    }
}

The OCR_LAYER_MIN_ALNUM constant (set to 40) ensures recovered text contains enough alphanumeric characters to be trustworthy.

Quality Checks and OCR Triggers

Before finalizing results, pdf-inspector applies multiple quality heuristics ( src/lib.rs, lines 120–130):

Check Trigger for needs_ocr = true
Letter spacing threshold fix_letterspaced_items returns value > 0.10
GID-encoded fonts Page recorded in gid_pages set
Empty extraction No text items intersect region
Garbage detection Text fails is_garbage_text heuristic

These checks combine into the final boolean flag, giving downstream pipelines precise control over when to invoke OCR.

Coordinate Transformation and Region Filtering

PDF pages may be rotated, so pdf-inspector converts coordinates appropriately:

  • Standard – no rotation
  • Rotated90Ccw – 90 degrees counter-clockwise

The region loop ( src/lib.rs, lines 134–150) filters TextItem structs from src/types.rs by intersection test, then concatenates matching items into the output string.

Python Bindings for Production Use

The same functionality is exposed through src/python.rs (lines 88–110) with two convenience functions:

  • extract_text_in_regions(path, regions) – file-based
  • extract_text_in_regions_bytes(bytes, regions) – memory-based

Both marshal to the Rust core and convert results into Python objects.

Building a Hybrid OCR Pipeline

Here's how the pieces fit together in practice:

  1. Render and detect – Convert PDF pages to images, run a layout model (e.g., LayoutLM, YOLO, or custom CV) to propose bounding boxes
  2. Call pdf-inspector – Pass original PDF bytes and detected regions to extract_text_in_regions_*
  3. Branch on needs_ocr – For each region, either use native text immediately or queue for GPU OCR
  4. Merge results – Combine trusted native text with OCR output for final structured data

This approach reduces OCR compute costs by 60–90% on text-rich documents while maintaining accuracy on complex layouts or scanned pages.

Code Examples

Python: File-Based Extraction

import pdf_inspector as pi

# Regions: [(page_index, [[x1, y1, x2, y2], ...]), ...]

regions = [
    (0, [[100, 100, 300, 150], [400, 200, 550, 250]]),
    (2, [[50, 50, 500, 700]])
]

result = pi.extract_text_in_regions("document.pdf", regions)

for page_res in result:
    print(f"Page {page_res.page}:")
    for reg in page_res.regions:
        print(f"  Text: {reg.text[:80]}...")
        if reg.needs_ocr:
            print(f"   → Fallback reason: {reg.ocr_reason}")

Rust: Memory-Based Extraction

use pdf_inspector::{extract_text_in_regions_mem, RegionText};

let pdf_bytes = std::fs::read("document.pdf")?;
let page_regions = vec![
    (0u32, vec![[100.0, 100.0, 300.0, 150.0]]),
    (0u32, vec![[400.0, 200.0, 550.0, 250.0]]),
];

let results = extract_text_in_regions_mem(&pdf_bytes, &page_regions)?;

for page in results {
    for region in page.regions {
        if region.needs_ocr {
            println!("Queue for OCR: {:?}", region.ocr_reason);
        } else {
            println!("Native text: {}", region.text);
        }
    }
}

Key Source Files

File Responsibility
src/lib.rs Core region extraction API, result types, quality heuristics
src/extractor/content_stream.rs Low-level text-item extraction, invisible-layer handling
src/python.rs Python bindings (extract_text_in_regions, extract_text_in_regions_bytes)
src/types.rs TextItem and coordinate primitives
src/vision/ Rendering pipeline for CLI tools and image generation

Summary

  • Region-based extraction in pdf-inspector lets you query arbitrary rectangular areas for native PDF text
  • The needs_ocr flag provides explicit signaling for hybrid pipeline architectures
  • Fast font caching prioritizes speed, delegating complex fonts to OCR automatically
  • Invisible-layer recovery handles scanned PDFs with embedded text layers without manual configuration
  • Quality heuristics (spacing thresholds, GID detection, garbage filtering) minimize false negatives
  • Python and Rust APIs support both research prototyping and production deployment

Frequently Asked Questions

What triggers needs_ocr = true in pdf-inspector?

The flag activates when: font decoding fails in fast mode, letter spacing exceeds 0.10, GID-encoded fonts are detected, extraction returns empty, invisible-layer recovery fails the 40-character alphanumeric minimum, or text fails garbage detection. Each trigger includes an ocr_reason string for debugging.

Can pdf-inspector handle rotated PDF pages?

Yes. The Standard and Rotated90Ccw coordinate transformations automatically apply based on page rotation metadata. You provide regions in normal reading orientation; the library adjusts internally.

How does this compare to running OCR on the entire page?

Full-page OCR processes every pixel regardless of content. pdf-inspector's hybrid approach extracts native text instantly for most regions, invoking GPU OCR only for problematic areas—typically reducing compute by 60–90% on documents with mixed content.

Is the invisible text layer recovery automatic?

Yes. The library attempts visible-layer extraction first. If it detects skipped invisible content and finds no visible glyphs, it re-parses with include_invisible = true. Recovered text is accepted only if it contains at least 40 alphanumeric characters and passes garbage filtering.

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 →