pdf

PDF files: create, read, merge, fill, OCR, edit text.

  • pdf
  • documents
  • forms
  • ocr
  • text-extraction
  • reportlab
  • pypdf
  • pdfplumber
  • pymupdf
  • marker

Declared platforms: linux · macos · windows

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/skills/productivity/pdf'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md
#!/usr/bin/env python3"""Stamp text or an image at coordinates onto selected PDF pages. Builds an in-memory single-page overlay with reportlab, then merges itonto each selected page with pypdf. Coordinates are PDF points, originbottom-left. Covers 'sign here' arrows, diagonal DRAFT banners, andpage-corner labels. Examples:  pdf_stamp.py in.pdf -o out.pdf --text "DRAFT" --x 200 --y 400 \      --font-size 60 --rotation 45 --opacity 0.3 --color "#cc0000"  pdf_stamp.py in.pdf -o out.pdf --image sig.png --x 400 --y 60 \      --width 120 --pages 3"""from __future__ import annotations import argparseimport ioimport jsonimport sys  def parse_pages(spec: str, page_count: int) -> list[int]:    pages: set[int] = set()    for part in spec.split(","):        part = part.strip()        if not part:            continue        if "-" in part:            start_s, _, end_s = part.partition("-")            start = int(start_s) if start_s else 1            end = int(end_s) if end_s else page_count            pages.update(range(start, end + 1))        else:            pages.add(int(part))    bad = [p for p in pages if not 1 <= p <= page_count]    if bad:        raise ValueError(f"pages out of range 1-{page_count}: {sorted(bad)}")    return sorted(pages)  def build_overlay(args, page_width: float, page_height: float) -> bytes:    from reportlab.lib.colors import HexColor    from reportlab.pdfgen import canvas    buf = io.BytesIO()    c = canvas.Canvas(buf, pagesize=(page_width, page_height))    c.saveState()    try:        c.setFillAlpha(float(args.opacity))        c.setStrokeAlpha(float(args.opacity))    except Exception:        pass  # very old reportlab: no alpha support    c.translate(float(args.x), float(args.y))    if args.rotation:        c.rotate(float(args.rotation))    if args.text:        c.setFont(args.font, float(args.font_size))        c.setFillColor(HexColor(args.color))        c.drawString(0, 0, args.text)    else:        kwargs = {}        if args.width:            kwargs["width"] = float(args.width)        if args.height:            kwargs["height"] = float(args.height)        if "width" in kwargs and "height" not in kwargs:            from PIL import Image as PILImage            with PILImage.open(args.image) as im:                kwargs["height"] = kwargs["width"] * im.height / im.width        c.drawImage(args.image, 0, 0, mask="auto", **kwargs)    c.restoreState()    c.save()    return buf.getvalue()  def main() -> int:    for stream in (sys.stdout, sys.stderr):        try:            stream.reconfigure(encoding="utf-8")        except Exception:            pass    parser = argparse.ArgumentParser(description="Stamp text or an image onto PDF pages.")    parser.add_argument("pdf", help="Input PDF path")    parser.add_argument("-o", "--output", required=True, help="Output PDF path")    what = parser.add_mutually_exclusive_group(required=True)    what.add_argument("--text", help="Text to stamp")    what.add_argument("--image", help="Image file to stamp (PNG/JPEG)")    parser.add_argument("--x", type=float, required=True, help="X in points (origin bottom-left)")    parser.add_argument("--y", type=float, required=True, help="Y in points")    parser.add_argument("--pages", default="1-", help="1-based ranges, e.g. '1-3,5' (default: all)")    parser.add_argument("--font", default="Helvetica", help="Font name (default Helvetica)")    parser.add_argument("--font-size", type=float, default=24, help="Font size in points")    parser.add_argument("--color", default="#000000", help="Text color as #RRGGBB")    parser.add_argument("--rotation", type=float, default=0, help="Degrees counterclockwise")    parser.add_argument("--opacity", type=float, default=1.0, help="0.0-1.0 (default 1.0)")    parser.add_argument("--width", type=float, help="Image width in points")    parser.add_argument("--height", type=float, help="Image height in points")    parser.add_argument("--under", action="store_true",                        help="Place the stamp under existing content instead of over it")    parser.add_argument("--password", help="Password if the input is encrypted")    args = parser.parse_args()     try:        from pypdf import PdfReader, PdfWriter    except ImportError:        print("Missing dependency: install with 'python3 -m pip install pypdf reportlab'",              file=sys.stderr)        return 2     reader = PdfReader(args.pdf)    if reader.is_encrypted:        if args.password is None or not reader.decrypt(args.password):            print("Error: input is encrypted; pass --password", file=sys.stderr)            return 3    try:        pages = set(parse_pages(args.pages, len(reader.pages)))    except ValueError as exc:        print(f"Error: {exc}", file=sys.stderr)        return 4     writer = PdfWriter()    overlay_cache: dict[tuple[float, float], object] = {}    for idx, page in enumerate(reader.pages, start=1):        if idx in pages:            size = (float(page.mediabox.width), float(page.mediabox.height))            if size not in overlay_cache:                overlay_pdf = PdfReader(io.BytesIO(build_overlay(args, *size)))                overlay_cache[size] = overlay_pdf.pages[0]            stamp = overlay_cache[size]            if args.under:                page.merge_page(stamp, over=False)            else:                page.merge_page(stamp)        writer.add_page(page)    with open(args.output, "wb") as fh:        writer.write(fh)    print(json.dumps({"output": args.output, "stamped_pages": sorted(pages),                      "kind": "text" if args.text else "image"}, ensure_ascii=False))    return 0  if __name__ == "__main__":    sys.exit(main()) 
Referenced from SKILL.md