Converting HTML to PDF using Rails

posted by Ayush Newatia
3 June, 2021



Exporting to PDF from HTML can be a bit of a can of worms, especially with CSS not quite working the way it does in a web browser. However with the right setup, it’s possible to take the pain out of it!

A couple of popular gems to convert HTML to PDF in Rails are PDFKit and WickedPDF. They both use a command line utility called wkhtmltopdf under the hood; which uses WebKit to render a PDF from HTML.

I’d highly advise against using both those gems. They’re good libraries but the underlying wkhtmltopdf doesn’t support modern CSS features such as custom properties or grid; so you might find yourself unable to use any of the existing CSS in your app to style your PDF export.

The gem I recommend is called Grover. It uses Puppeteer and Chromium to “print” an HTML page into a PDF. So your PDF will look exactly how your page looks in Google Chrome’s print preview. This will also allow you to reuse CSS from your app rather than having to write specific CSS just for your PDF exports.

Since Grover uses Chromium which runs external of your Rails app, you need to reference all your assets with absolute paths instead of relative paths. The easiest way to enable this is to set config.asset_host in your app configuration. This ensures the stylesheet_link_tag and font-url helpers output the absolute path including your domain name rather than just the relative path.

Depending on the complexity of your requirements, you might want to set up Grover as a middleware. You can read up on how to do that in their comprehensive Readme. However if all you’re trying to do is allow a user to download a dynamically generated PDF, the below controller code is all that’s needed!

def render_pdf(html, filename:)
  pdf = Grover.new(html, format: 'A4').to_pdf
  send_data pdf, filename: filename, type: "application/pdf"
end

I put this method in a Concern so I can include it in any controller I need to. I’d also recommend creating a new layout for your PDFs as you likely won’t need all the markup your application.html.erb includes.

Here’s an example of a controller action that generates and triggers a download of a PDF for an invoice:

def download
  invoice = render_to_string "download.html.erb", layout: "pdf"

  respond_to do |format|
    format.html { render html: invoice }
    format.pdf { render_pdf invoice, filename: t(".filename", id: @invoice.id) }
  end
end

Having both HTML and PDF formats for this action enables a quick feedback loop during development. It allows you to view the HTML page in your browser and then test the PDF export once you have the basics down.

Exporting to PDF doesn’t have to be a pain thanks to Grover! You might still find some quirks with your CSS so you might have to create a separate CSS bundle for PDFs; however the vast majority of your CSS should “just work”.