Pre-Filling PDF Form Templates in Ruby-on-Rails with PDFtk
<p>In <a href="/2014/01/14/generate-clean-testable-pdf-reports-in-rails-with-prawn">a recent post</a>, I talked about how to generate PDF reports in Rails using Prawn. This approach is great for generating PDF’s with lots of data tables and other variable-length content. But an alternative situation is when you already have a template authored in an application such as Adobe Acrobat and you want to populate it with data from your database. This makes it more difficult to insert variable-length content, but on the plus side, you no longer need to worry about the layout of the document.</p> <p></p> <p>While there are a number of libraries out there that can perform PDF manipulation, the one I have found to work best is <a href="http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/">PDFtk</a>. There is a GUI version, but you’ll want the <a href="http://www.pdflabs.com/tools/pdftk-server/">command line version</a>, which is available in most package managers. <sup id="fnref1"><a href="#fn1">1</a></sup> This library is free for personal use, but requires a license if used in production.</p> <p>PDFtk is a non-ruby command line tool and while it works great on its own in that context, it will be much easier if we use a ruby wrapper, so go ahead and add the <a href="https://github.com/jkraemer/pdf-forms">pdf-forms</a> gem to your project.</p> <p>For this example, I’ve provided a really simple PDF form that I created in Adobe Acrobat <sup id="fnref2"><a href="#fn2">2</a></sup>. You can <a href="/assets/other/test_form.pdf">download it here</a> and add it to your project (I put mine in <code>lib/pdf_templates</code>). There are plenty of other resources out there on creating PDF forms, so I won’t go over that, but make sure you take note of the names of the fields. If you are starting with a template created by someone else, you’ll still want to open it up in Adobe Acrobat (or a similar app) to reference the names of the form fields. <sup id="fnref3"><a href="#fn3">3</a></sup>. The pdf-forms gem has a command for viewing the names, but I found that it didn’t always work.</p> <p><img src="/assets/images/adobe_acrobat_pdf_form-befc1ead.png" alt="Create your PDF form using Adobe Acrobat" /></p> <p>Next, let’s get to the coding. Just as in my post on generating pdfs with Prawn, I like to represent each pdf document as a ruby class. But first let’s create a base class that takes care of the common functionality. So create a file called fillable<u>pdf</u>form.rb. In my rails app, I placed it in <code>app/pdfs</code>. <sup id="fnref4"><a href="#fn4">4</a></sup>. Here’s how my base class turned out:</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">FillablePdfForm</span> <span class="nb">attr_writer</span> <span class="ss">:template_path</span> <span class="nb">attr_reader</span> <span class="ss">:attributes</span> <span class="k">def</span> <span class="nf">initialize</span> <span class="n">fill_out</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">export</span><span class="p">(</span><span class="n">output_file_path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span> <span class="n">output_path</span> <span class="o">=</span> <span class="n">output_file_path</span> <span class="o">||</span> <span class="s2">"</span><span class="si">#{</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="si">}</span><span class="s2">/tmp/pdfs/</span><span class="si">#{</span><span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid</span><span class="si">}</span><span class="s2">.pdf"</span> <span class="c1"># make sure tmp/pdfs exists</span> <span class="n">pdftk</span><span class="p">.</span><span class="nf">fill_form</span> <span class="n">template_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">,</span> <span class="n">attributes</span> <span class="n">output_path</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">get_field_names</span> <span class="n">pdftk</span><span class="p">.</span><span class="nf">get_field_names</span> <span class="n">template_path</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">template_path</span> <span class="vi">@template_path</span> <span class="o">||=</span> <span class="s2">"</span><span class="si">#{</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="si">}</span><span class="s2">/lib/pdf_templates/</span><span class="si">#{</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">name</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="s1">'Pdf'</span><span class="p">,</span> <span class="s1">''</span><span class="p">).</span><span class="nf">underscore</span><span class="si">}</span><span class="s2">.pdf"</span> <span class="c1"># makes assumption about template file path unless otherwise specified</span> <span class="k">end</span> <span class="kp">protected</span> <span class="k">def</span> <span class="nf">attributes</span> <span class="vi">@attributes</span> <span class="o">||=</span> <span class="p">{}</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">fill</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="n">attributes</span><span class="p">[</span><span class="n">key</span><span class="p">.</span><span class="nf">to_s</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">pdftk</span> <span class="vi">@pdftk</span> <span class="o">||=</span> <span class="no">PdfForms</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'PDFTK_PATH'</span><span class="p">]</span> <span class="o">||</span> <span class="s1">'/usr/local/bin/pdftk'</span><span class="p">)</span> <span class="c1"># On my Mac, the location of pdftk was different than on my linux server.</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">fill_out</span> <span class="k">raise</span> <span class="s1">'Must be overridden by child class'</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>And now I can keep my actual test pdf form class short and sweet. The <code>fill_out</code> method is all that’s required.</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">TestPdfForm</span> <span class="o"><</span> <span class="no">FillablePdfForm</span> <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="vi">@user</span> <span class="o">=</span> <span class="n">user</span> <span class="k">super</span><span class="p">()</span> <span class="k">end</span> <span class="kp">protected</span> <span class="k">def</span> <span class="nf">fill_out</span> <span class="n">fill</span> <span class="ss">:date</span><span class="p">,</span> <span class="no">Date</span><span class="p">.</span><span class="nf">today</span><span class="p">.</span><span class="nf">to_s</span> <span class="p">[</span><span class="ss">:first_name</span><span class="p">,</span> <span class="ss">:last_name</span><span class="p">,</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">:address_2</span><span class="p">,</span> <span class="ss">:city</span><span class="p">,</span> <span class="ss">:state</span><span class="p">,</span> <span class="ss">:zip_code</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">field</span><span class="o">|</span> <span class="n">fill</span> <span class="n">field</span><span class="p">,</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="n">field</span><span class="p">)</span> <span class="k">end</span> <span class="n">fill</span> <span class="ss">:age</span><span class="p">,</span> <span class="k">case</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">age</span> <span class="k">when</span> <span class="kp">nil</span> <span class="k">then</span> <span class="kp">nil</span> <span class="k">when</span> <span class="mi">0</span><span class="o">..</span><span class="mi">17</span> <span class="k">then</span> <span class="s1">'0_17'</span> <span class="k">when</span> <span class="mi">18</span><span class="o">..</span><span class="mi">34</span> <span class="k">then</span> <span class="s1">'18_34'</span> <span class="k">when</span> <span class="mi">35</span><span class="o">..</span><span class="mi">54</span> <span class="k">then</span> <span class="s1">'35_54'</span> <span class="k">else</span> <span class="s1">'55_plus'</span> <span class="k">end</span> <span class="n">fill</span> <span class="ss">:comments</span><span class="p">,</span> <span class="s2">"Hello, World"</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>And my controller might look something like this:</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span> <span class="k">def</span> <span class="nf">show</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span> <span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span> <span class="nb">format</span><span class="p">.</span><span class="nf">pdf</span> <span class="p">{</span> <span class="n">send_file</span> <span class="no">TestPdfForm</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user</span><span class="p">).</span><span class="nf">export</span><span class="p">,</span> <span class="ss">type: </span><span class="s1">'application/pdf'</span> <span class="p">}</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>And that’s all you need! The generated form will be both readable and writable in Adobe Reader and Mac Preview. So if you need the user to fill out a few additional fields, they can do so. But if you don’t want this, simply mark the fields in your template as read-only. So if your template isn’t a “form” but you just want to merge some data into a document, this is still a great way to do it.</p> <p>From there, creating additional forms is extremely easy. The only difficulty I ever encounter is in figuring out the proper field names for templates I didn’t create, but this is still fairly trivial.</p> <p>If you have any questions, send them to me on <a href="http://twitter.com/adam_albrecht">Twitter</a>.</p> <div class="footnotes"> <hr> <ol> <li id="fn1"> <p>While PDFtk is free for personal use, it does require a license in production. <a href="#fnref1">↩</a></p> </li> <li id="fn2"> <p>There are many lower-cost and free alternatives to Adobe Acrobat, but I don’t have any experience with them, so I won’t make any recommendations. <a href="#fnref2">↩</a></p> </li> <li id="fn3"> <p>If Acrobat shows you an error message that something like “This form cannot be edited in Acrobat. Please use Adobe LiveCycle Designer to edit this form”, then you’ll either need to either get a version of the PDF that doesn’t have these permissions turned on or you’ll need to export it the document as a new pdf, and then re-import it into Acrobat and lay out the form fields yourself. <a href="#fnref3">↩</a></p> </li> <li id="fn4"> <p>You may need to restart your dev server to make sure it picks up changes in this new folder. <a href="#fnref4">↩</a></p> </li> </ol> </div>