Tools,

Stirling-PDF OCR, tested on scanned PDFs

401

first answer from the OCR API

Get the fix

Fix the 401 with an API key

A plain docker run of Stirling-PDF 3.3.0 turns login on. My first OCR call, with no key, got HTTP 401:

{"error":"Authentication required"}

I saved this as key.sh. It logs in as the default admin and prints the key:

JWT=$(curl -s -X POST "$STIRLING/api/v1/auth/login" -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "stirling"}' | jq -r .session.access_token)
curl -s -X POST -H "Authorization: Bearer $JWT" "$STIRLING/api/v1/user/get-api-key" | jq -r .apiKey

Put the key in STIRLING_KEY, then change the default password, stirling. SECURITY_ENABLELOGIN=false also worked with no key, but then anyone who can reach the port can use it.

A scan works, typed page or not

1,857 of 1,955 true words found

12 s for 10 pages, on 2 CPUs

I saved this as ocr.sh and sent the 10-page scan:

curl -s -H "X-API-KEY: $STIRLING_KEY" -F "fileInput=@$1" -F "languages=${2:-eng}" \
  "$STIRLING/api/v1/misc/ocr-pdf" -o "${1%.pdf}.ocr.pdf"

pdftotext scan.ocr.pdf - prints the text. With a typed cover page in front, I got the same 1,857 words and 13 of 13 cover words.

Stirling sends --skip-text, so OCRmyPDF reads only the pages with no text.

Join languages with a comma

Tesseract joins languages with a plus. Stirling does not. With languages=deu+eng, the API answered HTTP 400:

Invalid OCR languages format: none of the selected languages are valid

With deu,eng it ran OCRmyPDF with --language deu+eng.

Get clean text from the sidecar

On a two-column magazine page, the text layer of the output PDF mixed the columns. Add sidecar=true, and the zip has the text in reading order. I saved this as text.sh:

curl -s -H "X-API-KEY: $STIRLING_KEY" -F "fileInput=@$1" -F "languages=${2:-eng}" \
  -F "sidecar=true" "$STIRLING/api/v1/misc/ocr-pdf" -o ocr.zip
unzip -p ocr.zip "*.txt"
3 German pages, 1,887 words, 206 umlauts
Setup Words right Umlauts right
eng, the default 1,387 0
deu, PDF text layer 1,616 165
deu, sidecar text 1,843 192
Our OCR Actor, deu 1,853 192

Umlauts here are ä, ö, ü and ß. The Actor row is explained below.

A missing language fails quietly

The image has 5 OCR languages: chi_sim, deu, eng, fra and por. I asked for eng,ita on 3 Italian pages, and the API answered HTTP 200.

3 of 87 accents right

eng the only language OCRmyPDF got

Stirling drops a language it does not have, with no error. With ita alone, it answered the same HTTP 400 as above.

Add a language with a mount

Put the language file in a folder:

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

Then mount the folder at /usr/share/tessdata:

docker run -d --name stirling -p 8080:8080 \
  -v "$PWD/tessdata:/usr/share/tessdata" stirlingtools/stirling-pdf:latest

79 of 87 accents right, with ita

1,301 of 1,350 words right

At start, Stirling copies the file next to its own 5. A mount over /usr/share/tesseract-ocr/5/tessdata hid those 5, and eng got HTTP 400.

Want Markdown out? Use the Actor

Stirling OCR is free and good. Our OCR Actor gives Markdown, in 10 languages, with no server. On the German pages:

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/paperless-german-3-pages.pdf"], "language": "deu"}' \
  | jq -r '.[].markdown' > german.md

899 of 915 words on the one-column pages, sidecar 900

954 of 972 on the two-column page, sidecar 943

Build 0.1.65, live since Nov 11, reads the two columns in order. Build 0.1.64 mixed them and got 497. It costs $0.006 a page.

How I tested it
  • Scan: the 10-page PDF of OCR in n8n, proofread Wikisource book scans with no text layer. 1,955 true words.
  • Typed page: a 13-word cover sheet in front of the scan, the file of AnythingLLM OCR.
  • German: the 3 pages of Paperless-ngx OCR. Italian: 3 proofread it.wikisource pages from the Tesseract Italian test.
  • Score: true words found in order, with the scorer of the tessdata test. PDF text layer: pdftotext.
  • Stirling-PDF 3.3.0 on arm64, with Tesseract 5.3.4 and OCRmyPDF 17.13.0. A new container for each setup, 2 CPUs.
  • The code blocks above are the files that ran. run.sh ran with the port on 127.0.0.1. The Actor: one real run on build 0.1.64, then the same input on build 0.1.65.
What I did not test
  • The web page, pipelines, and the async job mode.
  • The fat image, deskew, clean, and force-ocr.
  • Handwriting, tables, and Stirling-PDF 2.x.
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 the Stirling-PDF OCR API return 401?

In 3.3.0, a plain docker run turns login on. Log in as admin, get the key from /api/v1/user/get-api-key, and send it as X-API-KEY. Or set SECURITY_ENABLELOGIN=false on a closed network.

How do I add an OCR language to Stirling-PDF?

Put the .traineddata file from tessdata_fast in a folder and mount it at /usr/share/tessdata. At start, Stirling copies it next to its 5 built-in languages.

Why does OCR answer 403 in Stirling-PDF?

The latest-ultra-lite image has no Tesseract and no OCRmyPDF, so its OCR call answered 403 Forbidden. Use the latest tag.