FullStack Labs

Please Upgrade Your Browser.

Unfortunately, Internet Explorer is an outdated browser and we do not currently support it. To have the best browsing experience, please upgrade to Microsoft Edge, Google Chrome or Safari.
Upgrade

Generate Pixel Perfect PDF Forms with PDFtk and Node.js

Save time and money creating PDFs by using PDFtk and Node.js

Written by 
Michael Godshall
,
Senior Software Engineer
Generate Pixel Perfect PDF Forms with PDFtk and Node.js
blog post background
A Day in The Life of a Software Engineer at FullStack Labs
2020 Software Development Price Guide & Hourly Rate Comparison
How Company Culture Attracts Top IT Talent in Colombia

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.

-- CODE language-javascript keep-markup --
$ brew install pdftk-java

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

-- CODE language-javascript keep-markup --
$ 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.

-- CODE language-javascript keep-markup --
$ 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.

-- CODE language-javascript keep-markup --
$ npm install node-pdftk

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

-- CODE language-javascript keep-markup --
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.

-- CODE language-javascript keep-markup --
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.

-- CODE language-javascript keep-markup --
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.

-- CODE language-javascript keep-markup --
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:

-- CODE language-javascript keep-markup --
/* 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


Michael Godshall
Written by
Michael Godshall
Michael Godshall

As a Senior Software Engineer at FullStack Labs, I build world-class applications in Django, Python, and JavaScript frameworks like Ember and React. I love bringing ideas to life, and I have a wide range of experience that includes building robust applications for enterprise clients, managing projects and development teams, helping companies bring their products to market, and producing exceptional work in the midst of impossible deadlines.

FullStack Labs Icon

Let's Talk!

We’d love to learn more about your project. Contact us below for a free consultation with our CEO.
Projects start at $50,000.

company name
name
email
phone
Type of project
Reason for contact
How did you hear about us?
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.