Prawn: How to add form fields to a PDF

Posted . Visible to the public.

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 the bounds.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 Tf selects the font resource Helv at 9 pt, 0 g fills 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

  • Annots lists 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: true asks 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 a DA may refer to.
    • Helv here is Helvetica, one of the 14 standard fonts every viewer knows, so it needs no embedding.
    • WinAnsiEncoding covers 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']])
Profile picture of Arne Hartherz
Arne Hartherz
Last edit
Arne Hartherz
License
Source code in this card is licensed under the MIT License.
Posted by Arne Hartherz to makandra dev (2026-09-29 12:39)