Generate Pixel Perfect PDF Forms with PDFtk and Node.js

Written by
Michael Godshall

A recent project at FullStack Labs needed PDF forms to be populated dynamically with requested data from a database. Previous projects required generating basic PDF documents with HTML and CSS, but this project required the forms to be pixel-perfect.

The nature of the project called for a large number of these forms to be generated, which we determined would be a costly endeavor if each form was generated with HTML and CSS.

After some research, my team discovered that this process can be streamlined significantly by creating a fillable PDF template and then programmatically filling in each field when the form is requested.

We hoped to find a pure JavaScript solution for our Node.js app, but the packages we found at the time were poorly documented and difficult to use. Instead, we found PDFtk to be the easiest way to populate fillable PDFs, which provides a CLI for reading and writing to PDF form fields. We also used the node-pdftk package which provides a Node.js wrapper for PDFtk.

This article will guide you through the process of bringing the tools and ideas above together in a Node.js application, but the same ideas can be applied to other languages that have their own PDFtk wrappers.

Create a fillable PDF template

The first step is to create a PDF template with fillable form fields. This can be accomplished using Adobe Acrobat Reader DC and the Prepare Form feature. 

The advantage of this approach is that it provides non-technical users the ability to create fillable PDF forms on their own which is typically a more economic solution for clients (especially if numerous forms need to be created and maintained). The Prepare Form feature does require a premium subscription to Adobe Acrobat Reader DC, but the cost is negligible compared to the savings on the development side.

When the fillable form is created, a name is assigned to each field that can later be targeted programmatically by a developer with PDFtk.

For the purposes of this article, you can download the 1099-NEC form from the IRS’s website, which contains fillable form fields.

pixel-perfect PDF form

Identify the PDF form fields to populate

When a developer receives a fillable PDF form, PDFtk reads the PDF and identifies the field names that can be populated. 

To get started, install pdftk locally with Homebrew.


$ brew install pdftk-java

Then, verify that the installation was successful and that the pdftk command is available.


$ pdftk -version

pdftk port to java 3.1.1 a Handy Tool for Manipulating PDF Documents
Copyright (c) 2017-2018 Marc Vinyals - https://gitlab.com/pdftk-java/pdftk
Copyright (c) 2003-2013 Steward and Lee, LLC.

pdftk includes a modified version of the iText library.
Copyright (c) 1999-2009 Bruno Lowagie, Paulo Soares, et al.

This is free software; see the source code for copying conditions. There is
NO warranty, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Now, run the dump_data_fields command on the 1099-NEC form you downloaded in the previous section to retrieve data for the available form fields.


$ pdftk path/to/f1099nec.pdf dump_data_fields
-
‍/* Sample output */
-

FieldType: Text
FieldName: topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_1[0]
FieldFlags: 8392704
FieldJustification: Left
-

FieldType: Text
FieldName: topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_2[0]
FieldFlags: 0
FieldJustification: Center
FieldMaxLength: 11
-

FieldType: Text
FieldName: topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_3[0]
FieldFlags: 0
FieldJustification: Center
FieldMaxLength: 11
-

The main information that you need from this output is the FieldName, which tells you how to target the form fields programmatically in your code. 

You’ll notice that most of the FieldNames on this form are a bit cryptic, like topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_1[0], so it might take some trial and error to figure out the actual fields the FieldNames correspond to. If desired, you can also use the Prepare Form feature in Adobe Acrobat Reader DC to inspect or edit the FieldNames so they are more intuitive for development purposes.

Fill out the PDF fields programmatically

1.  Add node-pdftk to your project.


$ npm install node-pdftk

2. Create a file called generate1099Form.js, and import node-pdftk.


import pdftk from node-pdftk;

3. Add example payer and contractor objects with relevant data. This can be enhanced later to dynamically load the information from a database.


const payer = {
 name: 'My Biz LLC',
 address: '2nd Street',
 city: 'San Francisco',
 state: 'CA',
 zipCode: '55555',
 ein: '98-7654321',};
const contractor = {
 name: 'John Doe',
 address: '1st Street',
 city: 'San Francisco',
 state: 'CA',
 zipCode: '55555',
 ssn: '123-45-6789',
 totalCompensation: '5,000.00',
‍};

4. Create a fieldMap object that maps the fields from your payer and contractor objects to the FieldName values for the PDF form.


const payerInfo = `${payer.name}\n${payer.address}\n${payer.city}, ${payer.state} ${payer.zipCode}`;const contractorCityStateZip = `${contractor.city}, ${contractor.state} ${contractor.zipCode}`;
const fieldMap = {
 'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_1[0]': payerInfo,
 'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_2[0]': payer.ein,
 'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_3[0]':
 contractor.ssn,
 'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_4[0]':
 contractor.name,
 'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_5[0]':
 contractor.address,
 'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_6[0]': contractorCityStateZip,
 'topmostSubform[0].CopyB[0].CopyBHeader[0].RghtCol[0].f2_8[0]':
 contractor.totalCompensation,
‍};

The 1099-NEC PDF has multiple pages with form fields, but in this example, we are just targeting form fields in the Copy B page. As I noted above, you can use the Prepare Form feature in Adobe Acrobat Reader DC to edit the FieldNames so they are more intuitive for development purposes.

5. Now that you have a fieldMap for the form, you just need to pass it to node-pdftk which will generate a new PDF with the corresponding form fields filled out.


const sourcePdf = 'forms/f1099nec.pdf';
return pdftk
 .input(sourcePdf)
 .fillForm(fieldMap)
 .flatten() /* Removes the form fields to lock the PDF from further edits */
 .output() /* Pass an optional file path to save the new PDF locally */
 .then(pdfBuffer => pdfBuffer)
 .catch(err => {
 throw err;
‍
 });

6. You can use a framework like Koa or Express to render the pdfBuffer in the browser as a PDF. Here is a streamlined example of what this looks like in Koa:


/* router.js */
import Router from 'koa-router';import generate1099Form from './generate1099Form';
const router = new Router();

router.get('/1099Form', async ctx => {
/* Can be further enhanced by loading payer and contractor from a database */

  const payer = {
    name: 'My Biz LLC',
    address: '2nd Street',
    city: 'San Francisco',
    state: 'CA',
    zipCode: '55555',
    ein: '98-7654321',
  };

  const contractor = {
    name: 'John Doe',
    address: '1st Street',
    city: 'San Francisco',
    state: 'CA',
    zipCode: '55555',
    ssn: '123-45-6789',
    totalCompensation: '5,000.00',
  };

  const pdfBuffer = await generate1099Form(payer, contractor);

  ctx.type = 'application/pdf';
  ctx.body = pdfBuffer;});
/* generate1099Form.js */
import pdftk from 'node-pdftk';
export default async (payer, contractor) => {
  const payerInfo = `${payer.name}\n${payer.address}\n${payer.city}, ${payer.state} ${payer.zipCode}`;
  const contractorCityStateZip = `${contractor.city}, ${contractor.state} ${contractor.zipCode}`;

  const fieldMap = {
    'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_1[0]': payerInfo,
    'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_2[0]': payer.ein,
    'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_3[0]':
      contractor.ssn,
    'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_4[0]':
      contractor.name,
    'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_5[0]':
      contractor.address,
    'topmostSubform[0].CopyB[0].CopyBHeader[0].LeftCol[0].f2_6[0]': contractorCityStateZip,
    'topmostSubform[0].CopyB[0].CopyBHeader[0].RghtCol[0].f2_8[0]':
      contractor.totalCompensation,
  };

  const sourcePdf = 'forms/f1099nec.pdf';

  return pdftk
    .input(sourcePdf)
    .fillForm(fieldMap)
    .flatten()
    .output()
    .then(pdfBuffer => pdfBuffer)
    .catch(err => {
      throw err;
    });
‍};

Congratulations! The final result is a pixel-perfect PDF form!

pixel-perfect PDF form