ChromicPDF: Generating PDF/A files with Chrome and Elixir

Announcing 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 librariesLink to section

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 ChromeLink to section

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 optionLink to section

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?Link to section

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 ChromicPDFLink to section

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 is fast. In contrast to the other libraries we looked at, it spawns the browser process once and keeps it in the background to be readily available when printing. This makes ChromicPDF’s “time-to-PDF” noticeably lower than both of the evaluated puppeteer-based solutions which create a new browser instance for every PDF. Besides, when printing from a HTML source (instead of an URL), it uses the setDocumentContent function to replace the page DOM which is considerably faster than navigating to a new page.
  • Keeping the browser process running also means it is a lot more frugal with OS resources. Even though peak memory usage shouldn’t differ much, frequent starting and stopping of external processes can be quite expensive.
  • 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 should help 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 startedLink to section

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></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 templateLink to section

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.

$ cat 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

With zero styling, our rendered PDF isn’t likely to win any prizes, but it shines in the little amount of code that was required to build it.

Inline PDF previews with iframes
Inline PDF previews with iframes • Own Work Bitcrowd

Generating PDFs from HTML: A praiseLink to section

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?

© 2020 bitcrowd GmbH.