# 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 None ``` **Extract 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 password ``` **Fill 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 return `None`**; always `or ""` 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"})`.