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%
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# PDF Recipes78## Contents9- Read / extract (text, tables, metadata, images)10- Create (Platypus documents, Canvas overlays)11- Modify (split, rotate, watermark, encrypt/decrypt, forms)12- Convert (PDF → text/CSV, images → PDF, OCR)13- Gotchas1415## Read / extract1617**Text, page by page (memory-safe on large files):**18```python19import pdfplumber20with pdfplumber.open("in.pdf") as pdf:21 for i, page in enumerate(pdf.pages, 1):22 text = page.extract_text() or "" # None on empty/scanned pages23 print(f"--- page {i} ---\n{text}")24```2526**Tables → CSV:**27```python28import csv, pdfplumber29with pdfplumber.open("in.pdf") as pdf, open("out.csv", "w", newline="") as f:30 w = csv.writer(f)31 for page in pdf.pages:32 for table in page.extract_tables():33 w.writerows(table)34```3536**Metadata and page count:**37```python38from pypdf import PdfReader39r = PdfReader("in.pdf")40print(len(r.pages), r.metadata) # metadata may be None41```4243**Extract embedded images:** `pdfimages -png in.pdf img_prefix` (poppler-utils).4445## Create4647**Structured document (Platypus):**48```python49from reportlab.lib.pagesizes import letter50from reportlab.lib.units import inch51from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle52from reportlab.lib.styles import getSampleStyleSheet53from reportlab.lib import colors5455styles = getSampleStyleSheet()56story = [57 Paragraph("Quarterly Report", styles["Title"]),58 Spacer(1, 0.2 * inch),59 Paragraph("Summary paragraph.", styles["Normal"]),60 Table([["Region", "Revenue"], ["East", "1,200"], ["West", "980"]],61 style=TableStyle([("GRID", (0, 0), (-1, -1), 0.5, colors.grey),62 ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey)])),63]64SimpleDocTemplate("report.pdf", pagesize=letter).build(story)65```6667**Low-level drawing (Canvas)** — for precise placement (stamps, labels):68```python69from reportlab.pdfgen import canvas70c = canvas.Canvas("stamp.pdf")71c.setFont("Helvetica", 36)72c.saveState(); c.translate(300, 400); c.rotate(45)73c.setFillGray(0.5, 0.3) # 30% opacity grey watermark text74c.drawCentredString(0, 0, "CONFIDENTIAL")75c.restoreState(); c.save()76```7778## Modify7980**Split — one file per page:**81```python82from pypdf import PdfReader, PdfWriter83r = PdfReader("in.pdf")84for i, page in enumerate(r.pages, 1):85 w = PdfWriter(); w.add_page(page); w.write(f"page-{i:03d}.pdf")86```8788**Extract a page range (1-based, inclusive):**89```python90w = PdfWriter()91w.append("in.pdf", pages=(4, 10)) # pypdf's range is 0-based, end-exclusive: pages 5–1092w.write("excerpt.pdf")93```9495**Rotate all pages 90° clockwise:**96```python97r = PdfReader("in.pdf"); w = PdfWriter()98for page in r.pages:99 w.add_page(page.rotate(90)) # rotate() mutates and returns the page100w.write("rotated.pdf")101```102103**Watermark every page** (build `stamp.pdf` with the Canvas recipe above):104```python105r = PdfReader("in.pdf"); stamp = PdfReader("stamp.pdf").pages[0]106w = PdfWriter()107for page in r.pages:108 page.merge_page(stamp) # stamp drawn over the page content109 w.add_page(page)110w.write("watermarked.pdf")111```112113**Encrypt / decrypt:**114```python115w = PdfWriter(); w.append("in.pdf")116w.encrypt(user_password="secret", algorithm="AES-256")117w.write("locked.pdf")118119r = PdfReader("locked.pdf")120if r.is_encrypted:121 r.decrypt("secret") # returns PasswordType; 0 means wrong password122```123124**Fill form fields:**125```python126r = PdfReader("form.pdf"); w = PdfWriter(); w.append(r)127print(r.get_fields().keys()) # inspect field names FIRST — they rarely match labels128w.update_page_form_field_values(w.pages[0], {"name": "Alice", "date": "2026-08-05"})129w.write("filled.pdf")130```131132## Convert133134**PDF → plain text preserving layout:** `pdftotext -layout in.pdf out.txt`135136**Images → PDF:**137```python138from PIL import Image139pages = [Image.open(p).convert("RGB") for p in ["a.png", "b.png"]]140pages[0].save("out.pdf", save_all=True, append_images=pages[1:])141```142143**OCR a scanned PDF:**144```python145# pip install pytesseract pdf2image ; also: brew install tesseract poppler146import pytesseract147from pdf2image import convert_from_path148# 300 dpi is the standard OCR sweet spot: below it accuracy drops, above it is slow149text = "\n".join(pytesseract.image_to_string(img)150 for img in convert_from_path("scan.pdf", dpi=300))151```152153## Gotchas154- **pdfplumber on scanned PDFs** returns `None`/empty text — that is a signal to OCR, not a bug.155- **`extract_text()` can return `None`**; always `or ""` before joining.156- **pypdf `append(pages=...)` is 0-based and end-exclusive** while users speak 1-based inclusive — convert deliberately.157- **`page.rotate()` mutates in place** (and returns the page); don't rotate twice by chaining carelessly.158- **Form fields keep old values visually** in some viewers unless you also set159 `w.set_need_appearances_writer(True)` before writing.160- **Encryption:** pypdf's default RC4 is weak — always pass `algorithm="AES-256"`.161- **Watermark order:** `page.merge_page(stamp)` draws the stamp *over* content; to put it under, merge the page onto the stamp instead.162- **reportlab coordinates** start at the bottom-left in points (1 pt = 1/72 inch).163- **Table extraction** depends on ruled lines; borderless tables need164 `extract_tables({"vertical_strategy": "text", "horizontal_strategy": "text"})`.165