Tools,

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

Words read right, 55 book scans
PyMuPDF, default
86.3%
PyMuPDF, dpi 150
94.3%
PyMuPDF, dpi 300
94.0%
page_text() above
93.6%
pytesseract
94.3%
ocrmypdf
94.3%

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.
Numbers right, of 3,212, on 72 invoice scans
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

Median seconds a page, 55 book scans
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.

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.