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

> Discover how pdf-inspector's selective OCR routing balances automatic detection and user control with its three-mode system for efficient page selection.

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

---

**pdf-inspector routes pages to its OCR engine through a three-mode routing system in [`src/vision/routing.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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:

```rust
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:

```rust
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):

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

```rust
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`](https://github.com/firecrawl/pdf-inspector/blob/main/src/vision/routing.rs) | Core routing logic and validation |
| [`src/vision/pipeline.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/vision/pipeline.rs) | Consumes routed pages, executes renderer/OCR |
| [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs) | Public API entry `process_pdf_with_options` |
| [`tests/ocr_tests.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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.