Generating PDFs in Background
November 10 2011 15:00 CET
Would you like to be able to generate PDFs in your Rails app using delayed_job or similar? Then read on :)
I have used PDFKit for some time. But locking your Rails process whilest waiting for wkhtmltopdf to finish might not always be desireable.
In Rails 3.x it is very easy and clean to render controller actions outside of the controller (props to AmberBit for a nice tutorial). So you can do something like this:
app/controllers/invoices_controller.rb
# This is not a real controller for webpages.
# It is used to render HTML output for PDFKit.
# Use it like this:
# InvoicesController.new.show(<invoice id>)
class InvoicesController < AbstractController::Base
include AbstractController::Rendering
include AbstractController::Layouts
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
helper InvoicesHelper
self.view_paths = "app/views"
def show(id)
@invoice = Invoice.find(id)
render layout: "basic", template: "invoices/show"
end
end
I’ve included its respective helper. This is because I need a method that gives the full path for an image in my template. It is a known problem with PDFKit.
app/helpers/invoices_helper.rb
module InvoicesHelper
def pdf_image_tag(filename, options = {})
path = Rails.root.join("app/assets/images/#{filename}")
options[:src] = path.to_s
attributes = options.map{ |k,v| "#{k}='#{v}'" }.join(" ")
raw("<img #{attributes} />")
end
end
When this is setup you can easily create a delayed_job that can render the action like this:
app/jobs/render_pdf_invoice.rb
require 'pdfkit'
class RenderPDFInvoice < Struct.new(:id)
def perform
begin
html = InvoicesController.new.show(id)
kit = PDFKit.new(html, page_size: "A4")
kit.to_file("#{Rails.root}/invoices/#{id}.pdf")
rescue
puts "could not find that"
end
end
end
Now, to render PDFs in the background I just:
Delayed::Job.enqueue RenderPDFInvoice.new(1337)
And when that’s done a nice PDF will be at invoices/1337.pdf :)