PDF generation and Heroku
For 請求書.jp, we wanted to provide our users with a way to generate PDF versions of invoices. We used the PDFKit gem (https://github.com/jdpace/PDFKit), which by itself is a thin wrapper around wkhtmltopdf (http://code.google.com/p/wkhtmltopdf/), to easily generate PDFs. With this tool chain it is possible to generate PDFs for any page, using the HTML and CSS you already have in place.
Installation
Installation is quite simple:
- add PDFKit to your Gemfile, install the wkhtmltopdf-binary gem
- add the following to your config/application.rb (Rails 3) to hook PDFKit into Rack:
config.middleware.use PDFKit::Middleware
- add an initializer (config/initializers/pdfkit.rb) like this:
PDFKit.configure do |config| config.default_options = { page_size: 'A4', print_media_type: true } end
Basically that’s it, but please see https://github.com/jdpace/PDFKit for more details. Anyway, pretty easy, no?
Getting it to work on Heroku
Well, the tricky part starts when you want to deploy this to Heroku.
First, you’ll need to include a statically linked 64bit Linux version of wkhtmltopdf in your application. You can download it from http://code.google.com/p/wkhtmltopdf/downloads/list (wkhtmltopdf-0.10.0_rc2-static-amd64.tar.bz2 for instance). To use the same binaries in development and production, we removed the wkhtmltopdf-binary gem again, downloaded the binaries for Linux and Mac so all our development environments are covered, and extended config/initializers/pdfkit.rb like this:
PDFKit.configure do |config|
config.default_options = { page_size: 'A4', print_media_type: true }
if RUBY_PLATFORM =~ /linux/
wkhtmltopdf_executable = 'wkhtmltopdf-amd64'
elsif RUBY_PLATFORM =~ /darwin/
wkhtmltopdf_executable = 'wkhtmltopdf-osx'
else
raise "Unsupported. Must be running linux or intel-based Mac OS."
end
config.wkhtmltopdf = Rails.root.join('vendor', 'bin', wkhtmltopdf_executable).to_s
end
After deploying to Heroku, the second issue you are likely to notice is the following: Requesting a PDF page just sits there for a while and then returns an application error. That is because you are most likely using external stylesheets and wkhtmltopdf will request them in order to generate the PDF. But your Dyno is currently busy with generating the PDF, so it can’t respond to the incoming request regarding the stylesheets.
Unfortunately starting a second Dyno or using unicorn on the cedar stack doesn’t really help here, because Heroku’s router doesn’t know which Dynos are busy but rather distributes the workload according to different parameters. So basically doing a request from within your application to your application won’t work reliably. Putting a cache / CDN (like AWS’ cloudfront) in front of your assets mitigates the problem, but users might still see an application error because the cache / CDN needs to request those files from time to time. Serving all your assets altogether from S3 works of course, but makes deployment to Heroku harder.
The solution we employed in the end was to avoid the additional requests entirely and instead embed the printing styles into the HTML itself, using the following code in the layout (HAML):
!!!
%html{:lang => "ja"}
%head
%title= 請求書.jp
- if request_from_pdfkit?
%style{type: "text/css"}
= File.read(Rails.root.join("public","stylesheets","print.css"))
- else
= javascript_include_tag 'application'
= stylesheet_link_tag ‘application’
And in the application_helper.rb:
def request_from_pdfkit?
# when generating a PDF, PDFKit::Middleware will set this flag
request.env["Rack-Middleware-PDFKit"] == "true"
end
The downside of this solution is though that we need to have a static print.css in the public/stylesheets directory and can’t use Rails 3.1’s asset pipeline as before.
The third challenge we had to address was caused by using non-Latin script, Japanese in our case. To have Japanese script in the PDF, you need Japanese fonts installed on the server doing the PDF generation, so it can embed the font. There are no Japanese fonts installed on Heroku though. Fortunately we could come up with a way to install the required font within the Heroku environment. For that to work, we added the font to vendor/fonts in the Rails project and added the following initializer:
if Rails.env.production?
font_dir = File.join(Dir.home, ".fonts")
Dir.mkdir(font_dir) unless Dir.exists?(font_dir)
Dir.glob(Rails.root.join("vendor", "fonts", "*")).each do |font|
target = File.join(font_dir, File.basename(font))
File.symlink(font, target) unless File.exists?(target)
end
end
With this setup we can now reliably generate nice looking PDFs for our users.
Trackbacks
Use the following link to trackback from your own site:
http://www.mobalean.com/blog/trackbacks?article_id=41


Social Links