| name | pdf-sign |
| description | This skill should be used when the user asks to "sign this PDF", "fill in this form", "add my signature to", or mentions signing letters, claim forms, or contracts. Overlays a signature image, ticks checkboxes, and fills date/name/phone fields on scanned or digital PDFs using pymupdf, with OCR fallback for scanned documents. |
PDF Sign
Sign and fill PDF forms programmatically. Works on both digital PDFs (with a text layer) and scanned PDFs (via OCR).
Signature image
Always ask the user where their signature image is — never assume a location, and never commit a signature image to a repository. A scanned signature (dark ink on white) works best.
If the image has a white background (typical JPG scan), convert it to a transparent PNG first — otherwise the overlay blanks out any text or lines beneath it:
from PIL import Image
img = Image.open(SRC_JPG).convert("RGBA")
img.putdata([(0, 0, 0, 0 if (r+g+b)//3 > 200 else 255-(r+g+b)//3)
for r, g, b, a in img.getdata()])
img.save(OUT_PNG)
Save the transparent version next to the original so it can be reused.
Setup
pymupdf in a venv (system Python is often externally managed):
python3 -m venv ./scratch/venv
./scratch/venv/bin/pip install -q pymupdf pillow
OCR (scanned PDFs) needs tesseract: brew install tesseract, and set TESSDATA_PREFIX (Homebrew: /opt/homebrew/share/tessdata).
Approach
- Try text search first.
page.search_for("SIGNATURE:") — a digital PDF locates fields directly.
- Scanned PDF →
page.get_text() returns empty. Fall back to an OCR textpage:
tp = page.get_textpage_ocr(full=True, dpi=200)
hits = page.search_for("SIGNATURE:", textpage=tp)
- Place content relative to the found label rects (see recipe).
- Always render and visually verify each modified page before declaring done:
page.get_pixmap(dpi=100).save("check.png")
Read the PNG — misplaced marks are common on the first pass.
Recipe
import os
os.environ.setdefault("TESSDATA_PREFIX", "/opt/homebrew/share/tessdata")
import pymupdf
doc = pymupdf.open(SRC)
page = doc[0]
tp = page.get_textpage_ocr(full=True, dpi=200)
def find(needle):
hits = page.search_for(needle, textpage=tp)
return hits[0] if hits else None
r = find("SIGNATURE:")
page.insert_image(pymupdf.Rect(r.x1 + 40, r.y0 - 32, r.x1 + 180, r.y1 + 4),
filename=SIGNATURE_PNG, keep_proportion=True)
r = find("DATE:")
page.insert_text((r.x1 + 15, r.y1 - 2), "01/31/2026", fontsize=11, color=(0, 0, 0.55))
r = find("Check here if your name")
page.insert_text((r.x0 - 22, r.y1 - 2), "X", fontsize=12, color=(0, 0, 0.55))
doc.save(OUT)
Blue-ish ink (color=(0, 0, 0.55)) reads as pen on printed forms. Match the form's date format (US forms: MM/DD/YYYY).
Gotchas
| Problem | Fix |
|---|
search_for finds nothing, no error | Scanned page with no text layer — check len(page.get_text()) == 0, use OCR textpage |
Label matches twice (e.g. DATE: also hits Check Date: header) | Filter hits by y-position, e.g. same line as the SIGNATURE match: abs(h.y1 - rsig.y1) < 8 |
| Partial-phrase match places mark mid-sentence | Anchor x on a reliably-found label (e.g. checkbox column from the first checkbox label), use the phrase match only for y |
| Checkbox X lands on text | Checkbox is left of label: x0 - 22 works for typical forms; verify visually |
| Signature overlay blanks out text/lines beneath it | White image background — convert to transparent PNG (see above) |
| Multi-copy documents (same letter × N pages) | Loop the letter pages, skip FAQ/back pages |
| Draft email already carries the old attachment after re-signing | Mail clients attach a snapshot; discard the draft and recreate it with the new file |
import fitz deprecation warning | Use import pymupdf |
Workflow
- Ask the user for the signature image location (and any fill values: date, printed name, phone)
- Save/locate the source PDF
- Sign to a new file
<name> - Signed.pdf — never overwrite the source
- Render pages → view the PNGs → verify placement
- Iterate until correct, then hand back or attach to a draft email