HTML / CSS to PDF using Ruby on Rails

Ever tried to save a web page and send it to someone? What about printing a web page? Both pretty much suck.

A lot of people are talking about printing with HTML and CSS these days but what they don’t tell you is the following:

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.

Wow, that’s lame.

Ever tried sending someone a web page? Not a link, but the actual web page. It doesn’t work well at all.

A little history

I wanted to be able to print or send estimates and invoices from our Ruby on Rails application, Cashboard. A good majority of our users felt the same way.

Just sending a link to someone to view a HTML document online doesn’t cut it. What if you want to snail mail that document? Shouldn’t it look the same on the page as it does on your screen?

When I used to create invoices manually I’d always save PDF files for my clients and email them.

PDF handled printing well, was portable, and my clients couldn’t edit the file.

We needed PDF for Cashboard.

The Prince of PDFs

I spent a good week just researching all of the PDF libraries and programs out there.

The ones I found for Ruby wanted me to re-create my documents in some PDF-specific layout language. That wasn’t going to cut it. See, in Cashboard we already had these documents designed in HTML. On top of that, we allow users to customize the colors and upload their logo for invoices. Duplicating the layout wasn’t an option.

After exhausting those possibilities I started looking into HTML to PDF programs. I found a few, but none that were any good at converting HTML and CSS to PDF.

This all changed when I ran across Prince XML. Not only is Prince great at converting HTML and CSS to PDF, it even passed the Acid2 test. They had a ton of demos available on their web site, and provide their program free for evaluation purposes.

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

Does it get any easier than that? Check out an example of a PDF file generated with Prince, straight from Cashboard. (Right click and save to your disk…)

Download

Here again are the libraries I’ve created for using Prince XML with Ruby on Rails:

They’re free to use, download em and check em out.

seth, Tue, 29 May 2007 21:37:00 GMT
10 comments
Comments

Leave a comment

  1. Zack about 21 hours later:

    Nice write-up.

    Prince looks great but I can’t stomach the price of a server license! $3800 – ouch!

    Zack

  2. Kevin Skoglund about 23 hours later:

    Thanks! This is great and will be very useful. PDF is one of those things I had been putting off.

    In case it is helpful for anyone else, I wrote up the installation steps and how I needed to modify it for use. http://www.nullislove.com/2007/05/30/easy-pdfs-from-rails/

  3. Mark Friedgan about 24 hours later:

    You can use a call to DOMPDF in PHP to do this or you can use HTML2PS -> PS2PDF. Both free. Only problem is they don’t scale well.

  4. Seth 1 day later:

    Mark,

    I checked out DOMPDF and HTML2PS. Neither supported CSS properties properly and the rendering of my documents was screwed up.

    It was essential that the rendering be perfect in PDF as we might possibly allow customers to modify our estimate and invoice templates via liquid in the future.

  5. Ovidiu C. 1 day later:

    Another option would be XML + XSL-FO + FOP. It’s free, but it needs extra work (you can’t just feed it HTML and CSS).

  6. TarquinWJ 1 day later:

    While I would disagree about saving of web pages (saving complete or web archive works very well in Opera which can then be opened in any browser – IE screws up CSS when saving so no other browser understands it, and Firefox screws up dynamic generated content – ok, so that sucks for most Web users), I currently make several of my pages Prince-compatible so my readers can make printed copies with nice formatting. (The cost is far too high for my own use, but my readers can use their own copies – they can even use the evaluation version at their own discretion.)

    The printed pages also work with browsers, and they would also work with those DOMPDF and HTML2PS (XSL is simply not an option).

    However, Prince really is by far the best due to its outstanding support for CSS. I use it for making automated table of contents, replicating the <a href="#foo"> links that you would use on a web page, but normally cannot work when printed – Prince turns them into a table of contents with page numbers.

    It also allows control of generated content inside elements and even page margins. Basically it is the only HTML+CSS->PDF/print tool that supports enough CSS to make my online book actually appear like the real printed book looked.

    Nice to see that someone else realises the value of Prince’s excellent CSS support. Thanks for sharing the Ruby libs.

  7. Seth 2 days later:

    RE: Prince’s CSS support…

    Another thing that kicks ass about Prince’s CSS support, which I’m not sure I made clear is that you can choose the paper size as well. I print a few things in landscape mode from Cashboard and it works great.

    The other biggie is you can do page headers and footers, or even page numbers. It gives your documents that much more of a professional touch.

  8. David L 2 days later:

    actually, you can set a page break in CSS

    hr style=”page-break-before: always;”

  9. David L 2 days later:

    but hey, cool work with ruby! :)

  10. Mark James 7 days later:

    Thank you David L for the page-break CSS tip. This works well printing multiple pages.

    It even works with multiple footers by setting the body height close to 100%, and wrapping each page in a container div having style “position: relative; min-height: 100%”

Comments