Skip to content

Add ExistingFormFiller: read, fill, flatten and stamp existing PDF forms - #1

Open
christiaan wants to merge 7 commits into
dskripchenko:mainfrom
christiaan:feature/existing-acroform-fill
Open

Add ExistingFormFiller: read, fill, flatten and stamp existing PDF forms#1
christiaan wants to merge 7 commits into
dskripchenko:mainfrom
christiaan:feature/existing-acroform-fill

Conversation

@christiaan

@christiaan christiaan commented Sep 2, 2026

Copy link
Copy Markdown

The library can already author new fillable forms from scratch, but has no
way to open a PDF someone else produced (an uploaded template, a government
form) and read its already-defined fields by name, fill them in, bake the
result into static page content, and stamp an image alongside it.

  • Add ExistingFormFiller: read AcroForm fields from an existing PDF, resolving
    field names through the /Parent chain into their fully qualified dotted form
  • Fill existing text, checkbox and radio fields, including /Parent-inherited
    names, matching every widget's own /AP/N on-state rather than guessing a
    fixed convention
  • Add flatten() to bake current field values into static page content and
    remove the AcroForm entries, for archival/signed copies that shouldn't stay
    editable, correctly handling Resources/Annots/DR stored as indirect objects
    (as Acrobat-produced forms commonly do)
  • Add standalone image stamping and combined fill+flatten+stamp support, for
    placing a signature or photo that doesn't map to a form field
  • Wire a fill/flatten/stamp fixture into the visual regression check
  • Document ExistingFormFiller in USAGE.md (en/de/ru/zh)
  • Expose a field's /TU tooltip on ExistingFormFiller::fields(), decoded as a
    PDF text string (UTF-16BE with BOM, per ISO 32000-1 §7.9.2.2), same as a
    field's /T name segment

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

christiaan and others added 7 commits September 2, 2026 12:13
The library can already author new fillable forms from scratch, but
has no way to open a PDF someone else produced (an uploaded template,
a government form) and read its already-defined fields by name — that
is the entry point for filling in an existing template's values.

Field names are resolved through the /Parent chain into their fully
qualified dotted form, since real-world AcroForms commonly nest
fields (radio groups, grouped sections) rather than keep them flat.
setValue()/setValues() now mutate a copy of the field, never the
source, so the same call site can safely reuse a filler across
multiple documents without any risk of the input template changing
underneath it.

Checkbox and radio correctness needs more than writing /V: each
widget's /AS must match one of the on-state keys actually present in
that widget's own /AP/N sub-dictionary, since authoring tools are free
to name those states however they like — guessing a fixed convention
like "Yes" would silently produce an unrendered field in real viewers.
A field can also have more than one widget annotation (the same
checkbox or radio repeated across pages), so every widget is checked
against the value and given a consistent /AS rather than only the
first one. Radio requires an exact match against a widget's export
name, since the generic yes/true/1 aliases meant for a single on/off
checkbox would otherwise spuriously match whichever radio option
happened to be checked last.

A field reachable only through /Parent chain inheritance (grouped
fields, as most real-world AcroForms use) previously had no way to be
addressed at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Downstream, a filled form sometimes needs to stop being interactive
(archival copies, signed submissions where the recipient's viewer
shouldn't let anyone edit the answers afterward). Interactive fill
alone (/V + /NeedAppearances) isn't enough for that — the field
removal and baked appearance need to happen for real.

Flattening reads each field's *current* value (including one set
earlier in the same call chain, not just the source), draws it with
the field's own /DA font pulled from the AcroForm's /DR so no font
gets redeclared, then removes the widget annotation and the field
entry itself. Supports flattening a named subset so unrelated fields
stay interactive, and drops /AcroForm entirely once nothing is left
in it, including once a grouped parent's last child is flattened.

A page's /Resources and /Annots, and the AcroForm's /DR, are read
through a shared helper that dereferences an indirect object first,
since ISO 32000-1 allows any of these to be stored as their own
indirect object rather than inlined — which Acrobat-produced forms
commonly do — and treating that as absent would otherwise discard the
page's existing fonts or leave a flattened widget's annotation behind.
The baked `Tf` operator is only emitted once a font actually resolves
against /DR, and baked text escapes bytes outside printable ASCII, so
a non-ASCII field value survives as a valid PDF literal string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Templates commonly need a signature or photo placed at a fixed spot
alongside the answered fields, and that placement doesn't always map
to a form field at all — so this is a plain page/x/y/w/h primitive,
not something bolted onto the field API, and it composes with
setValue()/flatten() on the same document.

Reuses the existing pure-PHP PNG/JPEG decoder rather than adding an
Imagick dependency, matching how the rest of the reader/merge stack
already avoids one. Reads a page's existing /Resources through the
same indirect-reference-aware helper flatten() uses, since Acrobat-
produced forms commonly store it as its own indirect object, and
treating that as absent would silently discard the page's existing
fonts and XObjects instead of adding the stamp alongside them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Structural tests already cover field/annotation correctness, but
nothing catches a rendering regression in the new flatten() drawing
path or the image stamp. The existing visual-check.sh harness already
renders reference PDFs and diffs them against committed goldens for
pdfa-1b/pdfx-4 — this adds a form-fill document exercising the whole
fill + flatten + stamp pipeline through the same check.

The golden PNG itself is not committed here: this repo's own harness
warns against generating goldens outside CI, since poppler builds
anti-alias differently across platforms — the first CI run against
this branch needs to establish it via visual-check.sh --update.
Fill-existing-form is a distinct entry point from the from-scratch
FormField authoring already documented, and downstream users need to
find it the same way they'd find the existing Forms section.

Kept in lockstep with the other three translated USAGE.md files, same
as the rest of the guide.
Some AcroForm templates carry a hidden field whose /TU is repurposed
to hold structured metadata (e.g. a JSON placement hint for stamping
an image onto the filled template) rather than a literal tooltip — a
convention older pdftk-based fillers exposed via their own field
dump. ExistingFormFiller had no way to read it back, blocking a
migration off such tooling.

Tooltips are stored as PDF text strings, which non-ASCII values
encode as UTF-16BE with a byte-order mark rather than raw bytes
(ISO 32000-1 §7.9.2.2), so the value is decoded accordingly to stay
readable for anything beyond plain ASCII. A field's /T name segment
is decoded through that same text-string helper, since forms use the
same UTF-16BE convention there too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@christiaan
christiaan force-pushed the feature/existing-acroform-fill branch from 6b08e09 to ede8399 Compare September 2, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant