--- name: real-pdf-acroform-census description: "Census a real-world PDF before running it through a flat→AcroForm pipeline: check for hidden widget layers, prove fill/flatten, avoid the pdfinfo ghost." --- Census recipe for a real-world PDF arriving at a flat-form pipeline (e.g. franksign's sidecar that injects AcroForm widgets). Run BEFORE assuming the document is flat. ## Sequence 1. **Metadata**: `pdfinfo file.pdf` — note `Form:` line, producer (`macOS ... Quartz PDFContext` = Preview/Preview-era export), page size. 2. **Text layer**: `pdftotext -layout file.pdf -` — underscore rules (`____`) look "flat". 3. **Widget census (the decisive probe)** — never trust the looks: ```bash podman run --rm --entrypoint python -v ~/Downloads:/in:z :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('annots:', sum(1 for p in d.pages() for _ in p.annots())) ``` Traps: a custom ENTRYPOINT script swallows `python` as argv — override with `--entrypoint python`. Mount a directory, not a single file (MuPDF atomic-save EBUSY). 4. **Fill live-fire**: pass the file through the fill step as-is with placeholder values; verify with `pdftotext -layout` that values land on the rules. 5. **Flatten audit**: re-run the widget census on the output (expect 0) — do NOT read `pdfinfo`. ## Laws learned - macOS Preview/Quartz exports (and most print-to-PDF paths) **already embed AcroForm widgets** under transparent rects over their drawn underscore rules. Field names are human-meaningful (`date_today`, ...). Injecting widgets over them duplicates/shadows fields — the design-time injection stage must be skipped when a widget layer exists (widget-first detection). - pdf-lib `flatten()` removes every **widget** but leaves the AcroForm **field dictionary**: `pdfinfo` keeps printing `Form: AcroForm` on a fully flattened file. Widget enumeration (MuPDF) is the only truth; pdfinfo is the ghost. - Underscore rules in the text layer are drawings, not fields; the text layer alone cannot tell flat from widget-bearing. ## Verdicts - Widgets present → fill as-is; detection spec = widget layer (names, types, rects; flip top-left→bottom-left origin if the pipeline spec uses bottom-left). - Zero widgets → true flat form; needs position detection (underscore runs + nearest preceding label) before injection.