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:
- Validate and deduplicate the detector's recommended pages
- Intersect with user-selected pages if a whitelist exists
- 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;
0triggersInvalidPage - Range enforcement: Pages exceeding
page_counttriggerInvalidPage
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.rswithroute_ocr_pagesas 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →