Creating PDFs in a Ruby on Rails application
Tags: ruby, rails, webdev
Published: 2021-10-26 14:25:39

Working with PDFs in Ruby on Rails can be a daunting task - inflexibility is an inherent part of PDF documents.

As such the best option is to avoid them if possible. Does you invoice really need to be a PDF? Most of the time it can just be an HTML page that the user can save as PDF via their browser. However, it is not always possible to complete ignore PDFs - f.ex. if the invoice needs to be send as an attachment to an e-mail.

You have a few options when trying to create a PDF in a Rails environment. [Prawn](https://github.com/prawnpdf/prawn) and [Wicked PDF](https://github.com/mileszs/wicked_pdf) have been around for quite a while. I have been using both gems and they work fine. However, they have a few limitations that can make it difficult to handle more complex PDFs. I recently discovered [Grover](https://github.com/Studiosity/grover), which can remediate some of this inflexibility in creating PDFs.

## Prawn

[Prawn](https://github.com/prawnpdf/prawn) is a *pure Ruby PDF generation library that provides a lot of great functionality while trying to remain simple and reasonably performant*. It is quick to setup and create simple PDFs:

```ruby
  require "prawn"

  Prawn::Document.generate("hello.pdf") do
    text "Hello World!"
  end
```
  
Where Prawn gets more complicated is when trying to create complex PDFs. It uses its own DSL you need to learn in order to "draw" your PDFs by hand. While the [manual](https://prawnpdf.org/manual.pdf) is very helpful in order to figure our how to add images, tables and styling, you will end up with a lot of custom code to create a more complex PDF.

There are gems to facilitate handling Prawn PDFs, e.g. [prawn-markup](https://github.com/puzzle/prawn-markup) which lets you add simple HTML snippets into Prawn-generated PDF.

## Wicked PDF

[Wicked PDF](https://github.com/mileszs/wicked_pdf) allows you to *serve a PDF file to a user from HTML*. As such, you can include your custom CSS and Javascript via the helpers provided by Wicked.

Wicked is a great gem to create PDFs with more complex layouts and styling, without the need to apply a custom DSL. There are quite a few tutorials out there to help you when you are stuck on a specific problem.

One problem I regularly came across with Wicked was that getting everything running in production proved to be more challenging than expected. Especially the custom CSS (with asset pipeline) and images proved to be a problem.

## Grover

In my effort to find a replacement for Prawn or Wicked PDF, I recently came across [Grover](https://github.com/Studiosity/grover). This gem allows you to transform HTML into PDFs, PNGs or JPEGs using Google Puppeteer and Chromium. It basically imitates the action of a user saving an HTML page as PDF in the Chrome browser.

While there are fewer tutorials out there on how to get everything up-and-running, the great flexibility it gives me to just use my existing views and print them as PDFs makes this my current favourite for creating PDFs.

In fact you can save any existing view as PDF, allowing you to reuse your current HTML and/or CSS framework. You can use [Tailwind](https://tailwindcss.com/) for your CSS or [ViewComponent](https://viewcomponent.org/) to display your components and print them to PDF.

However I had a bit of trouble getting Grover to generate PDFs on our Heroku production environment. I finally managed to find a solution in the issue tracker and comments.

For my setup, it was important to use `Grover::HTMLPreprocessor` as well as include the `display_url` (make sure to change that in production). You also need to include the correct [buildpacks](https://github.com/Studiosity/grover#running-on-heroku) mentioned in the readme.

An example application could include this setup - rendering a view from the controller, including a custom PDF layout and saving it as an A4 PDF with Grover:

```ruby
renderer = ApplicationController.renderer.new(
    http_host: "http://localhost:3000/"
  )

html = renderer.render_to_string(
  "invoice/show", # Any view within your app
  layout: "pdf",  # You can add a custom layout as well
  locals: { :@invoice => invoice }
)

html = Grover::HTMLPreprocessor.process html, base_url, "https"

Grover.new(html, {
  format: "A4", # Many other options are available
  emulate_media: "screen",
  display_url: "http://localhost:3000/",
  scale: 0.9    # This helped me to get the PDF displayed correctly on A4 format
}).to_pdf
```

The great advantage of this solution is that whenever I update a component or a layout style, my PDFs will reflect these changes, without the need to modify any PDF-specific resources.