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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;s required.</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">TestPdfForm</span> <span class="o">&lt;</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">&lt;</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&rsquo;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&rsquo;t want this, simply mark the fields in your template as read-only. So if your template isn&rsquo;t a &ldquo;form&rdquo; 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&rsquo;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.&nbsp;<a href="#fnref1">&#8617;</a></p> </li> <li id="fn2"> <p>There are many lower-cost and free alternatives to Adobe Acrobat, but I don&rsquo;t have any experience with them, so I won&rsquo;t make any recommendations.&nbsp;<a href="#fnref2">&#8617;</a></p> </li> <li id="fn3"> <p>If Acrobat shows you an error message that something like &ldquo;This form cannot be edited in Acrobat. Please use Adobe LiveCycle Designer to edit this form&rdquo;, then you&rsquo;ll either need to either get a version of the PDF that doesn&rsquo;t have these permissions turned on or you&rsquo;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.&nbsp;<a href="#fnref3">&#8617;</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.&nbsp;<a href="#fnref4">&#8617;</a></p> </li> </ol> </div>