How to OCR a PDF With PaddleOCR in Python
Install paddlepaddle and paddleocr, create PaddleOCR(lang='en'), and call ocr.predict() on an image or directly on a PDF. Each result has rec_texts, rec_scores and rec_boxes. To make a searchable PDF, render pages with PyMuPDF, OCR them, and write the recognised text back as an invisible layer.
PaddleOCR is Baidu’s open-source OCR toolkit. It detects text lines in an image and recognises them with deep-learning models, and it handles 100+ languages, including Chinese, Japanese and Korean.
This guide targets PaddleOCR 3.7 with PaddlePaddle 3.3. Version 3 changed the API substantially. If you’re updating old code, jump to Migrating from PaddleOCR 2.x.
Install
PaddleOCR runs on top of the PaddlePaddle deep-learning framework, so install both:
pip install paddlepaddle paddleocr
This installs the CPU build. For NVIDIA GPUs, install the matching paddlepaddle-gpu build for your CUDA version using the selector on paddlepaddle.org.cn, then install paddleocr.
First run downloads the models, about a hundred megabytes, into ~/.paddlex/official_models. Models come from Hugging Face by default. If Hugging Face is blocked or slow where you are, choose another source before running:
export PADDLE_PDX_MODEL_SOURCE=modelscope # or: aistudio, bos, huggingface
OCR an image
from paddleocr import PaddleOCR
ocr = PaddleOCR(
lang="en",
use_doc_orientation_classify=False, # skip whole-page rotation detection
use_doc_unwarping=False, # skip curved-page correction
use_textline_orientation=False, # skip upside-down line detection
)
for res in ocr.predict("page-1.png"):
for text, score in zip(res["rec_texts"], res["rec_scores"]):
print(f"{score:.2f} {text}")
The three use_* flags turn off extra models. That’s faster and fine for straight, flat scans. Turn them on for phone photos or pages that may be rotated.
What’s in a result
predict() returns one result per image (or per PDF page). The fields you’ll use most:
| Field | Contents |
|---|---|
rec_texts |
List of recognised text lines |
rec_scores |
Confidence for each line, from 0 to 1 |
rec_boxes |
Axis-aligned boxes as [x_min, y_min, x_max, y_max] in image pixels |
rec_polys |
Four-corner polygons, useful for rotated text |
page_index |
Page number when the input is a PDF |
Results can also print and save themselves:
for res in ocr.predict("page-1.png"):
res.print() # pretty-print everything
res.save_to_json("output") # output/page-1_res.json
res.save_to_img("output") # image with boxes drawn on it
OCR a PDF directly
predict() accepts a PDF path and processes every page:
from paddleocr import PaddleOCR
ocr = PaddleOCR(lang="en", use_doc_orientation_classify=False,
use_doc_unwarping=False, use_textline_orientation=False)
pages = []
for res in ocr.predict("scanned.pdf"):
pages.append("\n".join(res["rec_texts"]))
with open("scanned.txt", "w", encoding="utf-8") as f:
f.write("\n\n".join(pages))
PaddleOCR renders each PDF page internally at about 144 DPI. For small print, render the pages yourself at a higher resolution, as in the next section.
OCR with PyMuPDF for full control
Rendering pages with PyMuPDF lets you choose the resolution, skip pages that already have text, and map results back to PDF coordinates.
import numpy as np
import pymupdf
from paddleocr import PaddleOCR
DPI = 200
def page_to_image(page, dpi=DPI):
"""Render a PDF page to a BGR numpy array, the format PaddleOCR expects."""
pix = page.get_pixmap(dpi=dpi, colorspace=pymupdf.csRGB, alpha=False)
img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, 3)
return img[:, :, ::-1].copy() # RGB -> BGR
ocr = PaddleOCR(lang="en", use_doc_orientation_classify=False,
use_doc_unwarping=False, use_textline_orientation=False)
doc = pymupdf.open("scanned.pdf")
for page in doc:
if page.get_text().strip():
continue # page already has real text, no OCR needed
res = ocr.predict(page_to_image(page))[0]
print(f"--- page {page.number + 1} ---")
print("\n".join(res["rec_texts"]))
Make a searchable PDF
This is the most useful end result: the page looks exactly like the scan, but you can search, select and copy its text. The trick is to write each recognised line back onto the page as invisible text (render_mode=3) at the position PaddleOCR found it.
import numpy as np
import pymupdf
from paddleocr import PaddleOCR
DPI = 200
SCALE = 72 / DPI # image pixels -> PDF points (1 point = 1/72 inch)
def page_to_image(page, dpi=DPI):
pix = page.get_pixmap(dpi=dpi, colorspace=pymupdf.csRGB, alpha=False)
img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, 3)
return img[:, :, ::-1].copy()
def add_text_layer(page, texts, boxes, fontname="helv"):
"""Write invisible text over each OCR box so the page becomes searchable."""
for text, (x0, y0, x1, y1) in zip(texts, boxes):
rect = pymupdf.Rect(x0, y0, x1, y1) * SCALE
fontsize = rect.height * 0.8
natural_width = pymupdf.get_text_length(text, fontname=fontname, fontsize=fontsize)
if natural_width == 0:
continue
origin = pymupdf.Point(rect.x0, rect.y1 - rect.height * 0.2)
stretch = pymupdf.Matrix(rect.width / natural_width, 1) # fit the box width
page.insert_text(
origin, text, fontname=fontname, fontsize=fontsize,
render_mode=3, # 3 = invisible
morph=(origin, stretch),
)
ocr = PaddleOCR(lang="en", use_doc_orientation_classify=False,
use_doc_unwarping=False, use_textline_orientation=False)
doc = pymupdf.open("scanned.pdf")
for page in doc:
if page.get_text().strip():
continue
res = ocr.predict(page_to_image(page))[0]
add_text_layer(page, res["rec_texts"], res["rec_boxes"])
doc.save("searchable.pdf", garbage=3, deflate=True)
Check the result:
d = pymupdf.open("searchable.pdf")
print(d[0].search_for("Invoice")) # returns the word's position on the page
The page coordinates are simple: the image was rendered at DPI pixels per inch and PDF units are 1/72 inch, so multiplying pixel boxes by 72 / DPI gives PDF positions. Horizontal stretching makes each invisible line span its box, so selections line up with the visible text.
For Chinese, Japanese or Korean text, use one of PyMuPDF’s built-in CJK fonts: fontname="china-s" (Simplified Chinese), "china-t" (Traditional), "japan" or "korea". The default helv font only covers Latin characters.
Prefer a ready-made tool? OCRmyPDF does all of this from the command line (
ocrmypdf input.pdf output.pdf) using Tesseract. See Make a scanned PDF searchable.
Filter low-confidence results
Drop lines PaddleOCR isn’t sure about, which are often stamps, noise or handwriting:
ocr = PaddleOCR(lang="en", text_rec_score_thresh=0.8)
Or filter after the fact:
lines = [t for t, s in zip(res["rec_texts"], res["rec_scores"]) if s >= 0.8]
Languages
Set lang to the document’s language. Common codes:
lang |
Language |
|---|---|
ch |
Chinese and English (default when lang isn’t set) |
en |
English |
chinese_cht |
Traditional Chinese |
japan |
Japanese |
korean |
Korean |
fr, de, es, it, pt, nl… |
Latin-script languages |
ru |
Russian (and other Cyrillic) |
ar |
Arabic |
hi |
Hindi (Devanagari) |
PaddleOCR picks the best model version for the language automatically. To pin one, pass ocr_version="PP-OCRv5" (supported: PP-OCRv3 to PP-OCRv6).
Use a GPU
ocr = PaddleOCR(lang="en", device="gpu:0") # or "cpu"
This requires the GPU build of PaddlePaddle (see Install).
Command line
PaddleOCR also works without writing Python:
paddleocr ocr -i scanned.pdf --lang en --save_path ./output
Run paddleocr ocr --help to see all options.
Migrating from PaddleOCR 2.x
Most tutorials online still show the 2.x API, which is deprecated in 3.x:
| PaddleOCR 2.x | PaddleOCR 3.x |
|---|---|
ocr.ocr(img, cls=True) |
ocr.predict(img) |
use_angle_cls=True |
use_textline_orientation=True |
use_gpu=True |
device="gpu:0" |
Result: [[box, (text, score)], …] |
Result object: res["rec_texts"], res["rec_scores"], res["rec_boxes"] |
ocr.ocr() and use_angle_cls still work in 3.x but print deprecation warnings.
Troubleshooting
| Problem | Fix |
|---|---|
No available model hosting platforms detected |
Models can’t be downloaded. Check your network, or set PADDLE_PDX_MODEL_SOURCE (see Install) |
| Poor accuracy on small text | Render pages at 300 DPI instead of 200 |
| Garbled output on Chinese documents | You set lang="en"; use ch |
| Rotated or upside-down pages | Enable use_doc_orientation_classify=True |
| Slow on large PDFs | Turn off the optional models, use a GPU, or skip pages that already have text |
Frequently asked questions
Is PaddleOCR free for commercial use?
Yes. PaddleOCR and PaddlePaddle are released under the Apache 2.0 licence, which allows commercial use. If you combine it with PyMuPDF, note that PyMuPDF is AGPL unless you buy a commercial licence.
Does PaddleOCR need a GPU?
No. It runs on CPU. A GPU makes large batches much faster; install the GPU build of PaddlePaddle and pass device='gpu:0'.
Why does my old PaddleOCR code break?
PaddleOCR 3.0 changed the API. ocr.ocr() is deprecated in favour of ocr.predict(), results are objects with rec_texts, rec_scores and rec_boxes instead of nested lists, use_angle_cls became use_textline_orientation, and use_gpu was replaced by device.
PaddleOCR vs Tesseract: which is better?
PaddleOCR's deep-learning models are generally more accurate on photos, low-quality scans, rotated text and Chinese, Japanese and Korean text. Tesseract (used by OCRmyPDF) is lighter, easier to install and does well on clean printed documents.
Why does PaddleOCR fail with 'No available model hosting platforms detected'?
The first run downloads models, and none of the model hosts was reachable. Check your network or proxy, or set PADDLE_PDX_MODEL_SOURCE to a host you can reach: huggingface, modelscope, aistudio or bos.
Was this page helpful?
Thanks for the feedback!