pdf-inspector: Firecrawl Says 54% of Your PDFs Never Needed OCR
A 20-millisecond Rust check that tells you which pages actually need the expensive path, plus a benchmark table honest enough to show where it loses
Somewhere in your document pipeline there is a step that takes every incoming PDF and hands it to an OCR service. It has been there since the pipeline was built. It works. It costs a few cents and a few seconds per document, and nobody has looked at it in a year.
Firecrawl looked at it and published a number: about 54% of the PDFs going through that step already contain machine-readable text. They were never images. Nothing needed to be recognized. The pipeline paid an image-recognition bill to read text that was sitting right there in the file.
The tool they built to prove it is pdf-inspector, a Rust library that classifies a PDF as TextBased, Scanned, ImageBased, or Mixed in roughly 10 to 50 milliseconds without rendering a single page. It is MIT licensed, 98.7% Rust, and depends on exactly one crate for PDF parsing. No models. No external services. It sat at 1.4k stars, 128 forks, and 336 commits when I read the repository this morning.
The position: your pipeline made a routing decision nobody made
The interesting thing here is not a Rust library. It is that "send everything to the expensive path" is a decision, and almost nobody made it deliberately.
It got made by default, because the expensive path handles every case and the cheap path handles some cases, and writing the branch takes an afternoon you did not have. So the branch never got written, and the pipeline has been paying the maximum price for every document since.
That pattern is everywhere in AI tooling right now, and PDFs are just the clearest instance. The reflex is to reach for a model. The question worth asking first is whether the input requires one.
How it decides, and why it is fast
The mechanism is the part worth understanding, because it explains both the speed and the failure modes.
pdf-inspector parses the xref table and the page tree without loading full objects. Then it selects pages according to a scan strategy and looks inside the content streams for two things: the Tj and TJ text-showing operators, and the Do operator that draws an external object like an image. Text operators present means the page carries real text. Image operators with no text means the page is a picture of a document.
That is it. It never rasterizes anything, which is why a 300-page PDF classifies in milliseconds.
The scan strategy is the tuning knob most people will want. EarlyExit, the default, scans pages and stops at the first non-text page, which is what you want when the only question is "can I take the fast path with this whole document?" Full scans everything with no early exit and gives you an accurate Mixed versus Scanned answer. Sample(n) checks first, last, and evenly distributed middle pages for very large files. Pages(vec) checks exactly the page numbers you name.
The output that changes pipeline design is pages_needing_ocr, a list of specific page numbers lacking text. A scanned cover sheet stapled to a 40-page text-based contract stops being a 41-page OCR job and becomes a one-page OCR job. All-or-nothing routing becomes per-page routing.
Once a document classifies as text-based, the same loaded document feeds the extractor, so there is no second parse. Extraction is position-aware, carrying font info and X/Y coordinates, with automatic multi-column reading order and RTL support. It handles CID fonts through ToUnicode CMap decoding, rejoins hyphenated words broken across lines, filters page numbers, collapses table-of-contents dot leaders, merges drop caps, and flags broken font encodings so you can fall back to OCR when the text is technically present but garbage. Markdown conversion infers headings from font size tiers with 0.5pt clustering, detects code blocks from monospace font names, and finds tables two ways: rectangles from PDF drawing operations, and alignment heuristics from text positions.
Firecrawl's stated target for the whole text-based path is under 200 milliseconds locally, against 2 to 10 seconds for an OCR service round trip.
Put this into practice
The lowest-friction version of this takes about an hour and does not require adopting anything.
Measure your own ratio first. Firecrawl's 54% is Firecrawl's corpus. Yours could be 90% if you ingest research papers and filings, or 15% if you ingest scans from a county records office. Run detection over a sample of a few hundred real documents and count. That number alone tells you whether the rest of this is worth doing.
cargo run --bin detect-pdf -- document.pdf --json
Add --analyze if you also want layout information about tables and columns.
Then check quality on the ones you would reroute. Convert a handful of your actual text-based documents and read the output:
cargo run --bin pdf2md -- document.pdf
If you live in Node or Python, skip the Rust toolchain. npm install @firecrawl/pdf-inspector gives you processPdf and classifyPdf. On the Python side, maturin develop --release gets you pdf_inspector.process_pdf, which returns both pdf_type and markdown from one call.
Wire the branch as classification-only at first. Route on pdf_type and pages_needing_ocr but keep sending everything to your existing extractor. Log the classification next to the result for a week. Now you know your misclassification rate on real traffic before it can cost you anything, and the switch to local extraction becomes a config change rather than a leap.
Where it loses, in its own numbers
Firecrawl published a benchmark table against the 200-PDF opendataloader-bench corpus, restricted to direct text extraction engines. Read it carefully, because it does not say what a marketing table would say.
pdf-inspector scores 0.78 overall, 0.87 reading order, 0.59 tables, 0.57 headings, and runs the 200 documents in 4 seconds. opendataloader scores 0.84 overall, 0.91 reading order, 0.49 tables, 0.74 headings, in 11 seconds. pymupdf4llm lands at 0.73 overall in 18 seconds. markitdown at 0.58, with 0.00 on both tables and headings.
So the closest competitor beats pdf-inspector on overall quality, on reading order, and substantially on headings, while also using no OCR and finishing the same corpus in 11 seconds. Eleven seconds is not slow. If your bottleneck is quality rather than throughput, the honest answer from Firecrawl's own table is that you might want opendataloader.
pdf-inspector wins on speed (fastest of everything measured) and on table detection among direct-text tools, 0.59 against 0.49. The README says where it lags and why: many PDFs use bold text at body font size for headings, or headings barely larger than body text, and a font-size-tier heuristic cannot see those. Table detection also trails OCR and ML engines that can see visual structure.
For context, the README notes that docling, marker, and mineru score 0.83 to 0.88 overall and take 2 to 180 minutes on the same corpus. The gap between 0.78 in 4 seconds and 0.88 in three hours is the entire argument, and it is a good one. Just do not read it as "as good as OCR."
Other limits worth naming. It does no OCR at all, so the scanned half of your corpus still needs the expensive path and you now maintain two code paths instead of one. The benchmark is 200 documents, run by the tool's author, on a corpus that may not resemble yours. Classification is heuristic: a page carrying a tiny text watermark over a scanned image has text operators present, and I would test that case specifically before trusting the routing on anything mixed. The repository lists no tagged releases, and the Node package pulls a platform binary, so pin your version and read the diff when it moves.
And the headline percentage is Firecrawl's, measured on Firecrawl's traffic, published by a company that sells a document parsing product. It is a credible number and it is also a marketing number. Measure your own.
What to do with an hour
Run detection over your last thousand documents and look at the split. That is the whole exercise. If it comes back 15% text-based, close the tab and keep your pipeline. If it comes back 60%, you have been paying an OCR bill and a multi-second latency penalty on most of your traffic for something that was already readable, and you now have a 20-millisecond way to stop.
The broader habit is the thing worth keeping. Before a document, a request, or a task goes to a model, ask what fraction of the inputs need one. Sometimes the answer is all of them. Sometimes, as here, it is 46%.
Sources: firecrawl/pdf-inspector README and benchmark, @firecrawl/pdf-inspector on npm, opendataloader-bench. Repository figures read directly from GitHub on 2026-08-03.