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

Text

Coordinates, crop() and bounding boxes

Everything pdfplumber finds has a position, and every region you ask for is a box in the same coordinates. Once the coordinate system is clear, pulling a field from a form, dropping running headers or reading only one column is a one-liner.

Tested with pdfplumber 0.11.10 · Python 3.13Updated 2026-11-04Source of truth: official README ↗

Short answer

Boxes are (x0, top, x1, bottom) in points, measured from the top-left corner of the page. Crop to a box and use the result like a page:

crop.py
import pdfplumber

with pdfplumber.open("samples/invoice.pdf") as pdf:
    page = pdf.pages[0]
    # (x0, top, x1, bottom) in points, measured from the top-left corner
    bill_to = page.crop((50, 125, 300, 225))
    print(bill_to.extract_text())
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
BILL TO
Larkspur Robotics GmbH
Attn: Accounts Payable
Feldstraße 12
20357 Hamburg, Germany

Not sure of the numbers? Drag a box in the measuring tool below and copy them.

The coordinate system

pdfplumber coordinate systemA page with its origin at the top-left corner. A word's box is described by x0 and x1 measured from the left edge, and top and bottom measured from the top edge. PDF-native y0 and y1 are measured from the bottom edge. doctop adds the heights of all earlier pages to top.page 1 (0, 0) is the top-left cornerInvoicex0x1topbottomy0y1height = 792 pt (US Letter)page 1page 2doctoptopdoctop = top + heights of earlier pages
All values are in PDF points (1/72 inch). x0, top, x1, bottom are measured from the page's top-left corner, the convention for every bbox you pass to pdfplumber. y0, y1 are the PDF's native coordinates, measured up from the bottom edge. doctop is top plus the height of every earlier page.

Every char, word, line, rect and table cell carries these keys. A real word from the second page of report.pdf:

coordinates.py
import pdfplumber

with pdfplumber.open("samples/report.pdf") as pdf:
    page = pdf.pages[1]                       # second page
    word = page.extract_words()[1]
    print("page size:", page.width, "x", page.height)
    for key in ["text", "x0", "x1", "top", "bottom", "doctop"]:
        value = word[key]
        print(f"{key:7}", round(value, 2) if isinstance(value, float) else value)

    char = page.chars[0]
    print("char y0/y1 (from the bottom):", round(char["y0"], 2), round(char["y1"], 2))
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
page size: 612 x 792
text    Pine
x0      80.22
x1      96.23
top     27.66
bottom  35.66
doctop  819.66
char y0/y1 (from the bottom): 756.34 764.34

doctop is 819.66 because the word sits 27.66 pt from the top of page 2, and page 1 is 792 pt tall. Use doctop when you sort or compare positions across a whole document; use top within a page. The PDF’s own y0/y1 (from the bottom) are there too, but every pdfplumber method that takes a box expects the top-left convention.

crop(), within_bbox() and outside_bbox()

The three methods take the same box and differ only in what happens to objects on the boundary:

Method Keeps Objects crossing the edge
crop(bbox) objects at least partly inside kept, trimmed to the box
within_bbox(bbox) objects entirely inside dropped
outside_bbox(bbox) objects entirely outside dropped

Here is a box that slices through two lines of the invoice’s address block:

x — · top —
A bounding box that cuts through the words BILL TO and Larkspur Robotics GmbH
The box (dashed) catches the bottom of BILL TO and cuts through the final s of Robotics. Source: invoice.pdf (pdfplumber.dev sample, CC0)
crop_vs_within.py
import pdfplumber

with pdfplumber.open("samples/invoice.pdf") as pdf:
    page = pdf.pages[0]
    box = (50, 160, 130, 180)            # cuts through "Larkspur Robotics GmbH"

    print("crop:        ", repr(page.crop(box).extract_text()))
    print("within_bbox: ", repr(page.within_bbox(box).extract_text()))
    print("outside_bbox:", repr(page.outside_bbox(box).extract_text()[:32]), "...")
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
crop:         'BILL TO\nLarkspur Robotics'
within_bbox:  'Larkspur Robotic'
outside_bbox: 'Hollow Pine Supply Co. INVOICE\n4' ...

crop() keeps BILL TO (only its lower edge is inside) and the whole of Robotics, because both are partly inside. within_bbox() keeps only characters entirely inside, so the s that crosses the right edge is lost. When you crop to read a field, give the box a few points of margin; when you crop to exclude something, within_bbox() is the stricter choice.

Boxes must fit inside the page

By default (strict=True) a box that extends past the page raises an error:

strict_crop.py
import pdfplumber

with pdfplumber.open("samples/invoice.pdf") as pdf:
    page = pdf.pages[0]
    page.crop((0, 700, 612, 800))         # bottom edge 800 > page height 792
Output (the error)captured · Python 3.13.18 · pdfplumber 0.11.10
Traceback (most recent call last):
  …
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.13/site-packages/pdfplumber/page.py", line 671, in test_proposed_bbox
    raise ValueError(
    ...<2 lines>...
    )
ValueError: Bounding box (0, 700, 612, 800) is not fully within parent page bounding box (0, 0, 612, 792)

Clamp your box to page.bbox, or pass strict=False to allow it.

Cropping a cropped page

A cropped page keeps the original page’s coordinates, so a second crop uses the same numbers as the first. Pass relative=True to give the box as an offset from the cropped region’s top-left corner instead. Both of these select the same area:

relative_crop.py
import pdfplumber

with pdfplumber.open("samples/invoice.pdf") as pdf:
    page = pdf.pages[0]
    top_half = page.crop((0, 0, page.width, page.height / 2))
    absolute = top_half.crop((50, 160, 300, 220))
    relative = page.crop((0, 100, page.width, 400)).crop((50, 60, 300, 120), relative=True)
    print(absolute.bbox)
    print(relative.bbox)
    print(absolute.extract_text() == relative.extract_text())
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
(50, 160, 300, 220)
(50, 160, 300, 220)
True

Remove headers and footers

Running headers and page numbers sit in fixed strips at the top and bottom of every page. Crop them away before extracting:

drop_headers_footers.py
import pdfplumber

with pdfplumber.open("samples/two-column.pdf") as pdf:
    for page in pdf.pages:
        print(f"--- page {page.page_number}: first and last line, before and after ---")
        lines = page.extract_text().splitlines()
        print(lines[0], "|", lines[-1])

        body = page.crop((0, 50, page.width, page.height - 45))   # 50 pt top, 45 pt bottom
        lines = body.extract_text().splitlines()
        print(lines[0][:45], "|", lines[-1][-40:])
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
--- page 1: first and last line, before and after ---
THE DATA DESK · FIELD NOTES November 2026 | Page 1 of 2
Reading water-quality reports at scale | out in a table on page 27. The table has
--- page 2: first and last line, before and after ---
THE DATA DESK · FIELD NOTES November 2026 | Page 2 of 2
8 columns and is ruled only between rows, so  | ngs above the action level, set out in a

To find the numbers, hover over the header’s lowest line and the footer’s highest line in the measuring tool, or compute them: the bottom of the header’s last word plus a small margin. When the header height varies between pages, crop to the top of the first word in the body font size instead (font sizes are on words, chars and fonts).

Keep or drop objects with filter()

page.filter(fn) keeps the objects for which fn(obj) is true, and works with any property, not just position. The invoice prints its footer, the company address and the field labels in grey (0.4, 0.4, 0.4) and everything else in near-black, so a colour test separates them:

filter_by_colour.py
import pdfplumber

GREY = (0.4, 0.4, 0.4)

with pdfplumber.open("samples/invoice.pdf") as pdf:
    page = pdf.pages[0]
    colours = {c["non_stroking_color"] for c in page.chars}
    print("text colours on the page:", colours)

    no_grey = page.filter(lambda obj: obj["object_type"] != "char"
                          or obj["non_stroking_color"] != GREY)
    text = no_grey.extract_text()
    print(len(page.chars) - len(no_grey.chars), "grey characters removed")
    print("footer gone:", "fictional" not in text, "| body kept:", "Larkspur Robotics" in text)
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
text colours on the page: {(0.066667, 0.066667, 0.066667), (0.4, 0.4, 0.4)}
268 grey characters removed
footer gone: True | body kept: True

Compare colours exactly as pdfplumber reports them: this text is (0.066667, 0.066667, 0.066667), not pure black (0,), so a test for == (0,) would silently drop everything. Print the set of colours first, as above. And return True for objects you are not testing (here, everything that is not a char), or lines and rects disappear too and table extraction breaks.

Page boxes

A PDF page can declare up to five boxes. pdfplumber exposes all of them (trimbox, bleedbox and artbox since 0.11.7), in the same top-left coordinates:

page_boxes.py
import pdfplumber

# Every page box is set differently on this page
with pdfplumber.open("samples/page-boxes.pdf") as pdf:
    page = pdf.pages[0]
    for name in ["mediabox", "cropbox", "bleedbox", "trimbox", "artbox", "bbox"]:
        print(f"{name:9}", tuple(round(v, 1) for v in getattr(page, name)))

    trimmed = page.crop(page.trimbox)      # only what is inside the trim box
    print(len(page.chars), "chars on the page,", len(trimmed.chars), "inside the trim box")
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
mediabox  (0, 0.0, 623.6, 870.2)
cropbox   (14.2, 42.5, 581.1, 856.1)
bleedbox  (0, 0.0, 623.6, 870.2)
trimbox   (28.4, 56.7, 566.9, 841.9)
artbox    (42.5, 70.9, 552.8, 827.7)
bbox      (0, 0.0, 623.6, 870.2)
321 chars on the page, 248 inside the trim box

page.bbox, which sets width and height and limits cropping, is the MediaBox. PDF viewers usually display the CropBox, so a page can contain objects you never see on screen: printers’ marks, crop marks or text outside the visible area. Crop to page.cropbox to read only what a viewer shows.

Rotated pages

When a page carries a /Rotate setting, pdfplumber reports positions for the page as displayed: a landscape table saved on a portrait page with /Rotate 90 measures 612 × 1008. Its text runs sideways (upright is False for every character), so the default reading order produces one word per line. Tell pdfplumber which way the text runs, and how to write it out:

rotated_page.py
import pdfplumber

# The FBI NICS page saved with /Rotate 90 (from pdfplumber's test suite)
with pdfplumber.open("samples/real/nics-background-checks-2015-11-rotated.pdf") as pdf:
    page = pdf.pages[0]
    print("rotation:", page.rotation, "| size as displayed:", page.width, "x", page.height)
    print("upright chars:", sum(c["upright"] for c in page.chars), "of", len(page.chars))

    print(page.extract_text().splitlines()[:3])         # default: one word per line

    text = page.extract_text(
        line_dir="rtl", char_dir="ttb",                  # how the text runs on the page
        line_dir_rotated="rtl", char_dir_rotated="ttb",
        line_dir_render="ttb", char_dir_render="ltr",    # how to write it out
    )
    print(text.splitlines()[:2])
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
rotation: 90 | size as displayed: 612 x 1008
upright chars: 0 of 4319
['Page', '1 of 205', 'These']
['NICS Firearm Background Checks', 'November - 2015']

The result reads exactly like the unrotated version of the same report. The direction options are described on extract text.

Measure boxes on your own PDF

Drag a rectangle on the page to read its (x0, top, x1, bottom) and compare what crop(), within_bbox() and outside_bbox() return for it. It runs the real pdfplumber in your browser; your file is not uploaded.

BBox Picker

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