PaddleOCR on a Mac, tested on a real scanned PDF
1,919
of 1,955 scan words, read in order
Ten scanned pages on the CPU
319 s for 10 pages, 2 CPU threads
139 MB model download
- Run
pip install paddlepaddle paddleocr. - Save paddle_ocr.py below next to book.pdf.
-
Run
python3 paddle_ocr.py --fast. It writes book.txt. The first run downloads the models.
paddle_ocr.py
"""PaddleOCR on a PDF: book.txt plus page-NN.txt (brasspage.com/paddleocr-mac/, Nov 12, 2026)."""
import argparse
from paddleocr import PaddleOCR
ap = argparse.ArgumentParser()
ap.add_argument("--lang", default=None) # None = PP-OCRv6_medium, the 3.7.0 default
ap.add_argument("--size", default="medium") # PP-OCRv6 medium, small or tiny
ap.add_argument("--fast", action="store_true") # skip the page turn, unwarp and line turn models
ap.add_argument("--device", default=None) # None = PaddleOCR picks (CPU on a Mac)
ap.add_argument("--threads", type=int, default=10) # the PaddleOCR default is 10
a = ap.parse_args()
opts = {"lang": a.lang, "device": a.device, "cpu_threads": a.threads}
if a.size != "medium":
opts["text_detection_model_name"] = f"PP-OCRv6_{a.size}_det"
opts["text_recognition_model_name"] = f"PP-OCRv6_{a.size}_rec"
if a.fast:
opts["use_doc_orientation_classify"] = False
opts["use_doc_unwarping"] = False
opts["use_textline_orientation"] = False
ocr = PaddleOCR(**opts)
pages = [res["rec_texts"] for res in ocr.predict("book.pdf")]
for i, lines in enumerate(pages, 1):
with open(f"page-{i:02d}.txt", "w") as f:
f.write("\n".join(lines) + "\n")
with open("book.txt", "w") as f:
f.write("\n\n".join("\n".join(lines) for lines in pages) + "\n")
The default model is PP-OCRv6 medium. English, German and French get
the same model, so --lang changes nothing for them.
No GPU and no MPS on a Mac
The macOS PaddlePaddle wheel has no CUDA, and PaddleOCR has no Apple
GPU device. With --device gpu, it runs on the CPU:
The specified device (GPU) is not available! Switching to CPU instead.
The text was the same as the CPU run, byte for byte. With
--device mps, it stops at once:
assert device_type.lower() in SUPPORTED_DEVICE_TYPE
AssertionError
Turn off the three extra models
By default, PaddleOCR runs three more models on each page. They turn and unwarp photos of pages. On these flat scans, they cut letters. Page 9 of the default run:
n a safe-deposit in town. The police have
heir own methods of keeping certain matters
out of the newspapers; otherwise, it can
nardly be supposed that Watkins's evidence
bout Mr Poschwitz's death could have
ailed to furnish a good many head-lines
f a startling character to the press.
On the typed cover page, "From" became "rom". With
--fast, these lines and all 13 cover words were right.
Tiny reads 10 pages in 39 seconds
| Setup | Words | Time |
|---|---|---|
--fast |
1,919 | 319 s |
| Default | 1,908 | 333 s |
--size small |
1,885 | 117 s |
--size tiny |
1,860 | 39 s |
| Kraken | 1,875 | 321 s |
| docTR | 1,869 | 97 s |
The small and tiny rows also use --fast. PaddleOCR uses
10 threads by default. I gave it 2, the same as the other rows.
Spaces after apostrophes go missing
With --fast, 18 of the 24 missed places were only a
space. In 8, the space after an apostrophe is gone:
unloadin'my own business on strangers. And
what's more, I say nobody ain't goin'to get their
got hold o'sumpin'that felt powerful funny. Me
A search for "goin' to" will not find these lines. In 6 more, an opening quote mark got a space after it.
German works, two columns do not
| Setup | Umlauts | Words |
|---|---|---|
--fast |
145 | 1,392 |
| Default | 141 | 1,341 |
On a clean one-column page, --fast read 519 of 521 words.
On a two-column page, it read straight across both columns:
kf. Hauptmann Boris Mantow hatte bis spät in
vollkommen entging, wie dieselben Augen aufmerksam
A book printed in 1869 sets ß as a long s and an s. On its page, PaddleOCR wrote all 13 as "fs", as in "Fufs" for "Fuß".
PaddleOCR and our Actor: 1 win, 4 losses
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 | PaddleOCR | Actor |
|---|---|---|
| Scan words in order | 1,919 | 1,874 |
| Typed cover words | 13 | 13 |
| Umlauts | 145 | 192 |
| Two columns, in order | 493 | 954 |
| Time, 11 pages | 293 s | 133 s |
| Time, 3 German pages | 260 s | 60 s |
The Actor needs no install and no model download. It costs $0.006 a page.
Apache 2.0, models included
The PaddleOCR, PaddlePaddle and PaddleX license files are Apache 2.0. I read them in full before the install.
Every model card the test downloaded says apache-2.0, with no other terms.
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. One page has two columns, 972 words.
- Score: true words found in order in book.txt, with the scorer of the tessdata test. German: the scorer of Tesseract German.
- paddlepaddle 3.3.1, paddleocr 3.7.0 and paddlex 3.7.2, Python 3.12, on an Apple Silicon Mac CPU, at the lowest priority.
- PaddleOCR renders each PDF page at 144 dpi. Times count the whole script, with the model load. One run of each setup.
- The Kraken and docTR rows: the same files, scorer, Mac and 2 CPU threads, on November 12. They have their own pages.
- The Actor: two real runs earlier the same day, build 0.1.65, on the same files.
What I did not test
- 10 threads, the PaddleOCR default.
- Rotated or curved photos of pages, the job of the three extra models.
- Layout and table parsing, and PaddleOCR-VL.
- The PaddleOCR command line and server.
- Windows, Linux and NVIDIA GPUs.
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 PaddleOCR use the GPU on a Mac?
No. The macOS PaddlePaddle wheel is CPU only. --device gpu switches to the CPU with a warning, and --device mps stops with an AssertionError.
Can PaddleOCR read German?
Yes, with the default model and no language flag. It wrote 145 of 206 umlauts on 3 German pages, but it read two columns straight across.
Is PaddleOCR free for business use?
Yes. The code is Apache 2.0, and every model card the test downloaded says apache-2.0.
Related guides
- RapidOCR in Python: tested vs Tesseract and PaddleOCR
- Pix2Text OCR: the install fix, tested on scans and math
- keras-ocr in 2026: the install fix, tested vs Tesseract
- MinerU on a Mac: setup and a scanned PDF test
- TrOCR on real handwriting: setup and a 100-note test
- PaddleOCR-VL on a scanned PDF: Mac setup and results, tested
- EasyOCR vs Tesseract vs PaddleOCR vs RapidOCR, tested
- Kraken OCR on a scanned PDF: Mac setup and results, tested
- docTR OCR on a scanned PDF: Mac setup and results, tested
- GOT-OCR 2.0 on a scanned PDF: Mac setup and results, tested
- Immich OCR not working? Models and settings tested