PDF Recipes
Contents
- Read / extract (text, tables, metadata, images)
- Create (Platypus documents, Canvas overlays)
- Modify (split, rotate, watermark, encrypt/decrypt, forms)
- Convert (PDF → text/CSV, images → PDF, OCR)
- Gotchas
Read / extract
Text, page by page (memory-safe on large files):
python
import pdfplumber
with pdfplumber.open("in.pdf") as pdf:
for i, page in enumerate(pdf.pages, 1):
text = page.extract_text() or "" # None on empty/scanned pages
print(f"--- page {i} ---\n{text}")Tables → CSV:
python
import csv, pdfplumber
with pdfplumber.open("in.pdf") as pdf, open("out.csv", "w", newline="") as f:
w = csv.writer(f)
for page in pdf.pages:
for table in page.extract_tables():
w.writerows(table)Metadata and page count:
python
from pypdf import PdfReader
r = PdfReader("in.pdf")
print(len(r.pages), r.metadata) # metadata may be NoneExtract embedded images: pdfimages -png in.pdf img_prefix (poppler-utils).
Create
Structured document (Platypus):
python
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
styles = getSampleStyleSheet()
story = [
Paragraph("Quarterly Report", styles["Title"]),
Spacer(1, 0.2 * inch),
Paragraph("Summary paragraph.", styles["Normal"]),
Table([["Region", "Revenue"], ["East", "1,200"], ["West", "980"]],
style=TableStyle([("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey)])),
]
SimpleDocTemplate("report.pdf", pagesize=letter).build(story)Low-level drawing (Canvas) — for precise placement (stamps, labels):
python
from reportlab.pdfgen import canvas
c = canvas.Canvas("stamp.pdf")
c.setFont("Helvetica", 36)
c.saveState(); c.translate(300, 400); c.rotate(45)
c.setFillGray(0.5, 0.3) # 30% opacity grey watermark text
c.drawCentredString(0, 0, "CONFIDENTIAL")
c.restoreState(); c.save()Modify
Split — one file per page:
python
from pypdf import PdfReader, PdfWriter
r = PdfReader("in.pdf")
for i, page in enumerate(r.pages, 1):
w = PdfWriter(); w.add_page(page); w.write(f"page-{i:03d}.pdf")Extract a page range (1-based, inclusive):
python
w = PdfWriter()
w.append("in.pdf", pages=(4, 10)) # pypdf's range is 0-based, end-exclusive: pages 5–10
w.write("excerpt.pdf")Rotate all pages 90° clockwise:
python
r = PdfReader("in.pdf"); w = PdfWriter()
for page in r.pages:
w.add_page(page.rotate(90)) # rotate() mutates and returns the page
w.write("rotated.pdf")Watermark every page (build stamp.pdf with the Canvas recipe above):
python
r = PdfReader("in.pdf"); stamp = PdfReader("stamp.pdf").pages[0]
w = PdfWriter()
for page in r.pages:
page.merge_page(stamp) # stamp drawn over the page content
w.add_page(page)
w.write("watermarked.pdf")Encrypt / decrypt:
python
w = PdfWriter(); w.append("in.pdf")
w.encrypt(user_password="secret", algorithm="AES-256")
w.write("locked.pdf")
r = PdfReader("locked.pdf")
if r.is_encrypted:
r.decrypt("secret") # returns PasswordType; 0 means wrong passwordFill form fields:
python
r = PdfReader("form.pdf"); w = PdfWriter(); w.append(r)
print(r.get_fields().keys()) # inspect field names FIRST — they rarely match labels
w.update_page_form_field_values(w.pages[0], {"name": "Alice", "date": "2026-08-05"})
w.write("filled.pdf")Convert
PDF → plain text preserving layout: pdftotext -layout in.pdf out.txt
Images → PDF:
python
from PIL import Image
pages = [Image.open(p).convert("RGB") for p in ["a.png", "b.png"]]
pages[0].save("out.pdf", save_all=True, append_images=pages[1:])OCR a scanned PDF:
python
# pip install pytesseract pdf2image ; also: brew install tesseract poppler
import pytesseract
from pdf2image import convert_from_path
# 300 dpi is the standard OCR sweet spot: below it accuracy drops, above it is slow
text = "\n".join(pytesseract.image_to_string(img)
for img in convert_from_path("scan.pdf", dpi=300))Gotchas
- pdfplumber on scanned PDFs returns
None/empty text — that is a signal to OCR, not a bug. extract_text()can returnNone; alwaysor ""before joining.- pypdf
append(pages=...)is 0-based and end-exclusive while users speak 1-based inclusive — convert deliberately. page.rotate()mutates in place (and returns the page); don't rotate twice by chaining carelessly.- Form fields keep old values visually in some viewers unless you also set
w.set_need_appearances_writer(True)before writing. - Encryption: pypdf's default RC4 is weak — always pass
algorithm="AES-256". - Watermark order:
page.merge_page(stamp)draws the stamp over content; to put it under, merge the page onto the stamp instead. - reportlab coordinates start at the bottom-left in points (1 pt = 1/72 inch).
- Table extraction depends on ruled lines; borderless tables need
extract_tables({"vertical_strategy": "text", "horizontal_strategy": "text"}).