PDF generation with Clojure, thanks to Dmitri Sotnikov

Jose Ayudarte Blocked Unblock Follow Following Dec 19, 2017

Unlike consumer software, Reporting is something that enterprise software always needs, so a month ago our team did a benchmark analysis to choose the best PDF generator library or third-party tool. As Clojure runs on the JVM and has smooth Java interop we considered tools like JasperReports to create reports with drag-and-drop functionality but we weren’t convinced: the design tool is user-friendly in the sense of having the option to create PDF sections visually, but then we realized that we had complex requirements, making it more difficult to achieve the results using JasperReports without losing ability to have dynamic data structures.

So we took our exploration further afield.

Eventually, we decided on clj-pdf, a library created by Dmitri Sotnikov, which shares similarities with Hiccup library at the time of generating elements. Now that we have developed and released our PDF report tool to production I will try to summarize the benefits and share some tips on how to use clj-pdf:

I think it’s important to highlight that the most useful document element for structuring advance content is the pdf-table. It’s the only way to properly customize space and alignment between elements. Table could be also used but it has bugs and the display is broken on some advanced use-cases.

As for creating a header for the PDF, you might want to add a logo and some title aligned horizontally. In our case, as you can see in the code, we have created a pdf-table element that is 100% width and it’s right-aligned, with no borders. We have also positioned the header statically within “x” and “y” axes in the page. Then we have used a watermark element to set and scale the logo just in the right place. This method has the caveat of having to customize header position for each different case. So if you have several documents with different text-lengths in the header, you have to adjust the position by hand for each of one.

So a better method to create a header it might be not setting its “x” and “y” axes at all, but just creating a 2-column pdf-table with pdf-cell inside that has paragraphs aligned to the desired place (right or left). Thus you could use any length for the header text but with the limitations that you should use some negative paddings for pdf-cells (needed to align the items horizontally) as well as getting more vertical top-margin in the page.

Another interesting thing is to create simple tables with few lines of data:

For instance, we have used a pdf-table element with a chunk element inside, separating data lines with “

” end of line character, creating a background for the whole table.

Also, as a way to control jump or vertical gaps between elements, spacer element is good combined with size property that gives us more fined control, as the size specifies the font-size of the space inserted.

Finally, another interesting trick is the use of negative padding values to have a better control over vertical gaps. As pointed before, left, right, top and bottom padding properties are available to use within pdf-tables’s pdf-cell elements. By way of illustration, to move a pdf-cell’s content up you would set a property padding-top to -3 value or something like that. Numbers are pixel units but it’s better just to give it a go and experiment a bit.

Important issues:

We have had huge gaps with very long pdf-table columns. So we have created faux rows to split the content to avoid that. That shouldn’t be a problem and we have to report an issue in the author’s repository. In fact there is a property to control behavior of pdf-table’s content across different pages.

Pdf-table column number is strict and immutable. We have to use colspan attribute to merge columns for a given row but we cannot pass less or more columns than expected to the pdf-table’s body and that includes Clojure’s nil values as well (they are counted as a column even if there is no content inside). We remove nil values or we skip structure creation completely in case we find the risk of breaking that rule. Also we create rows with nil columns if required when there is no content.

As my colleague Asier mentioned in an earlier post, our goal is to contribute more to Open Source projects and this awesome library can be a good candidate to start doing so.