Skip to content

Form: AcroForm interactive fields and flattening

The Form module creates interactive forms for Portable Document Format (PDF) documents. It builds text, checkbox, radio, choice (list/combo), button, and signature fields, organizes them into a parent/child hierarchy, writes them as PDF objects with appearance streams, and can flatten the form into static page content.

Terminal window
composer require nextpdf/core:^3

An interactive PDF form is the AcroForm: a document-level field hierarchy whose terminal fields are paired with widget annotations on pages. ISO 32000-2 §12.7 defines that form, its field dictionaries, the AcroForm root, and the field hierarchy. This module encodes those structures for the engine.

FormFieldManager is the primary surface. It exposes textField(), checkBox(), radioButton(), button(), and signatureField() builders, tracks fields, and serializes them with writeFields(). It emits one Form XObject appearance stream for each widget (/Subtype /Form, /BBox), as §8.10 requires. FormField is the field value object. FormFieldDictionaryBuilder converts a field to its PDF dictionary and applies type-specific entries and flag bits for text, checkbox, choice, and button fields. The manager and FormField are @since 1.0.0. The dictionary builder is @since 1.1.0.

FormFieldHierarchy models the parent/child field tree with addChild(), getRootFieldNames(), getChildren(), getParent(), and a walkDepthFirst() visitor. A fully-qualified field name is the dotted path through this hierarchy, matching the nested-field model in §12.7.

FormFlattener renders the form into static page content. flatten() returns a FlattenResult, so you can freeze a filled form for archival. FieldMDP and FieldMdpAction model the field-lock transform. A signed document can lock a named set of fields against further modification. FieldMdpAction enumerates the lock scope, and requiresFieldList() indicates when an explicit field list is mandatory. FieldMDP is @since 2.0.0.

ClassKey membersRole
FormFieldManagertextField(), checkBox(), radioButton(), button(), signatureField(), addChildField(), getHierarchy(), writeFields()Field builder + serializer (@since 1.0.0)
FormFieldfield value objectOne AcroForm field (@since 1.0.0)
FormFieldDictionaryBuilderbuildFieldDictionary(), applyTextFieldOptions(), applyCheckBoxOptions(), applyChoiceFieldOptions(), applyButtonOptions()Field-to-dictionary builder (@since 1.1.0)
FormFieldHierarchyaddChild(), getRootFieldNames(), getChildren(), getParent(), walkDepthFirst()Parent/child field tree (@since 2.0.0)
FormFlattenerflatten(array $fields, array $pages): FlattenResultFlattens the form to static content (@since 1.0.0)
FieldMDPtoTransformParams()Field-lock (FieldMDP) transform (@since 2.0.0)
FieldMdpAction (enum)requiresFieldList()Field-lock scope (@since 2.0.0)

Run composer docs:generate-api-php -- --module=Form to generate the full PHPDoc table.

Source: examples/30-form-fields.php.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Form\FormFieldManager;
$form = new FormFieldManager();
$form->textField(name: 'applicant_name', x: 50, y: 700, w: 200, h: 18);
$form->checkBox(name: 'agree_terms', x: 50, y: 660, size: 12);
$form->radioButton(name: 'plan', x: 50, y: 620, size: 12 /* + option set */);
// The Writer invokes $form->writeFields(...) during document serialization.

Build a signed-document field set, then lock the signed fields with FieldMDP.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Form\FieldMDP;
use NextPDF\Form\FieldMdpAction;
use NextPDF\Form\FormFieldManager;
$form = new FormFieldManager();
$form->textField(name: 'contract_value', x: 50, y: 700, w: 160, h: 18);
$form->signatureField(name: 'approver_sig', x: 50, y: 600, w: 200, h: 60);
// Lock only the named fields after signing.
$lock = new FieldMDP(
action: FieldMdpAction::Include,
fields: ['contract_value'],
);
$transformParams = $lock->toTransformParams();
// $transformParams is attached to the signature's transform method by the signing layer.
  • A field name is a fully-qualified dotted path through FormFieldHierarchy. In a viewer, two terminal fields with the same fully-qualified name are the same logical field. You are responsible for uniqueness.
  • FieldMdpAction::requiresFieldList() tells you when a field list is mandatory. An Include/Exclude action without the required list creates a malformed lock; check the flag.
  • FormFlattener::flatten() deliberately removes interactivity. The result is static content. Keep the interactive source if you need it later.
  • Appearance streams are emitted per widget. A field with no appearance can render inconsistently across viewers; let the manager generate the appearance instead of omitting it.
  • signatureField() creates only the field placeholder. The Security/signing layer produces the actual signature; this module does not.

Field creation and serialization are O(n) in the field count, plus one Form XObject appearance stream per widget. Flattening cost scales with rendered content, not with the field count. The default reference workload stays within the 1500 ms wall / 64 MB peak budget. The reproducibility profile is structural: object numbers and trailer /ID vary between runs. Two documents with the same form are structurally equal but not byte-identical.

Form field values are user input. The module escapes string values for PDF serialization with PdfStringEscaper, so a field value cannot break out of its PDF string and inject structure. When a form is encrypted, the document’s AES-256 encryption covers field content. FieldMDP is a security control: it locks named fields against post-signature modification. A field-lock that does not match the signed field set undermines that control; set the lock scope deliberately. Treat any value extracted from a filled form as untrusted input. See the engine security model in /modules/core/security/.

The form structures this module emits follow the interactive-form model in ISO 32000-2 §12.7: the AcroForm root, field dictionaries, and the document field hierarchy. Per-widget appearances are emitted as Form XObjects per §8.10 and documented inline in src/Form/. tests/Unit/Form/ exercises these implementation facts. They are not a statement of end-to-end PDF 2.0 conformance. Full-document conformance is validated by the oracle and golden suites in /modules/core/conformance/.