Convert a scanned PDF to a searchable PDF in Python
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+Fworks 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:
| Flag | What it does | When to use |
|---|---|---|
skip_text=True | Skips OCR on pages that already have a text layer | Mixed digital/scanned documents — almost always want this |
force_ocr=True | Rasterizes and re-OCRs every page, replacing any existing text | Existing text layer is known bad (garbled, wrong language) |
deskew=True | Straightens rotated/crooked scans before OCR | Scans from a flatbed or phone camera, rarely a professional scanner |
clean=True | Runs unpaper to remove speckle/noise | Old or low-quality scans |
redo_ocr=True | Re-OCRs pages, keeping the existing text layer as a fallback if OCR fails | Upgrading 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.