Skip to content

Fill in a form and save it - #10

Merged
tannevaled merged 1 commit into
mainfrom
fill-forms
Aug 26, 2026
Merged

Fill in a form and save it#10
tannevaled merged 1 commit into
mainfrom
fill-forms

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

ops.OpenForm reads a document's form, Fill sets a field, Bytes writes the result. pdfops fields lists what a form asks for and what it holds; pdfops fill fills it in.

Why an incremental update

Every other verb here takes pages apart and builds a new document round them. A form cannot be treated that way: it is tied into the document by object number in a dozen places at once — the field tree, the widget annotations on the pages, the resources the appearances name.

So a filled form is written as the original file, byte for byte, with the changed objects appended and a new cross-reference section pointing back at the old one. That is how every program that saves a form saves one, and it is the safest thing a program can do to somebody's document.

The defect this found, and how

The first version wrote a plain xref table whatever the file already used. Our own reader read it back perfectly. macOS's renderer drew nothing at all — because a file whose cross-reference is a stream cannot be pointed back at by a plain table: a reader following /Prev finds an object where it expects the word xref.

That is the argument for putting output in front of a second renderer rather than only reading it back with the one that wrote it.

Measured

fw9.pdf, a real IRS form: every field filled, written out, read back with all 23 values in place and all 23 widgets carrying a drawing, and rendered by macOS.

macOS's own rendering, blank → filled changed by 0.0028 — it saw the values
our distance from macOS, blank 0.1095
our distance from macOS, filled 0.1131

The 0.11 baseline is our text against Quartz's on a dense form; it is the same before and after, so it says nothing about forms.

Running pdfops fields at that form is also what found field names coming back as raw UTF-16 bytes — unreadable and unmatchable (fixed in forms v0.2.1).

Stated plainly

  • A document that had to be repaired to be read is refused rather than added to.
  • An encrypted document is refused — everything in it is written through a key.
  • Filling is the only verb that keeps a form. The others lose it, and the README now says so.

100% statement coverage in both the package and the command, go vet clean, nine cross-compile targets.

Filling in a form is not like the other verbs. The rest of this package takes
pages apart and puts them together again, and what it does not understand it
leaves behind. A form cannot be treated that way: it is tied into the document
by object number in a dozen places at once — the field tree, the widget
annotations on the pages, the resources the appearances name — and a document
rebuilt around it would have to rebuild all of that correctly or quietly break
it.

So a filled form is written as an incremental update: the original file, byte
for byte, with the objects that changed appended after it and a new
cross-reference section pointing back at the old one. Nothing already there is
rewritten, so everything this does not understand survives untouched, and if
the update is wrong the original is still the first part of the file.

The update has to say where its objects went the same way the file already
does. A file whose cross-reference is a stream cannot be pointed back at by a
plain table: a reader following /Prev finds an object where it expects the word
"xref". That is not a nicety. The first version wrote a plain table either way,
and our own reader was lenient enough to read it back perfectly — while macOS's
renderer drew nothing at all for the file. That is how it was found, and it is
why a filled form is now put in front of another renderer rather than only read
back by this one.

Two things it will not do, and says so: a document that had to be repaired to
be read has no section worth pointing back at, and a document written through a
key would need everything appended to it written through that key too.

Measured on a real form. fw9.pdf, every field filled, written out, read back
with all 23 values in place and all 23 widgets carrying a drawing — and
rendered by macOS, which drew what we drew, in the same places, comb cells and
ticks included. Its own rendering moved by 0.0028 when we filled the form in,
so it saw the values; our distance from it moved 0.1095 to 0.1131, which is the
same order, and the 0.11 itself is our text against Quartz's on a dense form
and not a thing about forms at all.

pdfops gains "fields", which lists what a form asks for and what it holds, and
"fill". Running the lister at a real form is what found that field names are
written in UTF-16 and were coming back as bytes — unreadable, and worse,
unmatchable.
@tannevaled
tannevaled merged commit 8ee6bbc into main Aug 26, 2026
1 check passed
@tannevaled
tannevaled deleted the fill-forms branch August 26, 2026 14:23
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