Acrofill

Pure-Ruby PDF form (AcroForm) filling and flattening. No pdftk, no Java, no native extensions — drop it into any Ruby or Rails project and fill PDF templates in-process.

Acrofill.fill_form('template.pdf', 'out.pdf',
                   { 'Full Name' => 'Jane Roe', 'agree' => 'Yes' },
                   flatten: true)

Parsing is delegated to pdf-reader (MIT), which handles classic and cross-reference-stream PDFs, object streams, and every standard filter. Acrofill adds the write side: field value setting, appearance-stream generation, checkbox/radio state selection, flattening, and a full document serializer.

Why

The usual Ruby answer to "fill this PDF form" is the pdf-forms gem, which shells out to pdftk — a Java application you must install on every machine and container, and pay a JVM start-up for on every call. The main in-process alternative, HexaPDF, is AGPL/commercial. Acrofill is MIT-licensed and fills a form in a few milliseconds in-process.

Installation

# Gemfile
gem 'acrofill'

Usage

require 'acrofill'

# Discover fields in a template
Acrofill.field_names('form.pdf')
# => ["Full Name", "amount", "agree", "color", "notes"]

Acrofill.fields('form.pdf').map { |f| [f.name, f.type, f.states] }
# => [["Full Name", :Tx, nil], ["agree", :Btn, [:Yes]], ["color", :Btn, [:Red, :Blue]], ...]

# Fill: text fields take strings, checkboxes/radios take a state name
Acrofill.fill_form('form.pdf', 'out.pdf', {
  'Full Name' => 'Jane Roe',
  'agree' => 'Yes',        # checkbox: state name, or anything truthy
  'color' => 'Blue',       # radio group: state name
  'notes' => "line one\nline two" # multiline fields wrap automatically
})

# Flatten: burn values into page content and drop the interactive form
Acrofill.fill_form('form.pdf', 'out.pdf', data, flatten: true)

# Filling the same template many times (the typical server pattern)?
# Parse it once — each subsequent fill skips parsing entirely (~3-8x
# faster per fill) and works on its own copy, so it is thread-safe:
template = Acrofill::Template.new('form.pdf')
template.fill_form('out1.pdf', { 'Full Name' => 'Jane' }, flatten: true)
template.fill_form('out2.pdf', { 'Full Name' => 'John' }, flatten: true)

Drop-in for pdf-forms

Acrofill.new accepts (and ignores) a pdftk path, so most PdfForms call sites work unchanged:

filler = Acrofill.new                    # was: PdfForms.new('/usr/bin/pdftk')
filler.fill_form(tpl, out, data, flatten: true)

Unknown field names are silently ignored, matching pdftk.

Performance

Because Acrofill runs in-process, it avoids the JVM (or C++ process) start-up that dominates a pdftk subprocess call — the exact pattern a web app hits when it fills one form per request. Filling and flattening three real-world government claim forms, 50 iterations each, on an Apple M-series laptop against pdftk-java 3.3.3:

Form acrofill pdftk (java) speedup
form A (124 fields, 3 pp) 49 ms 192 ms 3.9×
form B (53 fields, 2 pp) 16 ms 124 ms 7.6×
form C (187 fields, 2 pp) 47 ms 154 ms 3.3×
weighted total 4.2×

Reproduce with your own templates — the benchmark discovers each form's fields automatically:

ruby benchmark/compare.rb path/to/*.pdf   # ITERATIONS=50 by default

Self-contained synthetic benchmark

benchmark/synthetic.rb needs no templates — it generates a form with a configurable number of text fields and reports three variants. Because pdftk (a CLI) has no server mode, the JVM boots on every call; to be fair we also measure that start-up cost separately and subtract it, so pdftk warm models the marginal PDF-processing cost with the runtime already hot (a lower bound on any long-lived-pdftk setup):

$ FIELDS=30 ruby benchmark/synthetic.rb
variant                        ms/fill vs acrofill
acrofill (in-process)             3.4       1.0×
acrofill (cached template)        1.1       0.3×
pdftk cold (subprocess)         122.3      36.0×
pdftk warm (JVM excluded)        90.6      26.6×
(measured pdftk start-up overhead: ~32 ms/call)

Even discounting JVM start-up entirely, Acrofill is ~13–29× faster on synthetic forms (the multiplier shrinks as field count grows: ~29× at 30 fields, ~13× at 80). The gap is largest on simple forms — exactly the common case — and start-up is a real per-request cost for any pdftk-CLI integration. The pdftk warm figure is a conservative estimate: pdftk --version loads fewer classes than an actual fill, so true warm cost is somewhat higher than shown.

Parsing dominates a one-shot fill (~86% of the time on a real 3-page form), which is what Acrofill::Template eliminates: on the 124-field form above a one-shot fill takes ~47 ms while a cached-template fill takes ~6 ms — against pdftk's ~192 ms for the same job.

Security

The PDF template is treated as untrusted input (the typical "upload a PDF and fill it" case), and so are field names and values. Acrofill is pure Ruby with no eval/system/native calls in its hot path, so the worst an input can do is cause an exception — never memory corruption or code execution. Specifically it defends against:

  • Cyclic, exponentially-shared and deeply nested object graphs — the page tree and field tree walkers are iterative and carry visited-sets, so a /Pages or /Kids cycle, a "billion laughs" DAG, or a thousands-deep linear chain terminates in O(objects) instead of hanging or overflowing the stack.
  • Content-stream injection via /DA — the template's default appearance string is sanitized: the font name must be a regular PDF name and only well-formed colour operators (g/rg/k) are copied into generated appearances. Field values are always written as escaped literals / hex strings, never as operators.
  • Malformed scalars — non-finite reals, deeply nested arrays, and mistyped dictionaries are clamped or rejected rather than crashing the writer.
  • Encrypted / unparseable input — rejected up front; every failure at the parse boundary surfaces as Acrofill::Error (including lazy per-object parse errors and parser stack overflow on pathologically nested objects), so callers only ever rescue one error type.

Feature matrix

Supported:

  • Text fields (/Tx) — hierarchical names (parent.kid), inherited /DA, alignment via /Q (left/center/right), auto font size (0 Tf), shrink-to-fit for overflowing values, multiline fields (/Ff bit 13) with word wrapping, standard-14 width metrics (Helvetica/Courier/Times).
  • Checkboxes and radio groups (/Btn) — state selection via /V+/AS using the template's own appearance states.
  • Choice fields (/Ch) — value set and rendered like text.
  • Flattening — every visible widget appearance is stamped into the page content; widget annotations and the AcroForm dictionary are removed.
  • Values in any encoding (stored as UTF-16BE when needed; appearances render the Windows-1252 subset).

Not supported (rejected or ignored, never a hard crash):

  • Encrypted documents (raise Acrofill::Error), XFA forms, digital signatures, JavaScript actions.
  • Push buttons; comb fields render as plain text; rich text (/RV) is dropped on fill.
  • Glyphs outside Windows-1252 in generated appearances (stored values keep full Unicode; unrenderable glyphs appear as ?).

Development

bundle install
bundle exec rake spec

The test suite generates its fixture PDFs from scratch — the repository contains no binary files.

License

MIT. See LICENSE.txt.