Generating PDFs in Background

By Morten Møller Riis

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 :)