SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# 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"}).