pdfmux/blog
ocr

Convert a scanned PDF to a searchable PDF in Python

TL;DRHow to add an invisible OCR text layer to a scanned PDF in Python with ocrmypdf, so it stays visually identical but becomes searchable and selectable.

Direct answer: To make a scanned PDF searchable in Python without changing how it looks, use ocrmypdf — it adds an invisible OCR text layer under the scanned image, keeping the original page visually identical while making text selectable and searchable in any PDF viewer. Install with pip install ocrmypdf (needs Tesseract and Ghostscript as system dependencies), then run ocrmypdf input.pdf output.pdf. This is a different problem from extracting data out of a PDF — if you actually need the text or table data as structured output rather than a searchable file, that’s a data-extraction tool like pdfmux, not an OCR-layering tool.


This is a different problem than “PDF data extraction”

Searching “convert scanned PDF to searchable PDF” and “extract text from scanned PDF” sound similar but ask for opposite outputs:

  • Searchable PDF: you want the same PDF file back, unchanged visually, but now Cmd+F works and text is selectable/copyable. Output is a .pdf.
  • Data extraction: you want the text or table data pulled out of the PDF into markdown, JSON, or a database. The PDF file itself is irrelevant afterward. Output is structured data, not a PDF.

If you’re building a document archive, a legal discovery tool, or anything where end users need to open and search the original document in a normal PDF viewer, you want the first one. If you’re feeding documents into a RAG pipeline, a database, or an LLM, you want the second — see PDF extraction for RAG pipelines or OCR PDF extraction in Python instead.

This guide covers the searchable-PDF case.


Method: ocrmypdf (the standard tool for this)

ocrmypdf is purpose-built for exactly this task. It wraps Tesseract for OCR and Ghostscript for PDF manipulation, and it’s what most document-management systems use under the hood.

Install

# System dependencies (Ubuntu/Debian)
sudo apt install tesseract-ocr ghostscript

# macOS
brew install tesseract ghostscript

pip install ocrmypdf

Basic usage

ocrmypdf input.pdf output.pdf

That’s the entire command for a straightforward scanned PDF. It runs OCR, generates the invisible text layer, and writes a new PDF with the original image untouched.

From Python

import ocrmypdf

ocrmypdf.ocr(
    "scanned-report.pdf",
    "scanned-report-searchable.pdf",
    deskew=True,        # straighten crooked scans before OCR
    clean=True,          # remove scan noise for better OCR accuracy
    skip_text=True,      # leave pages with existing text layers alone
    optimize=1,           # light image compression, keeps quality
)

Key flags:

FlagWhat it doesWhen to use
skip_text=TrueSkips OCR on pages that already have a text layerMixed digital/scanned documents — almost always want this
force_ocr=TrueRasterizes and re-OCRs every page, replacing any existing textExisting text layer is known bad (garbled, wrong language)
deskew=TrueStraightens rotated/crooked scans before OCRScans from a flatbed or phone camera, rarely a professional scanner
clean=TrueRuns unpaper to remove speckle/noiseOld or low-quality scans
redo_ocr=TrueRe-OCRs pages, keeping the existing text layer as a fallback if OCR failsUpgrading OCR quality on an already-searchable archive

Batch processing a directory

import ocrmypdf
from pathlib import Path

input_dir = Path("scanned_pdfs")
output_dir = Path("searchable_pdfs")
output_dir.mkdir(exist_ok=True)

for pdf_path in input_dir.glob("*.pdf"):
    out_path = output_dir / pdf_path.name
    try:
        ocrmypdf.ocr(pdf_path, out_path, skip_text=True, deskew=True)
        print(f"OK: {pdf_path.name}")
    except ocrmypdf.exceptions.PriorOcrFoundError:
        print(f"SKIP (already searchable): {pdf_path.name}")
    except Exception as e:
        print(f"FAILED: {pdf_path.name}{e}")

PriorOcrFoundError is worth catching explicitly — it means the PDF already has a text layer and ocrmypdf refuses to double-OCR it by default, which is the correct behavior for a batch job pulling from an unknown mix of files.


Language support

Tesseract (and by extension ocrmypdf) supports 100+ languages, but only the ones you’ve installed language data for. English-only installs silently produce garbage on other languages rather than erroring:

# List installed language packs
tesseract --list-langs

# Install additional languages (Ubuntu/Debian)
sudo apt install tesseract-ocr-fra tesseract-ocr-deu tesseract-ocr-ara

# Pass to ocrmypdf
ocrmypdf --language eng+fra input.pdf output.pdf

For a document with mixed languages on the same page — common in legal and international business documents — pass multiple language codes with +. Tesseract runs recognition against all specified languages and picks the best match per region, at some cost to speed.

Common failure modes

Output file is huge. ocrmypdf recompresses images by default, but aggressive source scans (600+ DPI, uncompressed TIFF-in-PDF) can still bloat. Add optimize=3 for maximum compression, or jpeg_quality=60 to trade image fidelity for size.

OCR text doesn’t line up with the visible text. This happens when the page was deskewed for OCR but the visible image wasn’t rotated to match. Always let deskew=True handle both, rather than pre-rotating the PDF yourself and then running ocrmypdf on top.

“Prior OCR found” error on a batch you know is unprocessed. Some scanners embed an empty or placeholder text layer even on pure image PDFs. Use force_ocr=True to override, or inspect the existing layer first with pdfmux analyze file.pdf — its page classifier will tell you if the PDF has real text or is scanned-in-disguise, which is a cheaper check than force-OCRing everything.

Ghostscript version errors. ocrmypdf pins compatible Ghostscript version ranges; a system Ghostscript that’s too new or too old fails silently on optimize steps. Check ocrmypdf --version output, which prints dependency versions, if output PDFs come back unoptimized.


Verifying the text layer actually worked

Don’t assume success from a zero exit code. ocrmypdf can complete without error and still produce a text layer that’s misaligned, empty on some pages, or garbled on low-quality scans. Check it programmatically before shipping the file:

import pypdf

def verify_searchable(pdf_path: str, min_chars_per_page: int = 20) -> list[int]:
    """Returns page numbers (0-indexed) with suspiciously little extracted text."""
    reader = pypdf.PdfReader(pdf_path)
    weak_pages = []
    for i, page in enumerate(reader.pages):
        text = page.extract_text()
        if len(text.strip()) < min_chars_per_page:
            weak_pages.append(i)
    return weak_pages

weak = verify_searchable("contract-searchable.pdf")
if weak:
    print(f"Pages with little/no OCR text: {weak} — inspect manually")

A page that comes back empty after OCR usually means the source scan was too degraded for Tesseract to find any text at all, or that page was blank in the original. Either way, don’t discover it three months later when someone tries to search for a name that’s on page 40 and gets nothing.

PDF/A: the archival variant

If the searchable PDF needs to meet long-term archival standards (common in legal, government, and healthcare records), you likely need PDF/A rather than a plain searchable PDF. PDF/A embeds all fonts, disallows external references, and locks color profiles — it’s a stricter, self-contained format designed to render identically decades from now, independent of what software opens it.

ocrmypdf supports this directly:

ocrmypdf --output-type pdfa input.pdf output.pdf
ocrmypdf.ocr("input.pdf", "output.pdf", output_type="pdfa", skip_text=True)

Check whether your use case actually requires PDF/A before adding it — it’s a stricter, sometimes-larger output than a standard searchable PDF, and matters mainly when there’s a compliance or long-term-retention requirement driving the choice, not as a default.

When you actually want both

Some workflows need a searchable archive copy and structured data extracted from the same documents — for example, a legal team that wants documents viewable/searchable in their document management system, while a separate pipeline extracts contract clauses into a database.

In that case, run both tools against the same source PDF independently — they don’t need to run in sequence, and running pdfmux against the OCR’d output rather than the original doesn’t help (pdfmux does its own page classification and OCR routing regardless of an existing text layer):

import ocrmypdf
from pdfmux import convert

# Searchable copy for humans
ocrmypdf.ocr("contract.pdf", "contract-searchable.pdf", skip_text=True)

# Structured data for the database, from the original
result = convert("contract.pdf", format="json", schema="contract")

Two outputs, two purposes, same source file.

Frequently asked questions

Does pdfmux convert scanned PDFs into searchable PDFs?

No. pdfmux extracts data OUT of a PDF into markdown, JSON, CSV, or LLM-ready text — it doesn't write a new PDF file back with an embedded text layer. For that, use ocrmypdf, which is built specifically for producing a visually-identical, searchable PDF.

Will OCR change how my scanned PDF looks?

No, if you use the right tool. ocrmypdf keeps the original scanned image untouched and adds an invisible text layer positioned exactly under the visible text, so the page looks identical but text becomes selectable and searchable.

My scanned PDF has some pages that are already digital text. Will OCR break those?

Not if you use --skip-text (ocrmypdf) to leave existing text layers alone, or --force-ocr only if you specifically need to replace a bad existing layer. Running blind OCR on every page wastes time on pages that already have real text.