HTML / CSS to PDF Using Ruby on Rials

6:27:00 PM | ,

With CSS you can’t…
  • Determine where page breaks happen.
  • Set page size or type (landscape or portrait).
  • Print background colors…some browsers don’t even print images.
  • Set page footers.

Making it happen

Prince is a command line program, available for whatever platform you’re probably running.
It can take a variety of inputs including files on disk, or even HTTP urls. We run a pretty tight ship on Cashboard, and everything is password protected.
Luckilly Prince also can take input from standard in, and can even pass its output back to standard out. This means if you don’t want to mess with saving files and dealing with cleaning them up you don’t have to.

prince.rb, pdf_helper.rb

I cooked up a very simple Ruby library to call Prince and a helper module to include on my Rails controllers. I store them both in the lib folder of my Rails application.
Here’s the full helper, slightly modified for simplicity:
# We use this chunk of controller code all over to generate PDF files.
#
# To stay DRY we placed it here instead of repeating it all over the place.
#
module PdfHelper
  require 'prince'

  private
    # Makes a pdf, returns it as data...
    def make_pdf(template_path, pdf_name, landscape=false)
      prince = Prince.new()
      # Sets style sheets on PDF renderer.
      prince.add_style_sheets(
        "#{RAILS_ROOT}/public/stylesheets/application.css",
        "#{RAILS_ROOT}/public/stylesheets/print.css",
        "#{RAILS_ROOT}/public/stylesheets/prince.css"
      )
      prince.add_style_sheets("#{RAILS_ROOT}/public/stylesheets/prince_landscape.css") if landscape
      # Render the estimate to a big html string.
      # Set RAILS_ASSET_ID to blank string or rails appends some time after
      # to prevent file caching, fucking up local - disk requests.
      ENV["RAILS_ASSET_ID"] = ''
      html_string = render_to_string(:template => template_path, :layout => 'document')
      # Make all paths relative, on disk paths...
      html_string.gsub!("src=\"", "src=\"#{RAILS_ROOT}/public")
      # Send the generated PDF file from our html string.
      return prince.pdf_from_string(html_string)
    end

    # Makes and sends a pdf to the browser
    #
    def make_and_send_pdf(template_path, pdf_name, landscape=false)
      send_data(
        make_pdf(template_path, pdf_name, landscape),
        :filename => pdf_name,
        :type => 'application/pdf'
      ) 
    end
end
This simple module has two methods. Both take an ERB template path as an argument, then a pdf file name.
Make_pdf renders the template to a string, then does some modifications to make all requests within local. I’m passing in all CSS files locally as well, so I don’t have to deal with authentication.
I’ve created some special CSS files for printing, and even can pass in if I’d like the page to be laid out in a portrait or landscape format.
When it’s done it returns the PDF file as data. Nothing is rendered to disk. This method is useful for not only sending the PDF file to the client (as in make_and_send_pdf), but when generating PDF files for email attachments.

Creating PDF files from the controller

Both of these files make creating a PDF file dead easy.
Here’s a slimmed down version of Cashboard’s estimate controller.
class Provider::EstimatesController < Provider::BaseController
  include PdfHelper
    # Sends pdf of an estimate out...
    #
    def pdf
      # @estimate is set with a before_filter and isn't relevant for this how-to ;)
      make_and_send_pdf('/client/estimates/show', @estimate.pdf_name)
    end
end