Independent guide. Not affiliated with the pdfplumber project or its maintainers.Official repository: jsvine/pdfplumber ↗

Beyond text

Scanned PDFs and OCR

pdfplumber reads the text layer of a PDF; it does not recognise text in images. A scanned page has no text layer, so pdfplumber sees an image and zero characters. Add a text layer with OCR first, and pdfplumber can read the result. We did that to a scanned invoice and recovered 38 of its 40 table cells exactly.

Tested with pdfplumber 0.11.10 · Python 3.13Updated 2026-11-04

Short answer

No, pdfplumber does not do OCR. Check whether a page has a text layer, and if it does not, run OCR before using pdfplumber:

Terminal
python -m pip install ocrmypdf          # also needs the Tesseract program
ocrmypdf --deskew --oversample 300 -l eng scanned.pdf searchable.pdf

Then pdfplumber.open("searchable.pdf") works as usual. The rest of this page shows how to detect scanned pages, what changes after OCR, and how to get tables out.

Detect pages that need OCR

A scanned page has no characters and one large image. Two numbers tell you:

needs_ocr.py
import pdfplumber

def page_needs_ocr(page, min_chars=20, min_image_share=0.5):
    """True when a page is (mostly) an image with little or no text layer."""
    if len(page.chars) >= min_chars:
        return False
    area = page.width * page.height
    image_share = max(((im["x1"] - im["x0"]) * (im["bottom"] - im["top"]) / area
                       for im in page.images), default=0)
    return image_share >= min_image_share

for name in ["invoice", "scanned-invoice", "form"]:
    with pdfplumber.open(f"samples/{name}.pdf") as pdf:
        page = pdf.pages[0]
        print(f"{name:16} chars={len(page.chars):4}  images={len(page.images)}  needs OCR: {page_needs_ocr(page)}")
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
invoice          chars= 851  images=0  needs OCR: False
scanned-invoice  chars=   0  images=1  needs OCR: True
form             chars= 159  images=0  needs OCR: False

Run this before OCR so you only process pages that need it; OCR is slow (seconds per page) and can slightly change pages that already had good text. Mixed documents, such as a typed report with a scanned signature page, are common. The PDF Doctor runs the same check on your own file.

Add a text layer with OCRmyPDF

OCRmyPDF runs Tesseract on every page and writes an invisible text layer on top of the original image, so the PDF looks the same but now contains characters. It needs the Tesseract program installed: apt install tesseract-ocr on Debian/Ubuntu, brew install tesseract on macOS, or the installer from the Tesseract project on Windows. Our sister site tesseractocr.org covers installation and languages in depth.

ocr_then_extract.py
import time
import ocrmypdf
import pdfplumber

t0 = time.perf_counter()
ocrmypdf.ocr("samples/scanned-invoice.pdf", "invoice-ocr.pdf",
             language="eng", deskew=True, oversample=300, progress_bar=False)
print(f"OCR took {time.perf_counter() - t0:.1f} s")

with pdfplumber.open("invoice-ocr.pdf") as pdf:
    page = pdf.pages[0]
    print(len(page.chars), "chars now, font:", {c["fontname"] for c in page.chars})
    print("\n".join(page.extract_text().splitlines()[:7]))
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
OCR took 3.0 s
931 chars now, font: {'MPDFAA+NotoSans'}
Hollow Pine Supply Co. INVOICE
418 Quarry Road, Unit 6
Millbrook, OR 97000 Invoice no. INV-2026-0417
billing@hollowpine.example Issue date 2026-10-03
Due date 2026-11-02
PO number PO-88213
BILL TO

The 931 characters are Tesseract’s guesses, placed where the words were found, in a placeholder font. pdfplumber treats them like any other text: extract_text(), extract_words(), search() and cropping all work.

OCR options change what pdfplumber reads

We OCR’d the same scanned invoice three ways and compared pdfplumber’s extract_text() with the text of the born-digital original:

Similarity to the original text after OCR (%)
  • no options95 % 2 s
  • --deskew94.1 % 2.1 s
  • --deskew --oversample 30096.5 % 3.3 s

Source: our measurement, 2026-11-04: tesseract 5.3.4, OCRmyPDF 17.13.0, scanned-invoice.pdf; script lab/bench/ocr_options.py

--deskew alone made things slightly worse on this page: it straightened the slight tilt, but two small grey address lines were then read as one interleaved line. Adding --oversample 300 (upsampling the image before recognition) fixed that and gave the best result. Measure on a few of your own pages; the right options depend on scan quality.

Errors that remain are OCR errors, not pdfplumber errors: here, Feldstraße came out as FeldstraRe without oversampling. For non-English documents, install the language data and pass it with -l (for example -l deu+eng).

Tables from scanned PDFs

After OCR there is still a catch: the table’s ruling lines are pixels in the image, not drawn lines, so the default table finder finds nothing. And Tesseract often reads a vertical rule as a | character. Remove those, then rebuild the columns from the gaps between words, the same recipe used for borderless tables:

ocr_table.py
import pdfplumber

with pdfplumber.open("invoice-ocr.pdf") as pdf:          # made by the previous example
    page = pdf.pages[0]
    print("default table:", page.extract_table())        # the rules are pixels, not lines

    # OCR often reads ruling lines as "|"; drop those characters first
    page = page.filter(lambda o: o["object_type"] != "char" or o["text"] not in "|[]")
    words = page.extract_words()
    top = next(w for w in words if w["text"] == "Item")["top"] - 6
    bottom = next(w for w in words if w["text"].startswith("Subtotal"))["top"] - 6
    region = page.crop((40, top, 572, bottom))

    # Same gutter recipe as for borderless tables
    spans = sorted((w["x0"], w["x1"]) for w in region.extract_words())
    xs, right = [region.bbox[0]], spans[0][1]
    for x0, x1 in spans[1:]:
        if x0 > right + 6:
            xs.append((right + x0) / 2)
        right = max(right, x1)
    xs.append(region.bbox[2])
    lines = region.extract_text_lines()
    ys = [region.bbox[1]] + [(a["bottom"] + b["top"]) / 2 for a, b in zip(lines, lines[1:])] + [region.bbox[3]]

    table = region.extract_table({"vertical_strategy": "explicit", "explicit_vertical_lines": xs,
                                  "horizontal_strategy": "explicit", "explicit_horizontal_lines": ys})
    for row in [r for r in table if any(c.strip() for c in r)][:5]:
        print(row)
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
default table: None
['Item', 'Description', 'Qty', 'Unit price', 'Amount']
['HP-1042', 'Stainless hex bolts M8 x 40 (box of 100)', '12', '18.40', '220.80']
['HP-2210', 'Aluminium extrusion 2020, 1 m', '40', '6.95', '278.00']
['HP-3307', 'Linear bearing LM8UU', '64', '1.85', '118.40']
['HP-4120', 'Timing belt GT2, 6 mm (5 m roll)', '6', '11.20', '67.20']

On this invoice, 38 of the 40 cells match the original exactly; the two misses are a dropped space (1.5A for 1.5 A, 1m for 1 m). Every number is right. That is typical: OCR’d tables need a cleaning pass, but geometry-based extraction on top of a good OCR layer works.

OCR only the part you need

For one field on many scanned forms, such as a total or an ID, you do not need to OCR whole pages. Render the page with pdfplumber, cut out the region in pixels and pass it to Tesseract directly:

ocr_region.py
import pdfplumber
import pytesseract

with pdfplumber.open("samples/scanned-invoice.pdf") as pdf:
    page = pdf.pages[0]
    dpi = 300
    image = page.to_image(resolution=dpi).original      # a Pillow image of the page

    # The total sits roughly here, in points: (x0, top, x1, bottom)
    box = (400, 465, 560, 490)
    scale = dpi / 72
    crop = image.crop(tuple(round(v * scale) for v in box))
    print(repr(pytesseract.image_to_string(crop).strip()))
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
'[Total due (USD) 1,118.23'

The box is in pdfplumber’s points; multiply by dpi / 72 to get pixels. The stray [ is the edge of a table rule caught in the crop: tighten the box, or strip punctuation you do not expect. to_image() needs pypdfium2, which pdfplumber installs; pytesseract needs the Tesseract program.

When to use something else

Situation Better tool
Mostly scanned documents, plain text needed OCRmyPDF + pdfplumber (this page)
Scanned tables with complex structure, merged cells A layout model such as docling, or a vision LLM (comparison)
Handwriting Handwriting-capable OCR services; Tesseract is weak at it
Already OCR’d but the text layer is poor Re-run OCR with --redo-ocr, or --force-ocr to rasterise and replace it

Check your file

The PDF Doctor below reports, page by page, whether there is a text layer, whether pages are image-only, and other problems ((cid:x) fonts, duplicated characters). It runs pdfplumber in your browser; your file is not uploaded.

PDF Doctor

Loads Pyodide + pdfplumber 0.11.10 (about 20 MB, cached after the first visit). Your PDF never leaves this device.
No file yet
x — · top —

Drop a PDF here or choose a sample above.

Text-based PDFs work best. Scanned pages have nothing for pdfplumber to read until OCR adds a text layer.

Text and objects

Hover over text to inspect it.
Run pdfplumber to see extract_text() for this page.
Python for this view