scripts/pdf_stamp.py
scripts/pdf_stamp.pyBrowse 21 files
1,387 tokens
5,779 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""Stamp text or an image at coordinates onto selected PDF pages.3 4Builds an in-memory single-page overlay with reportlab, then merges it5onto each selected page with pypdf. Coordinates are PDF points, origin6bottom-left. Covers 'sign here' arrows, diagonal DRAFT banners, and7page-corner labels.8 9Examples:10 pdf_stamp.py in.pdf -o out.pdf --text "DRAFT" --x 200 --y 400 \11 --font-size 60 --rotation 45 --opacity 0.3 --color "#cc0000"12 pdf_stamp.py in.pdf -o out.pdf --image sig.png --x 400 --y 60 \13 --width 120 --pages 314"""15from __future__ import annotations16 17import argparse18import io19import json20import sys21 22 23def parse_pages(spec: str, page_count: int) -> list[int]:24 pages: set[int] = set()25 for part in spec.split(","):26 part = part.strip()27 if not part:28 continue29 if "-" in part:30 start_s, _, end_s = part.partition("-")31 start = int(start_s) if start_s else 132 end = int(end_s) if end_s else page_count33 pages.update(range(start, end + 1))34 else:35 pages.add(int(part))36 bad = [p for p in pages if not 1 <= p <= page_count]37 if bad:38 raise ValueError(f"pages out of range 1-{page_count}: {sorted(bad)}")39 return sorted(pages)40 41 42def build_overlay(args, page_width: float, page_height: float) -> bytes:43 from reportlab.lib.colors import HexColor44 from reportlab.pdfgen import canvas45 buf = io.BytesIO()46 c = canvas.Canvas(buf, pagesize=(page_width, page_height))47 c.saveState()48 try:49 c.setFillAlpha(float(args.opacity))50 c.setStrokeAlpha(float(args.opacity))51 except Exception:52 pass # very old reportlab: no alpha support53 c.translate(float(args.x), float(args.y))54 if args.rotation:55 c.rotate(float(args.rotation))56 if args.text:57 c.setFont(args.font, float(args.font_size))58 c.setFillColor(HexColor(args.color))59 c.drawString(0, 0, args.text)60 else:61 kwargs = {}62 if args.width:63 kwargs["width"] = float(args.width)64 if args.height:65 kwargs["height"] = float(args.height)66 if "width" in kwargs and "height" not in kwargs:67 from PIL import Image as PILImage68 with PILImage.open(args.image) as im:69 kwargs["height"] = kwargs["width"] * im.height / im.width70 c.drawImage(args.image, 0, 0, mask="auto", **kwargs)71 c.restoreState()72 c.save()73 return buf.getvalue()74 75 76def main() -> int:77 for stream in (sys.stdout, sys.stderr):78 try:79 stream.reconfigure(encoding="utf-8")80 except Exception:81 pass82 parser = argparse.ArgumentParser(description="Stamp text or an image onto PDF pages.")83 parser.add_argument("pdf", help="Input PDF path")84 parser.add_argument("-o", "--output", required=True, help="Output PDF path")85 what = parser.add_mutually_exclusive_group(required=True)86 what.add_argument("--text", help="Text to stamp")87 what.add_argument("--image", help="Image file to stamp (PNG/JPEG)")88 parser.add_argument("--x", type=float, required=True, help="X in points (origin bottom-left)")89 parser.add_argument("--y", type=float, required=True, help="Y in points")90 parser.add_argument("--pages", default="1-", help="1-based ranges, e.g. '1-3,5' (default: all)")91 parser.add_argument("--font", default="Helvetica", help="Font name (default Helvetica)")92 parser.add_argument("--font-size", type=float, default=24, help="Font size in points")93 parser.add_argument("--color", default="#000000", help="Text color as #RRGGBB")94 parser.add_argument("--rotation", type=float, default=0, help="Degrees counterclockwise")95 parser.add_argument("--opacity", type=float, default=1.0, help="0.0-1.0 (default 1.0)")96 parser.add_argument("--width", type=float, help="Image width in points")97 parser.add_argument("--height", type=float, help="Image height in points")98 parser.add_argument("--under", action="store_true",99 help="Place the stamp under existing content instead of over it")100 parser.add_argument("--password", help="Password if the input is encrypted")101 args = parser.parse_args()102 103 try:104 from pypdf import PdfReader, PdfWriter105 except ImportError:106 print("Missing dependency: install with 'python3 -m pip install pypdf reportlab'",107 file=sys.stderr)108 return 2109 110 reader = PdfReader(args.pdf)111 if reader.is_encrypted:112 if args.password is None or not reader.decrypt(args.password):113 print("Error: input is encrypted; pass --password", file=sys.stderr)114 return 3115 try:116 pages = set(parse_pages(args.pages, len(reader.pages)))117 except ValueError as exc:118 print(f"Error: {exc}", file=sys.stderr)119 return 4120 121 writer = PdfWriter()122 overlay_cache: dict[tuple[float, float], object] = {}123 for idx, page in enumerate(reader.pages, start=1):124 if idx in pages:125 size = (float(page.mediabox.width), float(page.mediabox.height))126 if size not in overlay_cache:127 overlay_pdf = PdfReader(io.BytesIO(build_overlay(args, *size)))128 overlay_cache[size] = overlay_pdf.pages[0]129 stamp = overlay_cache[size]130 if args.under:131 page.merge_page(stamp, over=False)132 else:133 page.merge_page(stamp)134 writer.add_page(page)135 with open(args.output, "wb") as fh:136 writer.write(fh)137 print(json.dumps({"output": args.output, "stamped_pages": sorted(pages),138 "kind": "text" if args.text else "image"}, ensure_ascii=False))139 return 0140 141 142if __name__ == "__main__":143 sys.exit(main())144