How pdf-inspector's Selective OCR Routing for Page Selection Works

pdf-inspector routes pages to its OCR engine through a three-mode routing system in src/vision/routing.rs that balances automatic detection with explicit user control.

The firecrawl/pdf-inspector repository implements intelligent page selection for OCR through a dedicated routing module. Its route_ocr_pages function determines exactly which pages get sent to the vision pipeline based on analyzer recommendations, user preferences, and operational mode. This article breaks down the complete routing logic, validation rules, and implementation details found in the source code.

Core Routing Function and Parameters

The route_ocr_pages function serves as the entry point for all OCR page selection decisions. Located at lines 31-59 of src/vision/routing.rs, it accepts four parameters:

Parameter Type Purpose
mode OcrMode Operational mode: Off, Auto, or Force
page_count u32 Total pages in the document
recommended_pages &[u32] Detector-flagged pages likely needing OCR
selected_pages Option<&[u32]> Optional user-supplied whitelist (e.g., CLI --pages)

The function returns a Result<Vec<u32>, OcrRoutingError> containing the final page list in ascending order, or an error if validation fails.

The Three OCR Routing Modes

Off Mode: Complete OCR Disablement

When OcrMode::Off is specified, the function immediately returns an empty vector:

OcrMode::Off => Ok(Vec::new())

This mode guarantees zero OCR overhead—no models load, no rendering occurs.

Auto Mode: Detector-Driven Selection with Optional Filtering

Auto is the default and most sophisticated mode. The routing logic at lines 40-49 implements a two-stage filter:

let mut routed = validated_page_set("recommended", recommended_pages, page_count)?;
if let Some(selected) = selected_pages {
    let selected = validated_page_set("selected", selected, page_count)?;
    routed.retain(|page| selected.contains(page));
}
Ok(routed.into_iter().collect())

The process works as follows:

  1. Validate and deduplicate the detector's recommended pages
  2. Intersect with user-selected pages if a whitelist exists
  3. Return the filtered set in sorted order

This design lets users override detector recommendations without manually inspecting each page.

Force Mode: Explicit User Control

Force mode bypasses detector recommendations entirely (lines 51-56):

let routed = selected_pages
    .map(|pages| validated_page_set("selected", pages, page_count))
    .transpose()?
    .unwrap_or_else(|| (1..=page_count).collect());
Ok(routed.into_iter().collect())

Behavior depends on selected_pages:

Scenario Result
Whitelist provided OCR runs on validated whitelist only
No whitelist (None) OCR runs on all pages (1..=page_count)

Page Validation with validated_page_set

Both modes rely on validated_page_set (lines 75-89 of src/vision/routing.rs) to sanitize inputs. This helper function enforces two critical constraints:

  • Zero-page rejection: PDF pages are 1-indexed; 0 triggers InvalidPage
  • Range enforcement: Pages exceeding page_count trigger InvalidPage

Errors include source context ("recommended" vs "selected") and the offending page number for precise debugging.

Integration with the OCR Pipeline

After routing completes, the run_ocr_pages function receives the finalized page list. The implementation includes an optimization noted at lines 65-66: empty lists prevent engine instantiation, keeping model loading strictly lazy. This matters for performance when processing text-native PDFs that need no OCR.

Complete Code Examples

use pdf_inspector::vision::{
    route_ocr_pages, OcrMode,
};

// Auto mode: detector recommends [2,4,7], user filters to [4,5,7]
let recommended = &[2, 4, 7];
let user_filter = Some(&[4, 5, 7][..]);
let pages = route_ocr_pages(OcrMode::Auto, 10, recommended, user_filter).unwrap();
assert_eq!(pages, vec![4, 7]);

// Force mode with explicit whitelist
let pages = route_ocr_pages(OcrMode::Force, 8, &[], Some(&[1, 3, 5][..])).unwrap();
assert_eq!(pages, vec![1, 3, 5]);

// Force mode without whitelist: OCR every page
let pages = route_ocr_pages(OcrMode::Force, 5, &[], None).unwrap();
assert_eq!(pages, vec![1, 2, 3, 4, 5]);

// Off mode: disable OCR entirely
let pages = route_ocr_pages(OcrMode::Off, 12, &[1, 2], None).unwrap();
assert!(pages.is_empty());

Key Source Files

File Role in OCR Routing
src/vision/routing.rs Core routing logic and validation
src/vision/pipeline.rs Consumes routed pages, executes renderer/OCR
src/lib.rs Public API entry process_pdf_with_options
tests/ocr_tests.rs Unit tests including off_auto_and_force_route_expected_pages

Summary

  • pdf-inspector's selective OCR routing lives in src/vision/routing.rs with route_ocr_pages as the central decision function
  • Three modes cover all use cases: Off (disabled), Auto (detector-guided with optional filtering), Force (user-controlled or full-document)
  • Intersection logic in Auto mode lets users refine detector recommendations without manual page-by-page selection
  • Strict validation prevents 1-indexing errors and out-of-bounds access through validated_page_set
  • Lazy loading ensures OCR engines only initialize when pages actually require processing

Frequently Asked Questions

How does pdf-inspector decide which pages need OCR in Auto mode?

In Auto mode, the system combines two inputs: a detector-supplied list of pages flagged as likely needing OCR, and an optional user whitelist. The function intersects these lists, running OCR only on pages appearing in both. If no whitelist is provided, all detector-recommended pages are processed.

What happens if I specify page numbers that don't exist in the document?

The validated_page_set function rejects invalid page numbers with an OcrRoutingError::InvalidPage error. This catches both zero values (PDFs use 1-based indexing) and pages exceeding the document's total page count, with clear error messages indicating which input source contained the invalid value.

Can I force OCR on every page regardless of content?

Yes. Use OcrMode::Force with selected_pages set to None. According to the source code at lines 53-55, this triggers unwrap_or_else(|| (1..=page_count).collect()), which generates a complete 1-to-N page range. This mode ignores all detector recommendations entirely.

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 →