I was recently tasked with building out hundreds of pages of pdf based forms into online accessible “smart-forms” complete with text-replacement, expanding content and usable inputs (text, datepickers, pre-populated fields, selects, etc…).

At first, I started to manually build out each of the forms as html/css partials with AngularJS interpolation to do the text replacement. I used Angular directives to accomplish any advanced functionality my inputs needed. Building out the forms manually turned out to be a huge undertaking. Simple pages would take ~45 minutes to build, and more extreme forms would take over 3 hours per page. There had to be some other way.

While talking with another developer about a similar system he’d build, he mentioned absolutely positioning the inputs over a screenshot of the form pdf. Perfect! Well, almost. Using a static image wasn’t an option since I needed to do things like text-replacement and text color changes. However, using svgs would be the perfect solution. An svg would allow me to use Angular interpolation for text replacement, change text color using ng-class and build expanding sections by injecting html in-between the svg objects.

The first thing I needed to do was convert all of the pdf pages into svgs. I also needed to remove any explicit width and height attributes on the svg elements and replace them with a viewBox attribute. Thanks to inkscape, I was able to whip up a quick shell script to convert every page file into a corresponding svg:

#!/usr/bin/zsh

for file in *.pdf; do
   inkscape $file --export-plain-svg=$file.svg
   sed -i 's/width=".*"//g' $file.svg
   sed -i 's/height=".*"/viewBox="0 0 765.75 990.75"/g' $file.svg
done
rename -v 's/pdf\.svg/svg/' *.pdf.svg

While this isn’t the prettiest script in the world, it saved me a huge chunk of time.


After creating all of my svgs, I needed to go through and find where my text replacement and logic-based coloring needed to happen. I probably could have done this with sed, but I ended up doing it by hand fairly quickly.

Below is an example of how I modified the svg generated by inkscape, and the corresponding css example:

<text transform="matrix(1,0,0,-1,195.09,397.14)" id="text278">
    <tspan
    x="0"
    y="0"
    id="tspan280"
    ng-class="{'invalid': form.name.$invalid}"
    style="font-size:11.00016022px;font-variant:normal;font-weight:normal; ... ">
        Your name is {{data.name}}, and this is an example!
    </tspan>
</text>
.submitted .invalid {
    fill: #C00000;
}

The one downside to this technique is that there is no word wrapping support. If your interpolated string is too long, the line will simple run off the document. I played with using the foreignObject element to inject html into my svg, but it was not supported in IE.


For each form I wrote an html partial to pull in each page and to hold all of the inputs. Sublime Text snippets helped speed up the process of creating all of these partials:

<div class="page">
    <div class="svg-wrapper">
        <!-- page 1 inputs go here -->
        <div ng-include="'forms/svg/formPage1.svg'" class="svg-include"></div>
    </div>
</div>
<div class="page">
    <div class="svg-wrapper">
        <!-- page 2 inputs go here -->
        <div ng-include="'forms/svg/formPage2.svg'" class="svg-include"></div>
    </div>
</div>

The svg-wrapper class has a display: relative, while each of the inputs have display: absolute.


The next step in the process was to manually position each of the inputs. It didn’t take long to realize that absolutely positioning each element by hand would take a considerable amount of time (although it was still faster than the original solution). I decided to build a tool to help me out.

I hacked together a very simple draggable Angular directive that let me click and drag to define input boxes. After a few iterations I had a tool that reduced the time to lay out a complex page from hours to just minutes. Honestly, the tool’s code is some of the worst I’ve written in years, but I think that’s what makes it amazing. Haphazardly built, poorly functioning code saved me from billing hundreds of hours of work to my client and produced a better result in the end. I’d call that a success!

Here’s a quick screencast of the tool working on an example W-9 form: