PdfElixide.Form.fields/1 reads a PDF's AcroForm fields, from a read-only
PdfElixide.Document or from a mutable PdfElixide.Editor alike. Writing needs
an editor, since a document cannot be changed.
alias PdfElixide.Document
alias PdfElixide.Editor
alias PdfElixide.Form
doc = Document.open!("path/to/form.pdf")
Form.fields!(doc)
#=> [%PdfElixide.Form.Field.Text{name: "full_name", kind: :single_line, value: "John Doe", …},
# %PdfElixide.Form.Field.Button{name: "subscribe", kind: :check_box, value: true, …},
# %PdfElixide.Form.Field.Choice{name: "country", kind: :list_box, value: nil, …}]The read-only examples through "Field kinds and flags" reuse this doc; close
it after the last one.
Fields and their values
A field comes back as one struct per field type, so the type is what you match
on: PdfElixide.Form.Field.Text (/Tx), .Button (/Btn — push buttons, check
boxes and radio groups), .Choice (/Ch), and .Unknown for a field with no
recognized type, which includes the grouping parents a nested form reports.
PdfElixide.Form.Field is the umbrella defining the union. Which widget a
button or choice field is, the struct's :kind says — see below.
Every struct carries the same six keys. :name is the field's fully qualified
name, dotted for a field nested under a parent — "person.first", not "first"
— and is what every other function here addresses it by. :value is a
plain term: a string, true/false, a list of strings, or nil for a field
carrying no value. :default_value is the reset value the field itself
declares, in the same shapes — not always what a viewer's reset would restore,
for the reason "What a nested field inherits" gives. :tooltip is the text a
viewer shows on hover, and :rect the box the field occupies on the page.
:flags is described under "Field kinds and flags" below, along with the :kind
the first three also carry; the rest of the metadata is under "What else a field
reports".
PdfElixide.Form.Field.value/0 is both what a field reports and what
PdfElixide.Form.put_value/3 accepts. Anything else raises ArgumentError, so a
value read from one form can be written to another. Button fields are the
exception described below.
For one field there is no need to walk the list. PdfElixide.Form.field/2
returns the struct and PdfElixide.Form.value/2 just its value, from either
source:
Form.value!(doc, "full_name")
#=> "John Doe"
Form.field(doc, "country")
#=> {:ok, %PdfElixide.Form.Field.Choice{name: "country", kind: :list_box, value: nil, …}}{:ok, nil} and :not_found are different answers. A field that exists but
carries no value is {:ok, nil}; a name the form does not carry is
{:error, %PdfElixide.Error{reason: :not_found}}, from field/2 and value/2
as much as from put_value/3. The bang variants raise it instead.
Field kinds and flags
A field's /FT says only that it is a button, a choice field or a text field.
Which widget it is — a check box or a radio group, a combo box or a list box —
is decided by bits in its /Ff entry, and those bits are what :kind reports:
| Struct | :kind | Default |
|---|---|---|
PdfElixide.Form.Field.Button | :check_box, :radio, :push | :check_box |
PdfElixide.Form.Field.Choice | :combo_box, :list_box | :list_box |
PdfElixide.Form.Field.Text | :single_line, :multiline | :single_line |
case Form.field!(doc, "subscribe") do
%Form.Field.Button{kind: :check_box, value: checked?} -> checked?
%Form.Field.Button{kind: :radio, value: selected} -> selected
%Form.Field.Button{kind: :push} -> nil
endA field declaring no /Ff is not unknown. Every bit is clear, producing the
defaults above. Many real forms declare no /Ff at all.
A field inherits /Ff from its ancestors. A radio-group parent can therefore
supply the flags for kids that carry none, and each kid still reports :radio.
A kid's own /Ff replaces the inherited value instead of merging bit by bit, so
a :push button under a :radio parent stays a push button. "What a nested
field inherits" below lists the other inherited keys.
:flags carries the whole entry decoded, one boolean per bit the specification
names for that type, plus :raw for anything it does not:
Form.field!(doc, "notes").flags
#=> %PdfElixide.Form.Field.Text.Flags{multiline: true, password: false,
# read_only: false, required: false, comb: false, …, raw: 4096}
:ok = Document.close(doc)Each type has its own flags struct — PdfElixide.Form.Field.Text.Flags,
.Button.Flags, .Choice.Flags — because the same bit means different things
on different types. PdfElixide.Form.Field.Unknown carries
PdfElixide.Form.Field.Flags, which holds the three bits every field has:
:read_only, :required and :no_export.
PdfElixide.Document.Annotation reports the same classification for a widget
annotation, through its :field_type, so the two surfaces agree about a field
that appears on both.
What else a field reports
Beyond its name, value and flags, a field carries the metadata a form filler needs to render or validate it. Which keys a struct has depends on its type:
| Key | Text | Button | Choice | Unknown |
|---|---|---|---|---|
:tooltip, :rect, :default_value | ✓ | ✓ | ✓ | ✓ |
:max_length | ✓ | |||
:alignment | ✓ | ✓ | ||
:options | ✓ |
:max_length is the /MaxLen cap on how many characters may be entered; 0 is
a declared zero, not an absence. :alignment is :left, :center or :right,
and is nil both for a field declaring no justification and for one declaring a
value the PDF specification does not define.
:rect is the field's own box, which not every field has. A field and its widget
are often one dictionary, and then :rect is that widget's rectangle. A field
whose widgets are separate objects — a radio group, or any field appearing on
more than one page — reports nil, as does a field with no widget.
:tooltip reports nil both for absent text and text that could not be decoded.
What a nested field inherits
A field inherits its type. A leaf under a text-field parent is therefore a
PdfElixide.Form.Field.Text, not an Unknown, even when it declares no type of
its own. A field's own type takes precedence.
Four more keys are resolved the same way, so a field nested under a parent reports the parent's value where it declares none of its own:
:flags:options:alignment:max_length
A field declaring its own replaces the inherited value outright rather than
combining with it — the rule holds for all four, so a /MaxLen 3 leaf under a
/MaxLen 12 parent caps at 3 and a :push button under a :radio parent stays
a push button.
The PDF specification does not require :options to be inherited, and readers
differ. This library reports a parent's /Opt for a nested field that declares
none, while a strict reader may report no options. Only a field's own /Opt is
portable across viewers.
:value and :default_value are not inherited this way. Each comes from the
field's own dictionary, so both report nil when only the parent carries /V
or /DV. A viewer may still inherit that default when resetting the form. Read
a named parent directly to reach its value; a grouping level with neither a name
nor a type is not reported and its value cannot be reached.
A field written inline rather than as an indirect reference inherits nothing.
The PDF specification requires /Fields and /Kids entries to reference
separate objects. A hand-built form that puts a field dictionary directly in
either array is still reported, but only with what its dictionary declares. Its
referenced children remain unaffected.
A choice field's options
:options is what a combo box or list box permits, in the order the PDF lists
them:
Form.field!(doc, "country").options
#=> ["FR", {"DE", "Germany"}, "IT"]An entry is a plain string when the PDF spells the option as one value, and
{export, display} when it spells it as a pair. This API reads and writes the
export value; the display value is what a viewer shows. {"DE", "Germany"} is
therefore one option, not two.
# Every value this field will accept, whichever way each option is spelled.
Enum.map(field.options, fn
{export, _display} -> export
export -> export
end)nil means the field declares no options at all; [] means it declares an
empty list. An entry the PDF spells as neither a string, a name nor a pair is
skipped, and the options around it are still reported.
Options follow the inheritance rules above. A field's own /Opt replaces the
inherited list rather than extending it.
PdfElixide.Document.Annotation also reports a widget's options, but as export
values only — ["FR", "DE", "IT"] for the field above. A field's :options is
the one that keeps the display text.
Filling a form
Open the file as an editor, write values, then persist. Every call that changes an editor returns it, so the whole thing is one pipeline:
editor = Editor.open!("path/to/form.pdf")
try do
editor
|> Form.put_value!("full_name", "Jane Doe")
|> Form.put_value!("subscribe", true)
|> Editor.save!("path/to/filled.pdf")
:ok
after
Editor.close(editor)
end
#=> :okThe values are the plain terms fields/1 returns — no wrapper or tag. Fields
are addressed by name, and only existing fields can be written. An unknown name
is an error; this API cannot add fields.
The tuple-returning half
The non-bang functions are uniform in the same way — each returns
{:ok, editor} — so they read as one with/1 with no shape changes in the
middle:
with {:ok, editor} <- Editor.open("path/to/form.pdf") do
try do
with {:ok, editor} <- Form.put_values(editor, %{"full_name" => "Jane Doe"}),
{:ok, _editor} <- Editor.save(editor, "path/to/filled.pdf") do
:ok
end
after
Editor.close(editor)
end
end
#=> :okPdfElixide.Editor.to_binary/2 and PdfElixide.Editor.close/1 are the two ways
such a mutating pipeline ends: one hands back bytes, the other :ok. Every
mutating step before them hands back the editor.
Several fields at once
PdfElixide.Form.put_values/2 takes a map with string keys, or a list of
{name, value} pairs, and validates all of them before it writes any:
unknown names, duplicates, names that are not strings and values outside
PdfElixide.Form.Field.value/0 are all caught up front, against a single
fields/1 read.
{:ok, editor} = Form.put_values(editor, %{"full_name" => "Jane Doe", "subscribe" => true})
# A list when the order matters — a map is applied in `Enum` order, which is unspecified.
{:ok, editor} = Form.put_values(editor, [{"full_name", "Jane Doe"}, {"country", ["Canada"]}])It is not a transaction. A failure after validation stops at the first error, with any earlier writes already applied. It is a convenience for validation and composition, not an atomic batch.
PdfElixide.Form.update_value/3 transforms a field in place, handing fun the
current value and writing back whatever it returns:
{:ok, editor} = Form.update_value(editor, "full_name", &String.upcase/1)
# A field carrying no value hands `fun` a nil.
{:ok, editor} = Form.update_value(editor, "country", fn
nil -> ["Canada"]
other -> other
end)This is not an atomic read-modify-write. Another process holding the same editor can write between the read and the write.
Saving
Nothing is written until PdfElixide.Editor.save/3 writes a file or
PdfElixide.Editor.to_binary/2 hands back the bytes, and neither consumes the
editor: keep editing and write again. close/1 discards unsaved edits, so
write before you close.
{:ok, editor} = Editor.save(editor, "path/to/filled.pdf")
{:ok, bytes} = Editor.to_binary(editor)Both accept PdfElixide.Editor.save_opts/0: :incremental, :compress
and :garbage_collect. The exception is to_binary/2 with
incremental: true, which returns
{:error, %PdfElixide.Error{reason: :invalid_pdf}}: an incremental update must
be appended to the original file, so use save/3 for one.
For form filling against an existing PDF, an incremental save appends only the field-value updates and leaves the original AcroForm structure as it was:
{:ok, editor} = Editor.save(editor, "path/to/filled.pdf", incremental: true)to_binary/2 clears PdfElixide.Editor.modified?/1 even though it writes no
file; an incremental save/3 leaves it set.
to_binary/2 builds the whole output in native memory before copying it into an
Elixir binary, so peak usage includes both copies on top of the editor. For a
very large document, prefer save/3, which writes to the file without that
second full-size buffer.
Flattening
Flattening draws a field's appearance into the page content and takes the
interactive field away, so the written PDF shows the filled values but can no
longer be edited. PdfElixide.Form.flatten/1 covers the whole document,
flatten/2 one page:
editor
|> Form.put_value!("full_name", "Jane Roe")
|> Form.flatten!()
|> Editor.to_binary!()PdfElixide.Editor.flatten_annotations/1,2 is the same idea for annotations —
notes, highlights, stamps — and is a separate mark from the form one.
Nothing happens until the next full write. Both calls only mark what to
flatten; the drawing happens inside PdfElixide.Editor.save/3 or
PdfElixide.Editor.to_binary/2. Until then Form.fields/1 still reports every
field, because the editor is unchanged — what changes is the file you write.
PdfElixide.Editor.modified?/1 does go true at mark time.
An incremental save does not flatten. save(editor, path, incremental: true)
writes an unflattened file, reports no error and produces no warnings. An
incremental update appends to the original, and the original's fields are still
there. Write with save/3 without :incremental, or with to_binary/2.
A mark cannot be removed, and it applies to every later write. There is no unflatten; reopen the source if you need an unflattened document. Writing twice gives you two flattened files.
What each one leaves behind
Form.flatten/1 removes the document's AcroForm outright. Form.flatten/2
keeps it, rebuilt to hold only the fields that still have a widget on a page you
left alone. A field whose widgets do not say which page they are on is kept by a
partial flatten regardless of the selected page. Form.flatten/1 takes any
signature field with the AcroForm, so a signed document comes back unsigned —
the signature dictionary is still in the file, but nothing points at it.
flatten/2 keeps a signature field whose widgets are not on a page you flattened.
Any non-incremental write invalidates a signature whether or not it also removes
the field.
Both remove form-field widgets from a page's annotations while leaving notes,
links, and highlights unchanged. In hand-built PDFs, however, an annotation
written inline in /Annots rather than as an indirect reference is silently
dropped regardless of type.
Editor.flatten_annotations/1,2 is blunter. On a page where at least one
annotation appearance can be produced, it removes every annotation entry,
including form field widgets and annotations it could not draw. A skipped
annotation can therefore be deleted without being rendered or reported. If no
annotation on the page produces an appearance, the write creates no flatten data
for that page and draws or removes nothing.
Do not mark both kinds of flattening on the same page: where appearances are produced, the two marks are applied independently and fields can be drawn twice.
Check the warnings
PdfElixide.Editor.flatten_warnings/1 lists what could not be flattened
faithfully. It is empty until a write has happened, and it accumulates for the
life of the editor rather than being cleared per write — so read it after the
write you care about:
{:ok, bytes} = editor |> Form.flatten!() |> Editor.to_binary()
for warning <- Editor.flatten_warnings!(editor) do
Logger.warning("flatten: #{warning}")
endTreat an empty list as "nothing was reported", not as "nothing was lost". The list is a best effort: an inline annotation is dropped with no entry, and so is a widget whose appearance stream cannot be loaded. An empty list cannot establish that flattening was faithful.
Reported cases include:
A newly set value containing non-Latin text or emoji that the shipped appearance path cannot render faithfully. The field may contain incorrect glyphs or none at all while the PDF remains valid. Check the warnings after filling and flattening text outside Latin-1. Existing appearance streams are copied unchanged and are unaffected.
The warning may tell you to rebuild with an optional feature. The installed package is precompiled and cannot be reconfigured that way. Read the warning as "this field did not flatten legibly" and handle it in your own code — leave the form unflattened, substitute a value the field's font can render, or draw the text yourself before flattening.
A field with no appearance stream that could not be given one. The warning names the field. If another appearance causes that page to be flattened, the field is removed without being drawn; if the page produces no appearances at all, nothing on it is drawn or removed.
An XFA form left as it was after a per-page flatten, whose XFA data may still reference widgets that are now gone.
An editor is a handle, not a value
The editor returned by a mutating call is the same handle that went in:
editor = Editor.open!("path/to/form.pdf")
filled = Form.put_value!(editor, "full_name", "Jane")
# `editor` and `filled` are the same handle — the original is filled too.
:ok = Editor.close(editor)A pipeline therefore sequences effects; an earlier binding does not preserve the document's previous state. Reopen the source when a separate state is needed. The Concurrency guide describes sharing editor and form handles across processes.
Signature fields
A signature field (/FT /Sig) is not a fillable field, and this API does not
have one: fields/1 omits it, and field/2, value/2 and put_value/3 all
answer {:error, %PdfElixide.Error{reason: :not_found}} for its name, the same
result as an unknown field name. put_values/2 reports it the same way, from the
fields/1 read it validates against.
A signature's /V is a signature dictionary rather than a form value. This API
never writes over it; doing so — with nil included — would replace that
dictionary and silently remove the signature.
Flattening is the exception: PdfElixide.Form.flatten/1 removes the whole
AcroForm and a signature field goes with it, as "Flattening" above describes.
This holds for a field whose /FT is declared on an ancestor rather than on the
field itself, which the PDF specification permits.
Reading the signatures themselves is a separate capability, and
PdfElixide.Signature is where it lives. The Signatures
guide covers listing, byte verification, certificates, timestamps, and damaged
documents that signature reads reject but field reads tolerate.
Check boxes and radio groups
:kind tells the two apart, per "Field kinds and flags" above. What follows
applies to both, and to writing rather than reading.
Setting a button field writes /Yes for true and /Off for false, and those
are the only two states put_value/3 can produce. That makes the read-then-write
round trip lossy for some check boxes and radio groups, in two ways.
A box whose on-state is /On rather than /Yes becomes unchecked after a
read-then-write round trip. It reads as true, since both names mean
"checked", but writing that true back emits /Yes — which is not the state
the widget declares. Nothing in the value reveals this; the two spellings are
indistinguishable once read. (/No collapses to false and writes /Off in the
same way, but harmlessly: /Off is the off state for every check box.)
A box whose on-state is a custom name — /Export1, say — cannot be checked
at all. true writes /Yes, which matches no widget state, and no other value
writes a PDF name either. Writing the on-state's name as a string is not a
workaround and makes matters worse: it goes into /V and is copied into the
widget's /AS, where the PDF specification requires a name, so a reader may
render the field wrongly.
Either field needs its dictionaries edited directly, which this library does not expose. Reading such a field is unaffected — only writing one back is.