scripts/pdf_make_form.py
scripts/pdf_make_form.pyBrowse 21 files
1,582 tokens
5,988 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""Create a fillable AcroForm PDF from a JSON spec (reportlab canvas.acroForm).3 4Spec format (UTF-8 JSON; coordinates in PDF points, origin bottom-left):5{6 "title": "Example Intake Form",7 "page_size": "A4", // or "letter" or [width, height]8 "page_count": 1,9 "fields": [10 {"name": "surname", "type": "text", "page": 1,11 "label": "Surname", "label_box": [72, 700, 150, 714],12 "entry_box": [160, 696, 400, 716], "value": "", "tooltip": "Family name"},13 {"name": "agree", "type": "checkbox", "page": 1,14 "label": "I agree", "label_box": [72, 660, 150, 674],15 "entry_box": [160, 658, 176, 674], "checked": false},16 {"name": "color", "type": "radio", "page": 1,17 "label": "Color", "label_box": [72, 620, 150, 634],18 "entry_box": [160, 616, 400, 636], "options": ["red", "blue"],19 "value": "red"},20 {"name": "size", "type": "dropdown", "page": 1,21 "label": "Size", "label_box": [72, 580, 150, 594],22 "entry_box": [160, 576, 300, 596], "options": ["small", "large"],23 "value": "small"}24 ]25}26The same spec (label_box/entry_box/page) is what pdf_form_layout.py validates,27so lint the layout first, then build.28"""29from __future__ import annotations30 31import argparse32import json33import sys34 35 36def _reconfigure_stdio() -> None:37 for stream in (sys.stdout, sys.stderr):38 try:39 stream.reconfigure(encoding="utf-8")40 except Exception:41 pass42 43 44def _page_size(spec: dict):45 from reportlab.lib.pagesizes import A4, letter46 ps = spec.get("page_size", "A4")47 if isinstance(ps, (list, tuple)) and len(ps) == 2:48 return float(ps[0]), float(ps[1])49 return letter if str(ps).lower() == "letter" else A450 51 52def build_form(spec: dict, out_path: str) -> int:53 try:54 from reportlab.lib import colors55 from reportlab.pdfgen import canvas56 except ImportError:57 print("Missing dependency: install with 'python3 -m pip install reportlab'", file=sys.stderr)58 return 259 60 width, height = _page_size(spec)61 page_count = int(spec.get("page_count", 1))62 fields = spec.get("fields", [])63 by_page: dict[int, list[dict]] = {}64 for f in fields:65 by_page.setdefault(int(f.get("page", 1)), []).append(f)66 page_count = max([page_count, *by_page.keys()]) if by_page else page_count67 68 c = canvas.Canvas(out_path, pagesize=(width, height))69 if spec.get("title"):70 c.setTitle(str(spec["title"]))71 if spec.get("author"):72 c.setAuthor(str(spec["author"]))73 form = c.acroForm74 created = []75 76 for pageno in range(1, page_count + 1):77 for f in by_page.get(pageno, []):78 name = f["name"]79 ftype = f.get("type", "text")80 ex0, ey0, ex1, ey1 = (float(v) for v in f["entry_box"])81 ew, eh = ex1 - ex0, ey1 - ey082 if f.get("label"):83 lx, ly = (float(f["label_box"][0]), float(f["label_box"][1])) \84 if f.get("label_box") else (ex0 - 90, ey0 + 4)85 c.setFont("Helvetica", float(f.get("label_size", 10)))86 c.setFillColor(colors.black)87 c.drawString(lx, ly + 2, str(f["label"]))88 tooltip = f.get("tooltip", "")89 if ftype == "text":90 form.textfield(name=name, x=ex0, y=ey0, width=ew, height=eh,91 value=str(f.get("value", "")), tooltip=tooltip,92 borderWidth=0.5, forceBorder=True)93 elif ftype == "checkbox":94 size = min(ew, eh)95 form.checkbox(name=name, x=ex0, y=ey0, size=size,96 checked=bool(f.get("checked", False)),97 buttonStyle="check", tooltip=tooltip,98 borderWidth=0.5, forceBorder=True)99 elif ftype == "radio":100 options = f.get("options", [])101 if not options:102 print(f"Warning: radio {name!r} has no options, skipped", file=sys.stderr)103 continue104 size = min(eh, ew / max(len(options), 1) * 0.5, 16)105 slot = ew / len(options)106 sel = f.get("value")107 c.setFont("Helvetica", 8)108 for i, opt in enumerate(options):109 ox = ex0 + i * slot110 form.radio(name=name, value=str(opt), x=ox, y=ey0, size=size,111 selected=(str(opt) == str(sel)), buttonStyle="circle",112 borderWidth=0.5, forceBorder=True)113 c.drawString(ox + size + 2, ey0 + size / 3, str(opt))114 elif ftype == "dropdown":115 options = [str(o) for o in f.get("options", [])]116 value = str(f.get("value", options[0] if options else ""))117 form.choice(name=name, x=ex0, y=ey0, width=ew, height=eh,118 options=options, value=value, tooltip=tooltip,119 borderWidth=0.5, forceBorder=True)120 else:121 print(f"Warning: unknown field type {ftype!r} for {name!r}, skipped",122 file=sys.stderr)123 continue124 created.append({"name": name, "type": ftype, "page": pageno})125 c.showPage()126 c.save()127 print(json.dumps({"output": out_path, "pages": page_count, "fields": created},128 ensure_ascii=False))129 return 0130 131 132def main() -> int:133 _reconfigure_stdio()134 parser = argparse.ArgumentParser(135 description="Create a fillable AcroForm PDF from a JSON spec (reportlab).")136 parser.add_argument("spec", help="Path to UTF-8 JSON form spec")137 parser.add_argument("-o", "--output", required=True, help="Output PDF path")138 args = parser.parse_args()139 with open(args.spec, encoding="utf-8") as fh:140 spec = json.load(fh)141 return build_form(spec, args.output)142 143 144if __name__ == "__main__":145 sys.exit(main())146