Forms in PDF documents follow the
AcroForm definition
Show archive.org snapshot
.
Prawn has no native support for it, but you can build form fields through plain PDF objects.
The card is only about text fields, since I have only used those.
Text fields
Here is how to add a single-line text field, e.g. in a Prawn::View class.
Note that ref! and state are forwarded to the Prawn::Document instance.
First, you must create an AcroForm data object in the PDF. You can then add field objects to it.
Note that PDF viewers do not paint a line or box for fields; you must draw that yourself and make the AcroForm fields overlap them.
Example
I recommend you define a method so you can say the following.
add_text_field('customer_number', left: 0, bottom: cursor - 14, right: 200, top: cursor)
The method itself would look as follows. (Options are explained below.)
DEFAULT_APPEARANCE = '/Helv 9 Tf 0 g'.freeze
def add_text_field(name, left:, bottom:, right:, top:)
field = ref!(
Type: :Annot,
Subtype: :Widget,
FT: :Tx,
T: PDF::Core::LiteralString.new(name),
Rect: [bounds.absolute_left + left, bounds.absolute_bottom + bottom, bounds.absolute_left + right, bounds.absolute_bottom + top],
F: 4,
DA: PDF::Core::LiteralString.new(DEFAULT_APPEARANCE),
P: state.page.dictionary,
)
state.page.dictionary.data[:Annots] ||= []
state.page.dictionary.data[:Annots] << field
acro_form[:Fields] << field
end
def acro_form
state.store.root.data[:AcroForm] ||= {
Fields: [],
NeedAppearances: true,
DA: PDF::Core::LiteralString.new(DEFAULT_APPEARANCE),
DR: { Font: { Helv: ref!(Type: :Font, Subtype: :Type1, BaseFont: :Helvetica, Encoding: :WinAnsiEncoding) } },
}
end
Explanation
We use an API that is somewhat arcane. Here is what all those options mean.
For the field
-
ref!stores the field data and returns a reference to it. Both the page and the form point to it. -
Type: :Annot, Subtype: :Widget: the field is also its own widget annotation, i.e. the interactive area on the page. A field with a single widget may be merged with it into one dictionary like this. -
FT: :Tx: a text field. Other types are:Btn(check boxes, radio buttons, push buttons),:Ch(list and combo boxes) and:Sig(signatures). -
T: the field's name, by which viewers and tools (form data export,pdftk fill_form) identify it. Fields of the same name share their value. -
Rect: where the field sits, as[left, bottom, right, top]in points from the page's bottom left corner. Prawn's coordinates are relative to the current bounding box, hence thebounds.absolute_*offsets. -
F: 4: annotation flags. 4 is "Print": without it, viewers leave out the field, and whatever was typed into it, when printing. -
DA: the default appearance, i.e. content stream operators for the field's text./Helv 9 Tfselects the font resourceHelvat 9 pt,0 gfills in gray level 0 (black). -
P: the page the widget is on.
Note that we use a PDF::Core::LiteralString for T and DA to write them without changing encoding. Prawn's pdf-core writes plain Ruby strings as UTF-16 hex strings (<FEFF...>). However, drawing operators (DA) must not be UTF-16 by PDF standard -- and while T is allowed to be UTF-16, we want to read it back in tests without having to decode.
For the page
-
Annotslists the page's annotations. We must add our fields there for them to appear.
For the document catalog
We add an AcroForm entry with:
-
Fields: the document's fields. -
NeedAppearances: trueasks the PDF viewer to draw the fields itself, since we provide no appearance streams (AP).
(This is deprecated in PDF 2.0, but Prawn generates PDF 1.x). -
DA: default appearance for fields without one of their own. -
DR: default resources, i.e. the fonts aDAmay refer to.-
Helvhere is Helvetica, one of the 14 standard fonts every viewer knows, so it needs no embedding. -
WinAnsiEncodingcovers Western European characters like umlauts.
-
Tests
You can use pdf-reader Show archive.org snapshot to access fields. Example:
pdf = Prawn::Document.new { ... }.render
reader = PDF::Reader.new(StringIO.new(pdf))
objects = reader.objects
fields = objects.deref(objects.deref(objects.trailer[:Root])[:AcroForm])[:Fields]
expect(fields.map { |field| objects[field].values_at(:FT, :T) }).to eq([[:Tx, 'customer_number']])