How to Generate PDFs with HTML templates in 10 minutes with Rails

Generating PDF for your webapplication is not that difficult as you think.

There are many ways to do this, and there’s another great gem out there that does this functionality quite well too, called Prawn, but for the purpose of todays' tutorial, I’ll be going through another popular gem called wicked_pdf instead.

WickedPDF uses wkhtmltopdf which allows you to generate PDFs directly from HTML without having to adhere to a DSL.

This makes it much more easier to quickly get started without the hassle.

Step 1: Install wkhtmltopdf and wicked_pdf gem

  # Gemfile
  gem 'wicked_pdf'
  gem 'wkhtmltopdf-binary'

Step 2: Run the Generator

  $ rails generate wicked_pdf

Next, because you’re installing the wkhtmltopdf binary files via the Gemfile already, you’ll need to ensure that wicked_pdf knows where to find them.

  # in your config/initializers/wicked_pdf.rb
  # which was generated
  
  # comment out the :exe_path till it looks like this:
  WickedPdf.config = {
    #:wkhtmltopdf => '/usr/local/bin/wkhtmltopdf',
    #:layout => "pdf.html",
    #:exe_path => '/usr/local/bin/wkhtmltopdf'
  }

If you don’t comment it out, you’ll probably get a ‘Bad wkhtmltopdf’s path’ error.

Once that’s done, you’re pretty much ready to go! You now have 2 options to use wicked_pdf. One, rendering from a controller or Two, rendering it as an external library.


WARNING: OSX (Yosemite) Users

There is a bug with Yosemite and the wkhtmltopdf-binary gem which will prevent the wkhtmltopdf-binarygem from working. Replace your config/initializers/wicked_pdf.rb with the following to get back into track.


  # replace your config/initializers/wicked_pdf.rb with this
  module WickedPdfHelper
  if Rails.env.development?
    if RbConfig::CONFIG['host_os'] =~ /linux/
      executable = RbConfig::CONFIG['host_cpu'] == 'x86_64' ?
'wkhtmltopdf_linux_x64' : 'wkhtmltopdf_linux_386'
    elsif RbConfig::CONFIG['host_os'] =~ /darwin/
      executable = 'wkhtmltopdf_darwin_386'
    else
      raise 'Invalid platform. Must be running linux or intel-based Mac OS.'
    end

    WickedPdf.config = { exe_path:
"#{Gem.bin_path('wkhtmltopdf-binary').match(/(.+)\/.+/).captures.first}/#{executable}"
}
  end
end

Rendering from a Controller

To use it to render a pdf view from a controller, all you need to do is to add the following in your controller method.

Lets take the example of an invoicing app, and you want to generate a pdf invoice when the user access /invoices/1.

First, just add the following code into the invoices_controller.rb (or any controller of your choice of course!)

I’ll explain in a bit what do each of the parameters mean, but remember to substitute them for your own relevant data!

  # invoices_controller.rb

  def show

    @invoice = Invoice.find params[:id]

    respond_to do |format|
      format.pdf do
        render pdf: "file_name_of_your_choice",
               template: "invoices/show.pdf.erb",
               locals: {:invoice => @invoice}
      end
    end
  end


pdf: “file_name_of_your_choice”

This contains the filename of the pdf that will be generated. You need this line in order to let WickedPDF know to start its magic.

template: “invoices/show.pdf.erb”

This is your bread and butter. The draw of wicked_pdf is that you can use an existing HTML layout to generate your PDF. This line tells WickedPDF where to look for that particular file. In this case, it can be found in your app/views/invoices/show.pdf.erb.

locals: {:invoice => @invoice}

With your HTML layout being loaded, what use is it if you cannot populate it with dynamic data. In this example, you want to send your invoice details to your view. To do so, we set the locals params like in a partial.


So what is invoices/show.pdf.erb?

  <!-- app/views/invoices/show.pdf.erb -->

  <h1><%= invoice.title %></h1>
  <p><%= invoice.description %></p>
  <p><%= invoice.amount %></p>

It is just like your normal view, you can design it in anyway you wish and it will render it onto your pdf.

If you notice in the above snippet, we added the locals params. Just like render partials, this would allow you to use your variables in the view.

And that is all. When your user accesses /invoices/1, a pdf should pop up for them.

##Rendering as an external library There may be cases where you want to extract the pdf generation out. Where you don’t necessarily want to use it as an alternative display to a view.

In cases like that, you will need to externally render your pdf.

For example sake, lets say I have an external library that will generate a PDF and I want to use the generated PDF as an e-mail attachment.

How do we do that?

First, lets create the library and a class method.

# in /lib/pdf_generator/bot.rb

module PdfGenerator
  class Bot
    def self.receipt(payment_details)
    end
  end
end

Got it?

Ok, now lets add the code.

So if you noticed in the earlier example, we are rendering our PDF from ActionController and if you noticed, we have render pdf: ….

# invoices_controller.rb
def show
  ..
    render pdf: "file_name_of_your_choice",
 ..
end

The pdf params is very important. This tells the Rails app to use WickedPDF’s rendering method to parse and create the PDF.

Problem is, you can’t call a render outside of an ActionController, so how do we then render from an external library?

Easy, we bring ActionController out and use the render_to_string method

# in bot.rb
..
class Bot
  def self.receipt(payment_details)
    # we initialize ActionController
    ac = ActionController::Base.new

    # then, we can pretty much copy everything like in the above render example.
    pdf = ac.render_to_string pdf: "file_name_of_your_choice",
               template: "invoices/show.pdf.erb",
               locals: {:myvariable => variable}
  end
end
..

Protip

If you’re looking to use your Application Helpers, you probably will notice that your helpers cannot be used in your templates. To reactivate that function, replace ActionController::Base.new with ApplicationController.new Thats it!


But wait, isn’t this just a string? How useful can a string be?

Well, now you can create a tempfile to store the pdf.

# in bot.rb
..
    pdf = ac.render_to_string pdf: "file_name_of_your_choice",
               template: "invoices/show.pdf.erb",
               locals: {:myvariable => variable}
    
    # lets create a Tempfile which we will use to generate an actual file to be
    # attached to an e-mail
    tempfile = Tempfile.new "anynamehere.pdf"
    tempfile.binmode
    tempfile.write pdf
    tempfile.close
    tempfile
..

So now PdfGenerator::Bot.receipt takes in an object, parses it into a view, and returns a nice looking generated tempfile PDF.

With that tempfile, you can then add and attach it in your mailer.

Like:

# example_mailer.rb
def payment_details(details)
  attachments["anyfilenametoo.pdf"] = File.read(PdfGenerator::Bot.receipt(details).path)
  mail(subject: .....)
end

Tada!

Hope this helps!