How `extract_pages_markdown_mem` Enables Per-Page Extraction with Hybrid OCR Routing in PDF Inspector
extract_pages_markdown_mem is the core API in firecrawl/pdf-inspector that returns individual page Markdown with granular OCR flags, using the same classifier signals as the document-level pipeline to route pages through OCR when text quality is unreliable.
The firecrawl/pdf-inspector Rust crate provides two primary extraction paths: process_pdf_mem for simple full-document strings, and extract_pages_markdown_mem for per-page results with hybrid OCR routing. This article explains how the latter implements per-page extraction and hybrid OCR routing— ensuring that extraction decisions stay synchronized with the document classifier.
What extract_pages_markdown_mem Returns
Unlike the single-string API, extract_pages_markdown_mem returns a PagesExtractionResult containing a Vec<PageMarkdown> in [src/lib.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs#L8-L20). Each PageMarkdown struct includes:
page: Zero-based page indexmarkdown: Extracted Markdown content for that pageneeds_ocr: Boolean flag indicating OCR is recommendedocr_reason: Optional string explaining why (e.g., "scanned", "vector_text")
This structure lets downstream pipelines process only flagged pages through OCR services, avoiding wasted GPU cycles on clean digital PDFs.
Hybrid OCR Routing: Shared Signals with the Classifier
The key architectural decision is signal reuse: extract_pages_markdown_mem uses the same detection logic as detect_pdf_type to ensure consistency. According to the source code, three signal categories determine the needs_ocr flag per page:
1. Text Quality Signals
From [src/text_quality.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/text_quality.rs), the function analyze_text_quality runs once per document and identifies:
- GID-encoded fonts: Character glyphs encoded as graphics IDs rather than proper Unicode
- Garbled or empty text streams: Extracted text that fails entropy or validity checks
These map to OCR_REASON_VECTOR_TEXT or OCR_REASON_SUSPECTED_GARBLED_TEXT.
2. Page-Level Visual Signals
From [src/detector.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/detector.rs#L1820-L1835), the page_ocr_signals function inspects each page for:
has_template_image: Large background images indicating scanned documentshas_vector_text: Text drawn as vector paths without proper text objects
These map to OCR_REASON_SCANNED or OCR_REASON_VECTOR_TEXT.
3. Document-Wide Context
Font statistics, layout complexity (tables, columns), and header/footer patterns are computed once for the entire document before per-page slicing. This guarantees that:
- Column detection thresholds remain consistent across page ranges
- Table cell preservation rules apply uniformly
- Page number removal masks work correctly for any subset
Step-by-Step Extraction Flow
The implementation in [src/lib.rs lines 57-78](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs#L57-L78) follows this pipeline:
- Validation:
validate_pdf_bytesandload_document_from_memparse the PDF structure - Global analysis: Extract all items with
extract_positioned_text_for_document_analysisto build font stats andtext_qualityevaluation viaanalyze_text_quality - Page filtering: Convert the
pagesargument to aHashSet<u32>for O(1) lookup - Layout computation:
compute_layout_complexity_with_chart_regionsandcalculate_font_stats_from_itemsestablish document-wide baselines - Per-page processing: For each requested page:
- Slice items, rectangles, and removal masks for that page
- Combine OCR signals:
has_gid,has_text_quality_issue, andpage_ocr_signalsresults - Set
needs_ocrandocr_reasonif any signal fires - Generate Markdown via
to_markdown_from_items_with_rects_and_lineswith document-wide font stats - Collect into
PageMarkdown
This prevents the silent mismatch described in issue #227—where the classifier would route to OCR but extraction would proceed with garbled text.
Code Examples
Rust: Direct Crate Usage
use pdf_inspector::{extract_pages_markdown_mem, PagesExtractionResult};
fn main() -> Result<(), pdf_inspector::PdfError> {
// Load PDF into memory
let data = std::fs::read("example.pdf")?;
// Extract all pages (None = every page)
let result: PagesExtractionResult = extract_pages_markdown_mem(&data, None)?;
for page in result.pages {
println!("--- Page {} ---", page.page + 1);
println!("{}", page.markdown);
if page.needs_ocr {
println!("⚠️ OCR required: {}",
page.ocr_reason.unwrap_or("unknown".into()));
}
}
Ok(())
}
Python: PyO3 Bindings
import pdf_inspector
with open("example.pdf", "rb") as f:
data = f.read()
# Extract specific pages with zero-based indexing
result = pdf_inspector.extract_pages_markdown_bytes(data, pages=[0, 2, 5])
for page in result.pages:
print(f"\n--- Page {page.page + 1} ---")
print(page.markdown)
if page.needs_ocr:
print(f"⚠️ Route to OCR: {page.ocr_reason}")
Both examples demonstrate how callers receive actionable per-page metadata—enabling selective OCR pipelines that process only needs_ocr == true pages.
Key Source Files
| File | Purpose |
|---|---|
[src/lib.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs) |
extract_pages_markdown_mem implementation, PageMarkdown/PagesExtractionResult definitions |
[src/detector.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/detector.rs) |
page_ocr_signals and analyze_page_images for visual OCR signals |
[src/markdown/mod.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/markdown/mod.rs) |
to_markdown_from_items_with_rects_and_lines for Markdown generation |
[src/extractor/mod.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/mod.rs) |
Low-level positioned text and layout extraction |
[src/text_quality.rs](https://github.com/firecrawl/pdf-inspector/blob/main/src/text_quality.rs) |
GID-encoded font detection and garbled text analysis |
Summary
extract_pages_markdown_memreturns granular per-page results with OCR routing metadata- Hybrid OCR routing reuses
detect_pdf_typesignals:has_template_image,has_vector_text, and text-quality issues - Document-wide preprocessing ensures consistent font stats, layout detection, and header/footer removal across any page subset
- Signal consistency between extraction and classification prevents the mismatches that caused issue #227
Frequently Asked Questions
What is the difference between process_pdf_mem and extract_pages_markdown_mem?
process_pdf_mem returns a single concatenated Markdown string for the entire document with basic OCR routing, while extract_pages_markdown_mem returns structured per-page results with individual needs_ocr flags and reasons. Use the latter when you need granular control over which pages to process through OCR services.
How does the hybrid OCR routing decide if a page needs OCR?
The routing combines three signal layers: text quality analysis (GID fonts, garbled text from analyze_text_quality), visual signals (template images, vector text from page_ocr_signals), and document context (global font statistics). Any triggering signal sets needs_ocr = true with an appropriate reason constant.
Can I extract only specific pages and still get accurate OCR flags?
Yes. The pages parameter accepts a Vec<u32> or None for all pages. Document-wide preprocessing (font stats, layout complexity, header/footer detection) runs once before slicing, so any subset receives consistent OCR routing decisions based on complete document context.
Why is signal reuse with detect_pdf_type important?
Reusing the same signals prevents classification-extraction mismatches where the document classifier would route to OCR but the extraction pipeline would proceed with unreliable text. As noted in source comments referencing issue #227, this synchronization eliminates silent failures on scanned or vector-text PDFs.
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 →