--- name: pdf-widget-layer-census description: "Census a real-world \"flat\" PDF before assuming it needs field injection: Quartz/Preview exports often already carry an AcroForm widget layer under underscore rules; also audits pdf-lib flatten residue." --- # Real-PDF census (widget-first, then judge) A PDF that *looks* flat (`pdftotext` shows `____` fill rules) may already contain a full AcroForm widget layer — macOS Preview/Quartz exports write widgets with transparent rects under drawn underscores. Injecting fields into such a file duplicates/shadows widgets. Always census before touching the pipeline. ## Steps 1. **Surface facts** (host): ```bash pdfinfo file.pdf # "Form: AcroForm" hints at a widget layer pdftotext -layout file.pdf - # label lines + underscore rules = intended fields ``` 2. **Widget census** — authoritative check. With the franksign sidecar image, the image ENTRYPOINT *is* `inject_fields.py`; override it: ```bash podman run --rm --entrypoint python -v ~/Downloads:/in:z \ localhost/franksign-pdf-field-sidecar:local -c " import pymupdf d = pymupdf.open('/in/file.pdf') for p in d.pages(): for w in p.widgets(): print(w.field_name, w.field_type_string, [round(v) for v in w.rect]) print('widgets:', sum(1 for p in d.pages() for _ in p.widgets()), 'annots:', sum(1 for p in d.pages() for _ in p.annots())) " ``` 3. **Fill test**: if widgets exist, `fillTemplate()` (pdf-lib) consumes the file AS-IS — spec.json + sidecar are unnecessary for it. Flatten-verify below. ## Flatten audit gotchas - `pdfinfo` prints `Form: AcroForm` even on correctly flattened pdf-lib output — pdf-lib `flatten()` removes every *widget/annotation* but leaves the field *dictionary*. Truth = MuPDF widget enumeration (0), not the pdfinfo line. - `describeForm(bytes, specJson)` in `packages/pdf` parses the SPEC, not the PDF, for the field list — pass a spec or skip it; calling it spec-less throws `JSON Parse error`. ## Decision rule - widgets > 0 → the widget layer IS the spec (names/types/rects, flip y to bottom-left origin); NEVER run `inject_fields.py` over it. - widgets = 0 → genuine flat form: sidecar injection with a spec (position detection = issue #14 territory: underscore-run rects, label-derived names). - Missing signature widget is normal ("please sign" is prose); franksign decision 19: the typed full name is the signature. ## Privacy Real documents are personal: scratch copies in /tmp, placeholder fill values, destroy scratch (`rm /tmp/*` is fine OUTSIDE the repo; repo deletions still go to `.AGENTTRASH/`). Never commit the document, a spike script touching it, or its extracted text into the vault.