Crop PDF margins locally with Python: keep a separate reading copy
Choose the margins, change the visible page boundary, and check every page of the new copy. Cropping is not redaction.
Make the page fit the screen better
Wide margins can make PDF text look small when a viewer fits the whole page to a tablet screen. This recipe trims a chosen amount from each edge of every page, locally with Python. It creates a separate reading copy. It does not detect blank space automatically: you choose the margins after looking at the document.
Cropping changes the visible page boundary, called CropBox. It does not enlarge the letters inside the PDF, reflow paragraphs, or remove the hidden content. Never use this method to conceal private text. Someone can restore the page boundary or extract content outside it. File size may stay similar or even increase.
Choose the margins before running anything
Use an ordinary, unencrypted PDF with upright pages and no forms or digital signatures. The script refuses pages whose rotation metadata is nonzero or whose UserUnit differs from 1. A page can look upright because a viewer applies rotation metadata; that case is outside this recipe. The script also does not straighten sideways content drawn inside a page.
Look at several page types: the cover, full text, illustrations, footnotes, and odd and even pages. If your viewer has a measuring tool, measure the smallest empty strip at each edge. Otherwise begin with a small trial, such as 2 mm, and inspect the result. Leave breathing room around content. One fixed trim can cut off a page number on a differently laid-out page; this recipe is unsuitable when pages need separate crop choices.
Worked example: an A4 reading copy
Start with a 210 × 297 mm page. Trim 10 mm from the left and right and 5 mm from the top and bottom. The new visible area is 210 − 10 − 10 = 190 mm wide and 297 − 5 − 5 = 287 mm high. The sample’s 100 × 30 mm rectangle remains 100 × 30 mm; only the page boundary changes.
With the original lower-left corner at (0, 0), the new rectangle runs from (10, 5) to (200, 292) mm. PDF coordinates here use 72 units per inch: multiply millimetres by 72 ÷ 25.4. Thus the rectangle is approximately (28.35, 14.17, 566.93, 827.72) units. The script also handles a nonzero page origin and an existing crop: it trims the area shared by CropBox and MediaBox, rather than assuming every page starts at zero.
Download the A4 practice PDF
Original one-page sample with English labels. Save it as input.pdf for the command below. Trim 10 mm from each side and 5 mm from the top and bottom; the 100 × 30 mm rectangle should keep its size.
Prepare a working folder
Use Python 3.9 or newer. Create a folder, put a copy of your PDF there as input.pdf, and save the complete script below as a plain-text file named crop_pdf.py. Open a terminal in that folder. Keep the original elsewhere. The commands below use pypdf 6.21.2, the version used for this example.
On macOS or Linux, run these two setup commands. Installing the package needs a network connection; the cropping script itself does not upload your PDF.
Install pypdf in a separate environment
sh
python3 -m venv .venv-pdf-crop
.venv-pdf-crop/bin/python -m pip install "pypdf==6.21.2"Windows command names
On Windows, create the environment with py -3 -m venv .venv-pdf-crop. In the installation and run commands, replace .venv-pdf-crop/bin/python with .venv-pdf-crop\Scripts\python.exe. No environment activation is needed.
Save as crop_pdf.py
python
import argparse
import io
import math
from pathlib import Path
from pypdf import PdfReader, PdfWriter
from pypdf.generic import RectangleObject
parser = argparse.ArgumentParser(description="Crop visible PDF margins in mm.")
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
for edge in ("left", "right", "top", "bottom"):
parser.add_argument(f"--{edge}", type=float, default=0)
args = parser.parse_args()
trims = [args.left, args.right, args.top, args.bottom]
if any(not math.isfinite(v) or v < 0 for v in trims):
parser.error("Margins must be finite, nonnegative millimetres.")
left, right, top, bottom = [v * 72 / 25.4 for v in trims]
reader = PdfReader(args.input, strict=True)
if reader.is_encrypted:
parser.error("Encrypted PDFs are outside this recipe.")
root = reader.trailer["/Root"]
if "/AcroForm" in root or "/Perms" in root:
parser.error("Forms and signed/restricted PDFs are outside this recipe.")
if not reader.pages:
parser.error("The PDF has no pages.")
boxes = []
for number, page in enumerate(reader.pages, 1):
if page.rotation != 0 or float(page.get("/UserUnit", 1)) != 1:
parser.error(f"Page {number}: requires rotation 0 and UserUnit 1.")
media = [float(v) for v in page.mediabox]
crop = [float(v) for v in page.cropbox]
if any(not math.isfinite(v) for v in media + crop):
parser.error(f"Page {number}: invalid page coordinates.")
x0, y0 = max(media[0], crop[0]), max(media[1], crop[1])
x1, y1 = min(media[2], crop[2]), min(media[3], crop[3])
box = (x0 + left, y0 + bottom, x1 - right, y1 - top)
if box[0] >= box[2] or box[1] >= box[3]:
parser.error(f"Page {number}: margins leave no visible page.")
boxes.append(box)
writer = PdfWriter(clone_from=reader)
for page, box in zip(writer.pages, boxes):
page.cropbox = RectangleObject(box)
buffer = io.BytesIO()
writer.write(buffer)
with args.output.open("xb") as output:
output.write(buffer.getvalue())
print(f"Created {args.output} with {len(boxes)} pages. Check every page.")The script validates every page before creating the new output file.
Crop the worked example
sh
.venv-pdf-crop/bin/python crop_pdf.py "input.pdf" "cropped.pdf" --left 10 --right 10 --top 5 --bottom 5Check the copy and adjust it
Open cropped.pdf in your usual viewer and compare every page with the original. Check all four edges, page numbers, footnotes, links, annotations, and bookmarks you rely on. Page count should be unchanged. The A4 example should show a visible size of 190 × 287 mm in a tool that reports CropBox dimensions; the underlying MediaBox still describes the original page.
To change a margin, rerun from input.pdf using a different output name. Running on cropped.pdf would trim the already cropped boundary again. A FileExistsError means the chosen output already exists: choose another name. Negative, infinite, or excessive margins are rejected. If a command fails or is interrupted, do not use a possibly incomplete new output; inspect it or remove only that new file before retrying.
Compare both PDFs at the same zoom to see that text and marks retain their size. Fit-to-width may display the cropped page larger because less width is wasted on margins. Printing with Fit can also change the scale; use an actual-size workflow when physical measurements matter. This reading copy is not a replacement for the original document, a privacy-cleaned file, or a guarantee that every specialised PDF feature will behave identically.
What we tested
We ran this script with Python 3.12.3 and pypdf 6.21.2 on Linux. MuPDF and Poppler both rendered the example with a 190 × 287 mm visible area and the unchanged 100 × 30 mm rectangle. The four margin labels disappeared from view but remained extractable as text. Tests also covered nonzero page origins, existing crops, multiple pages, rejected inputs, and an existing output file. We did not execute the Windows or macOS commands, test physical printing, or validate every PDF feature.