Scan or text?

Before you can read a PDF, you have to know whether it has text in it or is just pictures of pages, which need OCR. I built a small classifier that answers without rendering a single page, in about a millisecond, and tested it against the tool that inspired it.

Made
Written by
Claude Opus 5.5 in Claude Code, directed by me
Built with
Rust, compiled to WebAssembly
Status
Working; try it below

Try it

With JavaScript on, you can drop a PDF here and the classifier runs on it in your browser. The file is never uploaded.

The question

Every document pipeline hits the same fork. A digital PDF already has its text, and extracting it is cheap and exact. A scanned one is only images, and needs OCR, which is slow and sometimes wrong. Many files are both: a typed contract with a scanned signature page. The fork has to be decided before anything else happens, so it needs to be fast.

pdf-inspector, a Rust library from Firecrawl, does this check and quotes about 10 to 50 ms. The idea I started from asked for under 5 ms. So: can a deliberately minimal classifier make that decision in under 5 ms, as accurately, and still do it inside a web page?

How it works

How I tested it

Results

525 of 525routed correctly: OCR or not
0.9 msmedian per file, native; 1.2 ms in WebAssembly
Head-to-head on 525 PDFs, same run
Routed correctlyMedian95th percentileSlowest
This classifier525 / 5251.3 ms4.1 ms14 ms
pdf-inspector 1.24.0491 / 5253.1 ms9.5 ms67 ms
Full parse (PyMuPDF, in Python)the ground truth14 ms40 ms703 ms

This laptop's speed drifted by about two times between runs, so compare numbers within a run. In the head-to-head, both tools ran under identical conditions, and this classifier was faster on 523 of the 525 files, typically 2.3 times. In two back-to-back runs of this classifier alone, the median was 0.9 ms native and 1.2 ms in WebAssembly, with the 95th percentile at 2.6 ms and 3.6 ms. That puts WebAssembly at about 1.3 times native, comfortably under 5 ms either way.

Where the two disagree

Every one of pdf-inspector's 34 misses sends a file to OCR that doesn't need it. It never missed a file that did.

What didn't work

Limits

Credits