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

> pdf-inspector enhances hybrid OCR pipelines by extracting native PDF text from specific regions, using vision models for bounding boxes and falling back to GPU OCR only when necessary.

- Repository: [Firecrawl/pdf-inspector](https://github.com/firecrawl/pdf-inspector)
- Tags: deep-dive
- Published: 2026-09-02

---

**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`](https://github.com/firecrawl/pdf-inspector/blob/main/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]>)`

```rust
// 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`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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`.

```rust
// 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`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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:

```rust
// 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`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs)**, lines 134–150) filters `TextItem` structs from **[`src/types.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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

```python
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

```rust
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`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs)** | Core region extraction API, result types, quality heuristics |
| **[`src/extractor/content_stream.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/content_stream.rs)** | Low-level text-item extraction, invisible-layer handling |
| **[`src/python.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/python.rs)** | Python bindings (`extract_text_in_regions`, `extract_text_in_regions_bytes`) |
| **[`src/types.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/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.