Skip to main content

ChromicPDF: Generating PDF/A files with Chrome, Ghostscript, and Elixir

· 11 min read
Malte Rohde

Programmatically generating PDF documents is a common requirement in many of our client projects at bitcrowd. While in Ruby there is a multitude of battle-proven libraries to choose from, the PDF library landscape in the Elixir/Erlang ecosystem is just beginning to evolve. Today, we introduce ChromicPDF, a fast and convenient Chrome-based HTML-to-PDF converter, written in Elixir.

Rendering business documents as PDF files is a frequent requirement in commercial software applications. At bitcrowd, we have faced this problem numerous times in various shapes, and have deployed a number of PDF generating libraries with our applications - most notably in the context of Ruby projects, where the list of such libraries is quite diverse. Recently, we were again asked to build a PDF rendering component into one of our client projects, this time an Elixir application. In order to offer a consistent and familiar interface for UI developers, we set out to generate these PDF files from HTML templates (more on this at the end of the article).

Exploring Elixir PDF libraries

At the time, we evaluated existing PDF libraries in the Elixir ecosystem. A search for “pdf” on hex.pm yields two libraries with the majority of package downloads:

Both packages are relatively thin wrappers around browser-based libraries from a foreign ecosystem with similar capabilities and audiences. Except that pdf_generator lets one choose from two major browser engines: Chrome, with its relatively new Blink engine and its popular puppeteer API wrapper, and the slightly more dated Webkit engine on which wkhtmltopdf is based. Due to unsatisfactory previous experiences with wkhtmltopdf, we were leaning towards depending on Chrome, mostly for its subjectively better rendering quality.

PDF rendering in Chrome

Since the release of Chromeʼs headless mode in Chrome 59, programmatically generating PDFs from HTML is considered a primary use-case of Chrome. The puppeteer library is an OSS JavaScript library, developed primarily by Google engineers, which neatly encapsulates Chromeʼs built-in remote control interface, the “DevTools” protocol. This protocol offers access to all sorts of internal APIs of Chrome, among them the Page.printToPDF function that renders the currently displayed webpage to a PDF file.

Going with the cheapest option

While technically puppeteer could have been a convenient solution to our PDF rendering needs, sadly we were not allowed to deploy NodeJS at runtime, and hence had to rule out any puppeteer-based libraries. Sticking with Chrome, we decided to bypass puppeteer and instead control the browser directly. Chromeʼs command line --print-to-pdf option to the rescue, we were able to wrap Chrome ourselves and bundle it with the application.

We eventually settled for this solution, even though it comes with two significant drawbacks: First, it does not provide access to crucial options of the printToPDF function - most notably, the command line switch cannot utilize Chromeʼs support for native page headers and footers. While our initial use-case did not require these as we only needed to render 1-page documents, we could foresee that we might need to support multi-page documents in the future. Our hope is that the CSS Paged Media specification has found adoption in browsers by then. Second, spawning a new Chrome instance (OS process) for each PDF feels quite costly in terms of resource usage. Luckily, the project at hand was not expected to have a huge “PDF throughput”, so we could simply neglect this potential bottleneck. However, in other circumstances, the resource usage characteristics of this solution might become a showstopper issue.

Better solutions?

Theoretically now would be a good time to end this article. For our project we found a solution that was acceptable, albeit not exactly satisfactory, and that provided a HTML-to-PDF flow using the rendering engine that we deemed to produce the highest quality results. However, not being able to use the printToPDF API to its full potential kept me searching for alternative options. After playing around with Chromeʼs DevTools interface for a while and realizing that in fact it is based on a relatively trivial JSON:RPC protocol, I decided to build a simple DevTools API client for Elixir, with a focus on PDF generation as its primary use-case.

Enter ChromicPDF

ChromicPDF is a new Elixir library wrapping Chrome (or Chromium) to print PDF files from URLs or HTML snippets. In contrast to pdf_generator, it does not use puppeteer to communicate with the browser, but instead implements a client for (a tiny fraction of) the DevTools protocol. It is therefore “NodeJS-free” and offers the full set of options of the printToPDF command. Besides, it may be interesting for other reasons:

  • It should be faster. No benchmarks yet, and a bold claim to make without numbers, but considering the elimination of an additional library (puppeteer) and a whole interpreted language (JavaScript) from the runtime tech stack, I would estimate its “time-to-PDF” to be noticeably lower than any puppeteer-based solutions. Memory usage is also likely to be lower when running multiple print jobs in parallel.
  • In contrast to a command line --print-to-pdf approach, it keeps the browser process running indefinitely, and hence is a lot more frugal with OS resources.
  • Additionally, it spawns a (configurable) number of sessions (Chrome tabs) held in a session pool. This allows simultaneous PDF generations to happen in the same browser process, and again helps to reduce resource usage.

But wait, thatʼs not all! As a bonus, since business document PDFs are often required to be stored in a format suitable for long-term archival, ChromicPDF offers support for PDF/A (ISO 19005) as well. Simply replace the print_to_pdf function call with print_to_pdfa in the snippet below. Resulting PDF/A files pass the verapdf compliance checks. PDF/A support is based on Ghostscript.

For more information, please see the README and the documentation on hexdocs.

Getting started

Hereʼs an example of its API.

{:ok, data} =
ChromicPDF.print_to_pdf(
{:html, "<p>Hello World!</p>"},
print_to_pdf: %{
"marginTop": 0.787402,
"headerTemplate":
~s(<p>Page <span class="pageNumber"></span> of <span class="totalPages"></span></p>)
}
)

The response binary blob is the Base64-encoded PDF and can either be written to a file or send to a remote host. Streaming is supported by the DevTools protocol (see transferMode), but has not been implemented. The Base64 binary can also be inserted into a data:application/pdf;base64,<data> data URL, to be displayed in an iframe. The print_to_pdf map is passed on to Chrome unmodified, and hence has camel-cased field names, as well as margins and dimensions specified in inches.

To make this work, we need to start a Chrome instance. Weʼre going with the default configuration which will spawn 5 tabs (“targets”) from 1 browser process.

defmodule Demo.Application do
def start(_type, _args) do
children = [
[others...]
ChromicPDF
]
opts = [strategy: :one_for_one, name: Demo.Supervisor]
Supervisor.start_link(children, opts)

Building a template

The libraryʼs main API interface is a basic wrapper around the printToPDF function. However, it can be quite cumbersome at first to get the options and the page CSS aligned, so that headers and footers are rendered as intended and are not, for instance, hidden behind the body or have an illegible small font size (Nathan Friend wrote a good summary of the limitations of native header and footer support in Chrome in this blog post). Thus, the ChromicPDF.Template module provides a simplifying abstraction on top.

With it, we can build a PDF template module with ease. Letʼs assume we have a typical Phoenix web application and hence have Phoenix.View available.

# lib/demo/example_pdf.ex
defmodule Demo.ExamplePDF do
use Phoenix.View,
root: "lib/demo/templates",
namespace: Demo

def print_to_pdf(assigns) do
[
header_height: "20mm",
header: render("header.html"),
content: content(assigns)
]
|> ChromicPDF.Template.source_and_options()
|> ChromicPDF.print_to_pdf()
end

@styles """
<style>h1 { color: blue; }</style>
"""

defp content(assigns) do
ChromicPDF.Template.html_concat(@styles, render("content.html", assigns))
end
end
<!-- lib/demo/templates/example_pdf/header.html.eex -->
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
<!-- lib/demo/templates/example_pdf/content.html.eex -->
<h1><%= @bottles %> bottles of beer</h1>
[...]

We will also add a minimal UI to give users the option to preview the PDF in the browser.

# lib/demo_web/live/pdf_live.ex
defmodule DemoWeb.PDFLive do
use Phoenix.LiveView
use Phoenix.HTML

def render(assigns) do
~L"""
<%= f = form_for :bottles, "#", [phx_submit: :generate_pdf] %>
<%= label f, :number %>
<%= number_input f, :number, value: @number %>
<%= submit "Preview" %>
</form>
<iframe
src="data:application/pdf;base64,<%= @data %>"
style="width: 900px; height: 600px;"
></iframe>
"""
end

def mount(_params, _session, socket) do
{:ok, generate_pdf(socket, 99)}
end

def handle_event("generate_pdf", attrs, socket) do
{number, _} = Integer.parse(attrs["bottles"]["number"])
{:noreply, generate_pdf(socket, number)}
end

defp generate_pdf(socket, number) do
{:ok, data} = Demo.ExamplePDF.print_to_pdf(%{bottles: number})
socket
|> assign(:number, number)
|> assign(:data, data)
end
end

🎉

Generating PDFs from HTML: A praise

As promised, I am going to conclude this article with a few general remarks on the “HTML-to-PDF” method of rendering PDF files. Despite the universality of electronic screens, page-based & paper-sized PDF files are still a surprisingly common way of presenting information, either for actual print or for archival purposes. Hence, automatic generation of PDF files from dynamic data sources is a standard requirement in many software projects. Which technology to deploy to render these PDFs can be a difficult question, and answers vary a lot with different use-cases.

  • Content: What should be printed? Business documents (invoices, letters, etc.) have different needs than graphics or print media documents. Document layouts can be simple or complex.
  • Quality: How should it be printed? Quality of images may be a concern as well as file size, encryption, and other “non-functional” attributes.
  • Performance and scalability: How often do we need to print?

From the many libraries and applications that can generate a PDF file, one can perhaps identify the following three main categories:

  • Classic text processors: Most notably, LaTeX. But also print media applications like Adobeʼs InDesign. LaTeX can be a good choice for PDF generation when your layout needs are more complex (e.g. table of contents, footnotes, etc.).
  • PDF “interfaces”: “Canvas”-like libraries that allow to programmatically write documents based on coordinates and commands, for example, PDFLib or Rubyʼs Prawn. These libraries offer great flexibility in your designs, but in our experience often reduce maintainability of the PDF templateʼs code.
  • HTML-to-PDF: Browser engines that render a webpage to PDF.

While all of the above techniques have their place in our toolbox, the HTML-to-PDF approach has become more popular and appealing in recent years. PDFs rendered by Chrome may not quite reach the print quality offered by text processors, but their quality is certainly sufficient for business documents. Being a weak spot in the past, the browser enginesʼ support for print styles improved a lot in the last years (e.g., Chrome automatically re-renders table headers after a page break). Besides, support for enhanced print styles in CSS is on the horizon and itʼs only a question of time until browsers will have implement them.

The most convincing argument in favour of HTML-to-PDF: Designers and developers can stay in their familiar environment and language. They have all the concepts and languages needed for layouting and design already available. They have all necessary tools (CSS linters, etc.) already installed on their machines. They can make use of any front-end libraries, for instance a JS graph library, and the rendered content will seamlessly be embedded into the PDF and the resulting code easy to maintain. No need to come back to the rarely-touched LaTeX templates after a year and having to re-learn its syntax. New developers joining the project do not have to adapt a new language before they can make changes to document templates.

Obviously, there cannot be a catch-all solution for all PDF rendering needs, and some advanced page layouts will continue to be more costly to implement in HTML than it would be to set up a basic LaTeX template for them. Yet, HTML templates have a lot of soft aspects that might give them an advantage over other technologies in cases where both can satisfy the hard requirements.

Maybe ChromicPDF is a candidate for your next Elixir project?

Malte Rohde

Malte Rohde

Runtime optimized Keyboard Yodeler

We’re hiring

Work with our great team, apply for one of the open positions at bitcrowd