PyMuPDF OCR works, except inside table grids
6.2%
of invoice numbers read inside ruled tables
Try it on your PDF, $0.006 a page
OCR a scanned PDF: the example
0 words from get_text(), on all 127 scans
212 words from page_text(), on the page below
import pymupdf
def page_text(page, dpi=300):
text = page.get_text(sort=True)
if text.strip(): # real text, or an OCR layer that is already there
return text
tp = page.get_textpage_ocr(dpi=dpi, full=True)
return page.get_text(textpage=tp, sort=True) # sort keeps lines whole
with pymupdf.open("scan.pdf") as doc:
for page in doc:
print(page_text(page)[:79])
86 The Boy Scouts of the Air
‘‘Keep your head, Hatton,’’ commanded
Pass dpi: the default 72 loses 8 points
8.9 words a line with sort=True
5.3 words a line without it
The scans are 300 dpi, and 150 read as well as 300.
sort=True cost a little word order.
Inside a table grid, the rows are gone
An invoice with 11 rows, at dpi 300:
Northfield Supply - Invoice 1735
Date: October 12, 2026. Terms: net 30.
“SKU | Description | Qty | Unit price| Amount
Thank you for your business.
| Method | Numbers right |
|---|---|
| PyMuPDF, dpi 300 | 199 (6.2%) |
| PyMuPDF, default | 253 (7.9%) |
| pytesseract, dpi 300 | 2,863 (89.1%) |
| ocrmypdf | 2,790 (86.9%) |
0 of 30 numbers, clean table, full grid
30 of 30 same table, lines under rows only
Same pixels, same eng model file. Tesseract 5.5.3 read them. Other dpi and tessdata_best did not help, and I do not know why.
Fix: erase the lines, then OCR
import cv2
import numpy as np
import pymupdf
def ocr_without_lines(page, dpi=300):
pix = page.get_pixmap(dpi=dpi, colorspace="gray")
img = np.frombuffer(pix.samples, np.uint8).reshape(pix.h, pix.w)
ink = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
lines = np.zeros_like(ink)
for shape in ((1, dpi // 6), (dpi // 6, 1)): # lines over 1/6 inch
lines |= cv2.morphologyEx(ink, cv2.MORPH_OPEN, np.ones(shape, np.uint8))
img = np.where(cv2.dilate(lines, np.ones((3, 3), np.uint8)), 255, img)
gray = pymupdf.Pixmap(pymupdf.csGRAY, pix.w, pix.h, img.astype(np.uint8).tobytes(), False)
rgb = pymupdf.Pixmap(pymupdf.csRGB, gray) # a gray pixmap gave no text
rgb.set_dpi(dpi, dpi)
return pymupdf.open(stream=rgb.pdfocr_tobytes())[0].get_text()
88.3% invoice numbers, lines erased
80.9% book words, lines erased
That was its first run, not tuned. It hurt the book pages, so use it
on table pages only. Or call pytesseract on the same
pixmap.
Already OCRed? The default reads it twice
15,337 words in the text layer
30,447 words from the default call
On the 55 book scans after ocrmypdf, get_textpage_ocr()
kept the text layer and read the picture under it too.
page_text() checks for text first, and
full=True also avoids it.
2.5 times faster than pytesseract
| Method | Seconds |
|---|---|
| PyMuPDF, default | 0.27 |
| PyMuPDF, dpi 150 | 0.43 |
| PyMuPDF, dpi 300 | 0.49 |
| pytesseract, dpi 300 | 1.25 |
| ocrmypdf | 1.93 |
Wall time for one page, 6 pages at a time, one thread each. ocrmypdf also writes a new PDF.
Tables in your scans? Our Actor reads them
PDF OCR to Markdown, our Apify Actor, reads each table cell with Tesseract and checks the table's math.
- Its code read 3,198 of 3,212 invoice numbers.
- Its table code was tuned on half of these invoices. On the other half it read 1,594 of 1,598.
- It took 1.34 s a page on this Mac.
- $0.006 a page with text, plus $0.00005 a run at the default 1 GB.
- The Apify free plan lets you try 20 pages a run and 50 a month.
Questions people ask
Does PyMuPDF have OCR?
Yes. get_textpage_ocr() and Pixmap.pdfocr_tobytes() use a copy of Tesseract that is built into PyMuPDF. You install Tesseract only for its language files.
What does the "No tessdata specified" error mean?
PyMuPDF raised "No tessdata specified and Tesseract is not installed" because it found no language files. Install Tesseract, or pass tessdata= with the folder that holds eng.traineddata. With tessdata= set, OCR worked with no tesseract program on the PATH.
Why do I get "Tesseract language initialisation failed"?
The folder has no file for that language, for example deu.traineddata for German. Check TESSDATA_PREFIX, or install the language.
Why does pdfocr_tobytes give no text?
In my test, a gray pixmap gave 0 words, and the same pixmap as RGB gave 57. Convert it first: pymupdf.Pixmap(pymupdf.csRGB, pix).
PyMuPDF OCR or ocrmypdf?
ocrmypdf writes a searchable PDF, and it read the grid tables. PyMuPDF stays inside Python and was about 4 times faster, but it lost the text inside grids.
Method, limits and sources
How I tested it
127 scanned pages, each a JPEG put into a one-page PDF with no change. 55 are printed book pages from Wikisource, with text that two people proofread.
72 are made-up invoices with ruled tables. Chrome drew them, then they got a tilt, blur, noise and JPEG grain, at 300 dpi. 5 more book pages are handwriting, so I left them out.
Books: true words found in order, exact case. Invoices: each number in the rows, found exactly, in any order. Speed: median wall time for one page.
The clean table has 30 numbers, drawn by PyMuPDF and read at 300 dpi. Scripts, true text and results are in research/bench/pymupdf-ocr-2026-11-02/.
What I did not test
- Real invoices. These are made up.
- Real scans with lines under rows only.
- Scans at other resolutions, or other languages.
- Linux and Windows.
Versions and sources
- PyMuPDF 1.28.2, whose Tesseract calls itself 5.0.0-alpha. Tesseract 5.5.3 from Homebrew, tessdata_fast eng model.
- pytesseract 0.3.13 and ocrmypdf 17.13.0, on a Mac, November 2, 2026.
- OpenCV 5.0.0 and Python 3.12.
- PyMuPDF source: get_textpage_ocr(), dpi default 72
- PyMuPDF Page docs
- We make the Apify Actor. PyMuPDF, Tesseract, ocrmypdf and OpenCV are open source tools we do not make.