Tools,

Unstructured OCR, tested on scanned PDFs

13

words out of 11 pages, strategy auto

Get the fix

One typed page turns OCR off

If any page has a text layer, auto, the default strategy, picks fast. Fast does no OCR.

11 pages: 1 typed, 10 scanned
Strategy Words out Cover words, of 13
auto (default) 13 13
ocr_only 2,068 12
hi_res 2,079 13

Both OCR strategies found 1,867 of the 1,955 scan words. If a PDF can mix typed and scanned pages, set the strategy yourself. The script I ran, partition.py:

import sys
from unstructured.partition.pdf import partition_pdf

elements = partition_pdf(sys.argv[1], strategy=sys.argv[2], languages=sys.argv[3:] or None)
print("\n\n".join(e.text for e in elements))

On a plain scan, auto works

With no text layer on any page, auto picks ocr_only. On the 10-page scan, auto and ocr_only gave the same bytes.

1,867 of 1,955 words, auto and ocr_only

0 words, fast

hi_res found the same 1,867 words. It downloads a 217 MB layout model on the first run.

Other languages need a Tesseract pack

With deu as the language, both OCR strategies stopped with Failed loading language 'deu'. Homebrew Tesseract had only eng and osd. I saved this as lang.sh:

mkdir -p tessdata
curl -sL -o tessdata/deu.traineddata https://github.com/tesseract-ocr/tessdata/raw/main/deu.traineddata

Then I set TESSDATA_PREFIX to that folder and ran partition.py with deu again.

3 German pages, 1,887 words
Setup Umlauts, of 206 Words right
ocr_only, no language 0 1,611
ocr_only, deu 191 1,794
hi_res, deu 162 1,568

hi_res mixed up two columns

One German page is a two-column magazine page. There, hi_res kept far fewer words in the right order.

901 of 972 words in order, ocr_only

675 of 972, hi_res

Both with deu. Our Actor got 954 on the same page.

pip install unstructured is not enough

With plain pip install unstructured, partition.py stopped at the import: No module named 'pdfminer'. pip install "unstructured[pdf]" fixed it. That venv was 1.5 GB, with torch.

Unstructured 0.27.37 also sends usage data on import and on each partition call. DO_NOT_TRACK=1 turns that off. I set it.

Want Markdown out? Use the Actor

Unstructured is free and runs offline. Our OCR Actor gives Markdown from a file URL. On the 11-page file:

curl -s -X POST "https://api.apify.com/v2/acts/brasspage~pdf-ocr-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls": ["https://api.apify.com/v2/key-value-stores/sTnmNrGoSsi7tLQvs/records/anythingllm-mixed-11-pages.pdf"]}' \
  | jq -r '.[].markdown' > mixed.md

1,874 of 1,955 scan words, Unstructured 1,867

192 of 206 umlauts, Unstructured 191

It also read 13 of 13 cover words with no strategy to pick. It was not faster: 135 s for the 11 pages. On one German page, Unstructured got one more umlaut. The Actor costs $0.006 a page.

How I tested it
  • Scan: the 10 pages of OCR in n8n, proofread Wikisource book scans, 1,955 true words. The 11-page file adds the 13-word typed cover page of AnythingLLM OCR.
  • German: the 3 pages of Paperless-ngx OCR, 1,887 words, 206 umlauts.
  • Score: true words found in order, with the scorer of the tessdata test. German: words and umlauts right, the scorer of Tesseract German.
  • Unstructured 0.27.37 and unstructured-inference 1.6.17 on Python 3.13.11, Homebrew Tesseract 5.5.4, on an arm64 Mac. 2 threads at the lowest priority.
  • The code blocks above are the files that ran. The Actor: two real runs, build 0.1.65.
What I did not test
  • The Unstructured API and its paid platform.
  • Tables (infer_table_structure), a GPU, and PaddleOCR.
  • LangChain's UnstructuredPDFLoader.
Sources

Two ways to skip the work

Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one page free

Questions people ask

Why does Unstructured return no OCR text from my PDF?

If any page has a text layer, the auto strategy picks fast, and fast does no OCR. On my 11-page file, it read only the 13 words of the typed page. Pass strategy="ocr_only" or "hi_res".

Is hi_res better than ocr_only for scanned PDFs?

Not in my test. On a 10-page scan, both found 1,867 of 1,955 words. On a two-column page, hi_res kept 675 of 972 words in order, ocr_only 901.

How do I set the OCR language in Unstructured?

Pass languages=["deu"] and install that Tesseract language pack. Without the pack, partition_pdf stopped with "Failed loading language 'deu'". With it, ocr_only got 191 of 206 German umlauts.