Skip to content

A form can be added to a document - #56

Merged
tannevaled merged 1 commit into
mainfrom
a-form-can-be-added-to-a-document
Oct 22, 2026
Merged

tannevaled merged 1 commit into
mainfrom
a-form-can-be-added-to-a-document

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Doc.AddFields and the addfields verb: the other half of Fill, which can only change a field the document already has.

This is what BentoPDF's form-creator needs. The fleet had every other piece already — forms reads and draws them, ops fills them, render draws widgets — and only the direction that creates one was missing. The field vocabulary comes from forms v0.6.0, so the writer and the reader cannot disagree on what a field is.

$ pdfops addfields -fields fields.json blank.pdf form.pdf
added 3 field(s)
$ pdfops fields form.pdf
applicant  text      ""     required
agree      checkbox  "Yes"  buttons [Yes]
size       combo     ""     row "s" row "l"

Fields are held until the write, because a widget names the page object it sits on and the pages have no object numbers until then. A field showing in one place is one object, which is what nearly every producer writes; a field showing on several pages becomes a parent with one Kid per place.

Two details the format makes easy to get wrong

A checkbox's drawing is not generated by the viewer. NeedAppearances covers text and choice fields, but a button shows whatever its own appearance dictionary holds for the state it is in — so one written without them is a box that never looks ticked however often it is clicked. One is drawn per state with the path operators rather than with a font, so the document needs no ZapfDingbats to show a tick.

A widget is marked Print. Without it the field is on screen and gone from the paper, which is where a filled form is most often looked at.

Joining rather than replacing

A document that already has a form is joined. Replacing it would drop every field the file came with, and the file would still open — the old fields simply gone.

A document assembled from several files can still be given a form: two sources' forms cannot be merged, since their fields may be named the same, but fields the caller asked for come from neither source and so are in no conflict.

The description is a file

JSON, because the thing being described is a layout and a command line is a poor place to put one. The kinds are the names pdfops fields prints, so a description can be written from a listing.

Unknown keys are refused: "mutliline" is a form missing the thing it was written to ask for, and a parser that ignores what it does not know says nothing about it. The list lengths are bounded (10 000 fields, 10 000 options, 1 000 widgets) because a small file otherwise asks for an enormous document and all the memory to build it.

It refuses before it writes, for everything that makes a file which opens and is broken: a field with no name, a choice with no rows, a checkbox whose on-state is the reserved Off, two widgets of one radio group in the same state, a comb with no maxLen, an empty rectangle, a page the document does not have, and two fields of one name — which the reader would silently shadow.

Controls

The oracle throughout is the reading half — forms.Read and the fields verb — rather than these tests' own expectations.

coverage 100% of statements, 0 uncovered blocks in both packages, counted on the profile
mutations caught the checkbox appearance never written; NeedAppearances not set; the existing form replaced instead of joined

runAddFields uses the c.open and save helpers every other verb uses rather than its own copy of the password, the write and their error paths — which is also how the last uncovered block went away.

Not in this change

pdfops uses the standard flag package across all 54 verbs rather than cobra. I matched the siblings rather than make one verb the odd one out; converting the command is its own piece of work.

🤖 Generated with Claude Code

Doc.AddFields and the addfields verb: the other half of Fill, which can only
change a field the document already has. This is what BentoPDF's form-creator
needs, and the fleet had every other piece already.

Fields are held until the write, because a widget names the page OBJECT it
sits on and the pages have no object numbers until then. A field showing in
one place is one object, which is what nearly every producer writes; a field
showing on several pages becomes a parent with one Kid per place.

Two details the format makes easy to get wrong, both held by tests:

A checkbox's drawing is not generated by the viewer. NeedAppearances covers
text and choice fields, but a button shows whatever its own appearance
dictionary holds for the state it is in, so one written without them is a box
that never looks ticked however often it is clicked. One is drawn per state
with the path operators rather than with a font, so the document needs no
ZapfDingbats to show a tick.

A widget is marked Print. Without it the field is on screen and gone from the
paper, which is where a filled form is most often looked at.

A document that already has a form is joined rather than replaced: replacing
it would drop every field the file came with, and the file would still open. A
document assembled from several files can still be given a form, because
fields the caller asked for come from neither source and so are in no
conflict.

The description the verb reads is JSON, since the thing being described is a
layout and a command line is a poor place to put one. Unknown keys are
refused: "mutliline" is a form missing the thing it was written to ask for,
and a parser that ignores what it does not know says nothing about it. The
list lengths are bounded, because a small file otherwise asks for an enormous
document and all the memory to build it.

The oracle throughout is the reading half -- forms.Read and the fields verb --
rather than these tests' own expectations.

Measured: 100% of statements and 0 uncovered blocks in both packages, counted
on the profile rather than the rounded percentage. Three mutations caught (the
checkbox appearance never written; NeedAppearances not set; the existing form
replaced instead of joined).

runAddFields uses the c.open and save helpers every other verb uses rather
than its own copy of the password, the write and their error paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 0755bfa into main Oct 22, 2026
1 check passed
@tannevaled
tannevaled deleted the a-form-can-be-added-to-a-document branch October 22, 2026 08:07
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