Forms & Fields
A form is a named container that lives on the Document, and fields are added to a Page. Each
field names the form it belongs to, and when the document compiles, every field that names a
registered form is written as a widget annotation and its object number is indexed into that form.
HTML to PDF builds these same objects from <form> markup, so everything on this page
describes what that conversion produces as well.
Disclaimer
Form support varies widely by reader — Acrobat implements the full field and flag set, while browser viewers, OS previewers and mobile apps each implement a subset. Pop PDF writes a field's structure and leaves the drawing of it to the reader, so test any form in the readers your audience uses.
Creating a Form#
Document\Form takes one argument, the name, and addForm() puts it on the document:
use Pop\Pdf\Document;
use Pop\Pdf\Document\Form;
use Pop\Pdf\Document\Page;
$form = new Form('signup');
$document = new Document(new Page(Page::LETTER));
$document->addForm($form);
Forms are keyed on the document by name:
getForm('signup')hands back the instance you added;getForm()returnsnullfor a name never registeredgetForms()returns the whole map, keyed by namehasForms()reports whether the document carries any- a document with at least one form compiles a catalog carrying an
/AcroFormreference; one that never seesaddForm()compiles<</Pages 2 0 R/Type/Catalog>>and writes no form dictionary
getFieldIndices() and getNumberOfFields() read the indices the compiler wrote. Both are empty
until the document is compiled, because a field's object number is not known until the objects around
it have been laid out:
use Pop\Pdf\Document;
use Pop\Pdf\Document\Form;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Field\Text;
use Pop\Pdf\Pdf;
$form = new Form('signup');
$form->getNumberOfFields(); // 0
$first = new Text('a');
$first->setWidth(200);
$first->setHeight(24);
$second = new Text('b');
$second->setWidth(200);
$second->setHeight(24);
$page = new Page(Page::LETTER);
$page->addField($first, 'signup', x: 72, y: 700);
$page->addField($second, 'signup', x: 72, y: 650);
$document = new Document($page);
$document->addForm($form);
Pdf::writeToFile($document, filename: __DIR__ . '/signup.pdf');
$form->getNumberOfFields(); // 2
$form->getFieldIndices(); // [5, 6]
Those two integers are PDF object numbers, and they are the numbers the compiled catalog references.
A document may hold as many named forms as needed; their indices combine into the one form dictionary
the catalog points at. A contact form and a billing form with one field each produce
<</Fields[5 0 R 6 0 R]>>, with getFieldIndices() returning [5] on the first and [6] on the
second — the reader sees one flat set.
One form also gathers fields from as many pages as the document has, so a questionnaire running over several sheets is still one form:
use Pop\Pdf\Document;
use Pop\Pdf\Document\Form;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Field\Text;
use Pop\Pdf\Pdf;
$name = new Text('full_name');
$name->setWidth(240);
$name->setHeight(24);
$comments = new Text('comments');
$comments->setWidth(240);
$comments->setHeight(72);
$comments->setMultiline();
$one = new Page(Page::LETTER);
$one->addField($name, 'signup', x: 72, y: 680);
$two = new Page(Page::LETTER);
$two->addField($comments, 'signup', x: 72, y: 680);
$form = new Form('signup');
$document = new Document();
$document->addPage($one);
$document->addPage($two);
$document->addForm($form);
Pdf::writeToFile($document, filename: __DIR__ . '/signup.pdf');
$form->getFieldIndices(); // [5, 7]
Compiling that:
- each page carries its own widget in its own
/Annotsarray —/Annots[5 0 R]on the first,/Annots[7 0 R]on the second — each widget's/Ppointing back at its page - the catalog collects both into
<</Fields[5 0 R 7 0 R]>> - the indices aren't consecutive, because the second page object was written between the two widgets
Adding a Field to a Page#
addField($field, $form, $x, $y) takes the field, the name of the form it belongs to, and the
coordinates of the widget rectangle's bottom left corner. Both coordinates default to 0, and the
method returns the page, so fields chain:
use Pop\Pdf\Document;
use Pop\Pdf\Document\Form;
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Field\Button;
use Pop\Pdf\Document\Page\Field\Choice;
use Pop\Pdf\Document\Page\Field\Text;
use Pop\Pdf\Pdf;
$name = new Text('full_name', font: Font::HELVETICA, size: 12);
$name->setWidth(240);
$name->setHeight(24);
$notes = new Text('notes', font: Font::HELVETICA, size: 12);
$notes->setWidth(240);
$notes->setHeight(72);
$notes->setMultiline();
$country = new Choice('country', font: Font::HELVETICA, size: 12);
$country->setWidth(240);
$country->setHeight(24);
$country->addOption('Canada')->addOption('Mexico')->addOption('United States');
$country->setCombo();
$agree = new Button('agree', font: Font::HELVETICA, size: 12);
$agree->setWidth(16);
$agree->setHeight(16);
$agree->addOption('Yes');
$page = new Page(Page::LETTER);
$page->addField($name, 'signup', x: 72, y: 680)
->addField($notes, 'signup', x: 72, y: 560)
->addField($country, 'signup', x: 72, y: 500)
->addField($agree, 'signup', x: 72, y: 460);
$form = new Form('signup');
$document = new Document($page);
$document->addFont(Font::HELVETICA);
$document->addForm($form);
Pdf::writeToFile($document, filename: __DIR__ . '/signup.pdf');
$form->getNumberOfFields(); // 4
That writes a 2,222-byte single-page letter document at PDF version 1.7, whose catalog carries
<</Fields[6 0 R 7 0 R 8 0 R 11 0 R]>> and whose page carries the matching
/Annots[6 0 R 7 0 R 8 0 R 11 0 R]. The four widgets compile to:
| Object | /FT |
/T |
/Rect |
/Ff |
/Opt |
|---|---|---|---|---|---|
6 |
/Tx |
full_name |
[72 680 312 704] |
— | — |
7 |
/Tx |
notes |
[72 560 312 632] |
4096 |
— |
8 |
/Ch |
country |
[72 500 312 524] |
131072 |
[ (Canada) (Mexico) (United States) ] |
11 |
/Btn |
agree |
[72 460 88 476] |
— | [ (Yes) ] |
Objects 9 and 10 are the checkbox's own on and off appearance streams, written just before the
widget that references them — which is why the checkbox lands on 11 rather than following 8.
The second argument must match a registered form name exactly. A field naming a form that was never
registered is dropped silently at compile time — no widget object, no /Annots entry — and a form
that is registered but never named by a field compiles with an empty <</Fields[]>>.
Written out in full, the first field is:
/Type /Annot
/Subtype /Widget
/FT /Tx
/Rect [72 680 312 704]
/P 5 0 R
/DA(/MF1 12 Tf 0 g)
/T(full_name)/TU(full_name)/TM(full_name)
That shape holds for every field:
- the
/Rectruns from theaddField()coordinates to those coordinates plus the field's width and height — the 240 by 24 field at 72, 680 covers x 72–312, y 680–704; the 16 by 16 checkbox at 72, 460 covers[72 460 88 476] - a field with no
setWidth()/setHeight()call compiles with a degenerate/Rect [72 680 72 680]— four corners on one point, no error - the name reaches the dictionary three times: the partial name
/T, the tooltip/TUand the mapping name/TM /Ppoints back at the page object the widget sits on
Options Every Field Has#
Every field type extends Field\AbstractField and takes the same constructor —
__construct($name, $font = null, $size = 12) — and the same setters. All of them return the field,
so a whole configuration is one chain:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
use Pop\Color\Color\Rgb;
$field = new Text('full_name', font: Font::HELVETICA, size: 14);
$field->setWidth(240)
->setHeight(24)
->setValue('Alex Doe')
->setDefaultValue('Alex Doe')
->setFontColor(new Rgb(0, 51, 102))
->setRequired();
setValue() and setDefaultValue() write /V and /DV into the dictionary:
- on a text field, hand each one the plain string —
Field\Textquotes and escapes it for you, so the chain above emits/V (Alex Doe)and/DV (Alex Doe) - on a choice field, hand it the option's export value the same way —
setValue('Canada')emits/V (Canada) - on a checkbox or radio,
setValue()carries the export name for the box's checked state rather than the state itself; see Checked State below getValue()andgetDefaultValue()read them back, and both start outnull
setFont() names a font, setSize() sets the point size and setFontColor() takes any Pop\Color
implementation. The three travel together into the field's default appearance string, /DA, and that
string is written once the named font is registered on the document with addFont():
use Pop\Pdf\Document;
use Pop\Pdf\Document\Form;
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Field\Text;
use Pop\Pdf\Pdf;
use Pop\Color\Color\Rgb;
$field = new Text('full_name', font: Font::HELVETICA);
$field->setWidth(240);
$field->setHeight(24);
$field->setSize(30);
$field->setFontColor(new Rgb(255, 0, 0));
$page = new Page(Page::LETTER);
$page->addField($field, 'signup', x: 72, y: 680);
$document = new Document($page);
$document->addFont(Font::HELVETICA);
$document->addForm(new Form('signup'));
Pdf::writeToFile($document, filename: __DIR__ . '/signup.pdf');
That field compiles with /DA(/MF1 30 Tf 1 0 0 rg) — the document's reference for Helvetica, the
size, and the color, each color space rendering in its own operator:
- in the same field,
new Color\Cmyk(100, 0, 0, 0)renders1 0 0 0 kandnew Color\Grayscale(50)renders0.5 g - registering a second font shifts the reference — a field naming Helvetica on a document that
registered Arial first compiles with
/MF2 - every font a field names must be registered — compiling a field whose font is absent raises
Pop\Pdf\Build\Exceptionnaming it - a field with no
setFont()call compiles with no/DAentry at all, taking the reader's own default appearance - the default color, where a font is set but a color is not, is
0 g, black
getFont(), getSize(), getFontColor(), getWidth() and getHeight() read the current values.
Size starts at 12; font, color, width and height start at null.
Three flags are shared by every field type, and each writes one bit into /Ff:
| Setter | /Ff value |
Flag bit |
|---|---|---|
setReadOnly() |
1 |
1 |
setRequired() |
2 |
2 |
setNoExport() |
4 |
3 |
Flags accumulate into a single integer, so setReadOnly()->setRequired()->setNoExport() emits
/Ff 7. Calling one of them more than once sets the bit once — three setMultiline() calls emit
/Ff 4096, the same as one. A field with no flag setter called on it emits no /Ff entry at all.
Borders and Backgrounds#
Every field type takes a border and a background of its own, so the box a reader sees comes from the field rather than from shapes drawn beside it:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
$field = new Text('full_name', font: Font::HELVETICA, size: 12);
$field->setWidth(240)
->setHeight(24)
->setBorderWidth(1)
->setBorderColor([153, 153, 153])
->setBackgroundColor([255, 255, 255]);
That compiles to /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> and /BS << /W 1 >> — the appearance
characteristics a reader draws the box from, and the border style that gives it a width:
- both color setters take an
[r, g, b]array, 0–255 per channel, and render as PDF's 0–1 range - border width is the gate:
0, the default, writes neither/BCnor/BS, so a field given onlysetBackgroundColor()compiles with/MK << /BG [0.933 0.933 0.933] >>and nothing else getBorderWidth()starts at0, andgetBorderColor()andgetBackgroundColor()start atnull- a checkbox or radio draws its border and background into its own appearance streams, so the box looks the same whether it is currently checked or clear
Text Fields#
Field\Text compiles to /FT /Tx. Give one a width and a height and call nothing else, and it
compiles with a /Rect and no /Ff at all — the plain single-line entry box. Each setter below turns
that box into a variant by adding one flag bit, and each returns the field.
Multiline#
setMultiline() emits /Ff 4096, flag bit 13, which is the bit a reader checks to wrap typing onto
a second line instead of scrolling it sideways. Pair it with a height that has room for the lines:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
$notes = new Text('notes', font: Font::HELVETICA, size: 12);
$notes->setWidth(240);
$notes->setHeight(72);
$notes->setMultiline();
isMultiline() reports whether the bit is set, and returns false on a field straight from the
constructor.
Password#
setPassword() emits /Ff 8192, flag bit 14, the bit that has the reader echo each typed character
as a bullet:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
$secret = new Text('passphrase', font: Font::HELVETICA, size: 12);
$secret->setWidth(240);
$secret->setHeight(24);
$secret->setPassword();
isPassword() reads the bit back.
Comb Fields#
setComb() emits /Ff 16777216, flag bit 25 — the bit that lays the field out as evenly spaced cells,
one character per cell, which is the shape a serial number or a postal code wants:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
$zip = new Text('postal_code', font: Font::HELVETICA, size: 12);
$zip->setWidth(120);
$zip->setHeight(24);
$zip->setComb();
File Select#
setFileSelect() emits /Ff 1048576, flag bit 21, which marks the field's contents as the path of a
file to submit rather than text to submit literally:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
$upload = new Text('attachment', font: Font::HELVETICA, size: 12);
$upload->setWidth(240);
$upload->setHeight(24);
$upload->setFileSelect();
Rich Text#
setRichText() emits /Ff 33554432, flag bit 26, which lets the field carry styled text instead of
one uniform run:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
$bio = new Text('biography', font: Font::HELVETICA, size: 12);
$bio->setWidth(240);
$bio->setHeight(96);
$bio->setMultiline();
$bio->setRichText();
Flags combine freely, and that pair emits /Ff 33558528.
Spell Check and Scrolling#
setDoNotSpellCheck() emits /Ff 4194304, flag bit 23, and setDoNotScroll() emits /Ff 8388608,
flag bit 24. The first has the reader accept the entry as typed rather than underlining it, which
suits a part number or a reference code; the second caps entry at what the rectangle can display:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Text;
$sku = new Text('sku', font: Font::HELVETICA, size: 12);
$sku->setWidth(160);
$sku->setHeight(24);
$sku->setDoNotSpellCheck();
$sku->setDoNotScroll();
$sku->setComb();
The three together emit /Ff 29360128.
Choice Fields#
Field\Choice compiles to /FT /Ch and covers both dropdowns and list boxes. addOption() appends
to the field's option list, which compiles to /Opt:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Choice;
$country = new Choice('country', font: Font::HELVETICA, size: 12);
$country->setWidth(240);
$country->setHeight(24);
$country->addOption('Canada');
$country->addOption('Mexico');
$country->addOption('United States');
That code:
- emits
/Opt [ (Canada) (Mexico) (United States) ], in the order the options were added hasOptions()reports whether any have been added, and returnsfalseon a new field- a
Field\Choicewith a width and a height and nothing else compiles to/FT /Chwith a/Rect, no/Optand no/Ff— add options to give the field its list
Give addOption() a second argument to separate what a reader submits from what it shows:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Choice;
$plan = new Choice('plan', font: Font::HELVETICA, size: 12);
$plan->setWidth(240);
$plan->setHeight(24);
$plan->addOption('mo', 'Monthly');
$plan->addOption('yr', 'Annual');
$plan->setValue('yr');
That emits /Opt [ [(mo) (Monthly)] [(yr) (Annual)] ] — each pair the export value followed by its
display label — and /V (yr), preselecting Annual. An option added with one argument, or with a label
matching its value, compiles as the plain string form above.
Dropdowns#
setCombo() emits /Ff 131072, flag bit 18, which turns the list into a dropdown that shows one row
until it is opened. setEdit() emits /Ff 262144, flag bit 19, which lets the reader type a value
the list does not offer:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Choice;
$country = new Choice('country', font: Font::HELVETICA, size: 12);
$country->setWidth(240);
$country->setHeight(24);
$country->addOption('Canada')->addOption('Mexico')->addOption('United States');
$country->setCombo();
$country->setEdit();
The pair emits /Ff 393216. isCombo() reads bit 18 back.
List Boxes#
Leave setCombo() off and the field is a list box showing as many rows as its height allows.
setMultiSelect() emits /Ff 2097152, flag bit 22, which lets more than one row be chosen at once,
and setSort() emits /Ff 524288, flag bit 20, which has the reader present the options in
alphabetical order:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Choice;
$topics = new Choice('topics', font: Font::HELVETICA, size: 12);
$topics->setWidth(240);
$topics->setHeight(96);
$topics->addOption('Releases')->addOption('Events')->addOption('Announcements');
$topics->setMultiSelect();
$topics->setSort();
/Opt keeps the insertion order you gave it and the sort flag is what asks the reader to reorder the
display, so addOption('Zebra')->addOption('Aardvark')->setSort() compiles to
/Opt [ (Zebra) (Aardvark) ] with /Ff 524288. isMultiSelect() reads bit 22 back.
Commit on Change#
setCommitOnSelChange() emits /Ff 67108864, flag bit 27, which commits the new selection the moment
it is picked rather than when focus moves off the field. setDoNotSpellCheck() is available here too
and emits the same /Ff 4194304 it does on a text field:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Choice;
$plan = new Choice('plan', font: Font::HELVETICA, size: 12);
$plan->setWidth(240);
$plan->setHeight(96);
$plan->addOption('Monthly')->addOption('Annual');
$plan->setMultiSelect();
$plan->setCommitOnSelChange();
Those two emit /Ff 69206016.
Buttons#
Field\Button compiles to /FT /Btn and covers checkboxes, radio groups and push buttons:
addOption($option, $xOffset = 0, $yOffset = 0)takes the option label and two offsets that default to0- the label is what reaches
/Opt - the two offsets are recorded on the option only — neither reaches the compiled document nor comes back from a getter
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Button;
$agree = new Button('agree', font: Font::HELVETICA, size: 12);
$agree->setWidth(16);
$agree->setHeight(16);
$agree->addOption('Yes');
With no flag setter called, that is a checkbox: /FT /Btn, a 16 by 16 rectangle and /Opt [ (Yes) ],
with no /Ff. It compiles clear — /V /Off and /AS /Off — with an /AP pair holding a /Yes state
ready for the reader. hasOptions() reports whether any options have been added.
Checked State#
setValue() names the state a reader reports for the widget when it is checked — its export value —
and setChecked() says whether it starts out checked. The two are independent, and each is read back
by its own getter:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Button;
$agree = new Button('agree', font: Font::HELVETICA, size: 12);
$agree->setWidth(16);
$agree->setHeight(16);
$agree->setValue('Yes'); // the export value, reported once the box is checked
$agree->setChecked(); // and check it
$agree->isChecked(); // true
$agree->getValue(); // 'Yes'
That compiles to /V /Yes and /AS /Yes alongside /AP << /N << /Yes 6 0 R /Off 7 0 R >> >>, the
pair of appearance streams the widget draws itself from — a filled square for a checkbox, a filled
circle for a radio. Leave setChecked() off and the same field compiles with /V /Off and /AS /Off,
its /Yes on-state ready for the reader to switch to.
Give the export value as a plain word such as Yes or email: it is written as a PDF name, and
anything outside letters, digits and _ becomes _.
A Field\Choice takes setValue() too, and writes it as a text string rather than a name —
setValue('Canada') emits /V (Canada), preselecting that row of the list.
Radio Groups#
Three flags apply to a Field\Button used as a radio option:
setRadio()emits/Ff 32768, flag bit 16 — treats the field as one choice among severalsetNoToggleToOff()emits/Ff 16384, flag bit 15 — keeps a chosen option chosen when clicked a second timesetRadiosInUnison()emits/Ff 33554432, flag bit 26 — turns every box sharing an export value on together
Build the group from one Field\Button per option, each marked setRadio(), all sharing a field
name and a form, each with its own export value and its own coordinates:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Button;
$email = new Button('contact_method', font: Font::HELVETICA, size: 12);
$email->setWidth(16);
$email->setHeight(16);
$email->setRadio();
$email->setValue('email');
$email->setChecked();
$phone = new Button('contact_method', font: Font::HELVETICA, size: 12);
$phone->setWidth(16);
$phone->setHeight(16);
$phone->setRadio();
$phone->setValue('phone');
Two or more such widgets compile to one grouped field — a shared, non-visual parent plus one child widget each — so choosing one option clears the rest:
- the parent carries the group's identity:
/FT /Btn /T(contact_method) /Ff 32768, plus/V /emailnaming the option that starts out chosen - each child carries
/Parent, its own/Rectand its own/APon/off pair, and no/Tor/Ffof its own — added at 72, 600 and 72, 570, the two above compile to/Rect [72 600 88 616]and/Rect [72 570 88 586] - the form counts the group as one field:
getNumberOfFields()reports the parent, andgetFieldIndices()carries the parent's object number, not the children's - give every option its own
setValue(), including the ones that start out clear, so each has a distinct export name; callsetChecked()on the single option that starts out chosen - a lone
setRadio()field with no same-named sibling stays a top-level field of its own,/Tand/Ff 32768included isRadio()reads bit 16 back
Push Buttons#
setPushButton() emits /Ff 65536, flag bit 17, which makes the field an action button that holds no
value of its own:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Button;
$submit = new Button('submit', font: Font::HELVETICA, size: 12);
$submit->setWidth(80);
$submit->setHeight(24);
$submit->addOption('Submit');
$submit->setPushButton();
isPushButton() reads bit 17 back. setCaption() gives the button its label, drawn into an
appearance stream of its own using the field's registered font:
use Pop\Pdf\Document\Font;
use Pop\Pdf\Document\Page\Field\Button;
$submit = new Button('submit', font: Font::HELVETICA, size: 12);
$submit->setWidth(80);
$submit->setHeight(24);
$submit->setPushButton();
$submit->setCaption('Send');
That compiles to /MK << /CA (Send) >> and /AP << /N 6 0 R >>, the stream that centers the caption
in the widget using the font's own metrics. getCaption() reads it back, and starts out null. Give
the button a setFont() and register that font on the document, so the caption has a resource to draw
with; the button holds no action of its own.
See Also#
- Adding Text — drawing the labels that sit beside each field
- Fonts — registering the fonts a field's
/DAstring references - HTML to PDF — building the same fields from
<form>markup instead of by hand - Modifying Imported Pages — adding fields to a page that came from an existing PDF