There are tons of ways to generate PDF documents in just about any language. If you’re using Ruby, you have a number of libraries at your disposal to generate PDF documents using the specific API’s of these libraries. But if you have a Rails or other Ruby web application, the primary goal of your application is outputting HTML to be displayed in clients web browsers. So, wouldn’t it be nice to generate your PDF reports the same way, by outputting HTML?
If you’re running JRuby on Rails, this is a piece of cake! Using the great Flying Saucer Project (AKA xhtmlrenderer), you can convert your HTML + CSS to PDF documents. The great thing about Flying Saucer is that it will mostly render your HTML to PDF the same way standard compliant browsers will render it to screen. You have most of CSS 2.1 at your disposal, plus some custom CSS properties to handle print-specific matters such as paginated tables, and complex headers and footers. It has suprisingly few bugs, which really is an achievement when you’re talking about a HTML renderer. Did I mention it’s pretty fast, even for very large documents?
Of course, this is a Java library, so you need to be running JRuby to use it from your Rails application (or you need to resort to interfacing with it through the command line). First thing you need to do is installing the Flying Saucer gem, provided by the Wolfe project:
jruby -S gem install flying_saucer
This will automatically download the needed java library jars (flying saucer itself and iText) and put them in your gems folder.
Next, create a Rails controller you will use to output your PDF documents, for instance “ReportingController”. Let’s have it output a basic Hello World PDF.
The code for the controller (note the “render_pdf” method):
require "java" require "flying_saucer" class ReportingController < ApplicationController layout "pdf" def hello @text = "Hello world" render_pdf("hello.pdf") end private def render_pdf(filename) @html = render_to_string html_file = java.io.File.createTempFile(params[:action], ".html") html_file.delete_on_exit pdf_file = java.io.File.createTempFile(params[:action], ".pdf") pdf_file.delete_on_exit file_output_stream = java.io.FileOutputStream.new(html_file) file_output_stream.write(java.lang.String.new(@html).get_bytes("UTF-8")) file_output_stream.close renderer = org.xhtmlrenderer.pdf.ITextRenderer.new # if you put custom fonts in the lib/fonts folder under your Rails project, they will be available to your PDF document # Just specify the correct font-family via CSS and Flying Saucer will use the correct font. fonts_path = RAILS_ROOT + "/lib/fonts/*.ttf" if File.exist?(fonts_path) font_resolver = renderer.getFontResolver() Dir[fonts_path].each do |file| font_resolver.add_font(file, true) end end renderer.set_document(html_file) renderer.layout renderer.createPDF(java.io.FileOutputStream.new(pdf_file), true) send_file pdf_file.path, :type => "application/pdf", :filename => filename, :disposition => "attachment" end end
The main layout for our PDF documents:
<html> <head> <style type="text/css"> body { font-family: sans-serif; } </style> </head> <body> <h1>My PDF Wielding Application Title</h1> <%= yield %> </body> </html>
And the code to the view for the “hello” action:
Cliché message goes here: <strong><%=h @text %></strong>
Surfing to /reporting/hello will generate this PDF document.
So, the gist is: render all you want (as long as it’s properly escaped XHTML), call the render_pdf method and voila, instant PDF! Feel free to re-use your existing partials and helpers, it’s all good.
One remark: if you refer to resources like stylesheets and images, you must use an absolute URL or FlyingSaucer won’t be able to access them. For instance, use:
<link rel="stylesheet" type="text/css" href="http://app.example.com/stylesheets/reports.css" />
In stead of:
<link rel="stylesheet" type="text/css" href="/stylesheets/reports.css" />
Happy reporting!