Tools,

PaddleOCR on a Mac, tested on a real scanned PDF

1,919

of 1,955 scan words, read in order

Get the setup

Ten scanned pages on the CPU

319 s for 10 pages, 2 CPU threads

139 MB model download

  1. Run pip install paddlepaddle paddleocr.
  2. Save paddle_ocr.py below next to book.pdf.
  3. 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

Scan words in order, CPU only, 2 threads
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

3 German pages: 206 umlauts, 1,887 words
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
Same files, PaddleOCR with --fast on the CPU
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.
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

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.