LightOnOCR, tested on a real scanned PDF
1,954
of 1,955 scan words, read in order
On a Mac, use llama.cpp
LightOnOCR-2 is a small model, 1B. Its GGUF model runs in llama.cpp on the Mac GPU.
1,954 of 1,955 words, in order
72 s for 10 pages, Mac GPU
Install Poppler for pdftoppm, and jq. Put llama.cpp and
the two Q8_0 GGUF files next to the PDF, then run
sh lighton.sh. You get one Markdown file, book.md.
The full lighton.sh script
pdftoppm -scale-to 1540 -png book.pdf page
./llama-b12316/llama-server -m gguf/LightOnOCR-2-1B-Q8_0.gguf --mmproj gguf/mmproj-LightOnOCR-2-1B-Q8_0.gguf \
--host 127.0.0.1 --port 8093 --threads 2 --no-warmup > server.log 2>&1 &
S=$!
until curl -sf http://127.0.0.1:8093/health > /dev/null; do kill -0 $S || exit 1; sleep 1; done
for p in page-*.png; do
{ printf '{"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":"data:image/png;base64,'
base64 -i "$p" | tr -d '\n'
printf '"}}]}],"max_tokens":4096,"temperature":0.2,"top_p":0.9,"seed":42}'; } > req.json
curl -sf http://127.0.0.1:8093/v1/chat/completions -H "Content-Type: application/json" -d @req.json \
| jq -er '.choices[0].message.content' > "${p%.png}.md" || { kill $S; wait $S; exit 1; }
done
kill $S; wait $S
cat page-*.md > book.md
The settings come from the model card
The script uses the settings on the model card:
- No text prompt. The message holds only the page image.
- Pages drawn with the longest side at 1,540 px
- Temperature 0.2, top_p 0.9, up to 4,096 tokens
llama-mtmd-cli cannot send an image with no text. So the script starts llama-server once and sends each page to it. A fixed seed gives the same text on a rerun.
Typed pages: all 13 cover words
LightOnOCR reads every page as a picture, typed pages too. It read all 13 words of the typed cover page, COVER SHEET included.
In the 11-page file, the scan words stayed at 1,954, in 72 s.
The top scan score in our tests
The same 10 scanned pages, scorer and Mac GPU:
| Model | Words | Time |
|---|---|---|
| LightOnOCR-2 | 1,954 | 72 s |
| GLM-OCR | 1,950 | 118 s |
| DeepSeek OCR | 1,948 | 83 s |
| PaddleOCR-VL | 1,947 | 73 s |
| dots.ocr | 1,946 | 168 s |
All five models are close on words. LightOnOCR was also the fastest.
German: two columns in order
The script has no language option. I gave it 3 German pages. One is a magazine page with two columns. It kept the column order.
199 of 206 umlauts
959 of 972 words in order, two columns
Next to GLM-OCR on the same files: 8 more umlauts, 8 more two-column words.
dots.ocr read 5 more umlauts and 10 more two-column words.
It took 157 s for the 3 pages. LightOnOCR took 41 s.
LightOnOCR and our Actor: 5 wins, 1 tie
Our Actor, from a file URL, with no install:
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
| Test | LightOnOCR | Actor |
|---|---|---|
| Scan words in order | 1,954 | 1,874 |
| Typed cover words | 13 | 13 |
| Umlauts | 199 | 192 |
| Two columns, in order | 959 | 954 |
| Time, 11 pages | 72 s | 133 s |
| Time, 3 German pages | 41 s | 59 s |
LightOnOCR read more words, and faster. Both read all 13 cover words.
The Actor needs no GPU and no 1.1 GB download. It costs $0.006 a page.
The license allows business use
The LightOnOCR-2 model card says Apache 2.0. I read it before the download. It adds no use terms.
I planned to test HunyuanOCR first. Its license does not apply in the EU, the UK and South Korea, and it bans use of the output there. So I did not test it.
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 in book.md, with the scorer of the tessdata test. German: the scorer of Tesseract German.
- llama.cpp b12316 for macOS on an Apple Silicon Mac GPU, at the lowest priority, 2 threads. GGUF revision 7d4daf70c285, Q8_0 model and Q8_0 mmproj. Times include one model start.
- One run each. The 11-page file holds the same 10 scan pages, and their text was the same in both runs, page for page.
- The other models: the same files and scorer, on the same Mac, on November 12. Each page has its own setup.
- The script above is the file that ran. The Actor: two real runs earlier the same day, build 0.1.65, on the same files.
What I did not test
- LightOnOCR-3, the newer 4B model. It has its own test: LightOnOCR-3.
- The f16 files, the bbox models and the base model.
- Transformers, vLLM and NVIDIA GPUs.
- Handwriting, tables, math and forms.
Sources
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
Does LightOnOCR run on a Mac?
Yes. With its GGUF model in llama.cpp on the Mac GPU, LightOnOCR read 10 scanned pages in 72 s. The two GGUF files are 1.1 GB.
What prompt does LightOnOCR need?
None. The model card sends only the page image, with the longest side at 1,540 px, at temperature 0.2 and top_p 0.9.
Can a business use LightOnOCR?
Yes. The model card says Apache 2.0, with no added use terms. Keep the license and notices if you share the model.
Related guides
- LightOnOCR-3 4B on a scanned PDF: Mac setup and results
- Granite-Docling on a scanned PDF: Mac setup, tested
- GLM-OCR on a scanned PDF: Mac setup and results, tested
- DeepSeek OCR on a scanned PDF: Mac setup and results, tested
- PaddleOCR-VL on a scanned PDF: Mac setup and results, tested
- dots.ocr on a scanned PDF: Mac setup and results, tested
- PDF to Markdown in Python: 5 libraries tested