1. Code
  2. Ruby
  3. Ruby on Rails

Generating PDFs From HTML With Rails

Scroll to top

There are many ways to generate PDFs in Ruby and Rails. Chances are that you are already familiar with HTML and CSS, so we are going to use PDFKit to generate PDF files using HTML from standard Rails view and style code.

Introduction to PDFKit

Internally, PDFKit uses wkhtmltopdf (WebKit HTML to PDF), an engine that will take HTML and CSS, render it using WebKit, and output it as a PDF with high quality.

To start, install wkhtmltopdf on your computer. You can download the binary or install from Brew on Mac, or your preferred Linux repository.

You also need to install the pdfkit gem, and then run the following bit of Ruby to generate a PDF with the text “Hello Envato!”

1
require "pdfkit"
2
3
kit = PDFKit.new(<<-HTML)
4
  <p>Hello Envato!</p>

5
HTML

6
7
kit.to_file("hello.pdf")

You should have a new file called hello.pdf with the text at the top.

An example of the PDF that was generatedAn example of the PDF that was generatedAn example of the PDF that was generated

PDFKit also allows you to generate a PDF from a URL. If you want to generate a PDF from the Google homepage, you can run:

1
require "pdfkit"
2
3
PDFKit.new('https://www.google.com', :page_size => 'A3').to_file('google.pdf')

As you can see, I’m specifying the page_size—by default, A4 is used. You can see a full list of options here.

An example of the Google homepage in PDF formatAn example of the Google homepage in PDF formatAn example of the Google homepage in PDF format

Styling Your PDF Using CSS

Earlier I mentioned that we are going to generate PDF files using HTML and CSS. In this sample, I have added a bit of CSS to style the HTML for a sample invoice, as you can see:

1
require "pdfkit"
2
3
kit = PDFKit.new(<<-HTML)
4
  <style>

5
    * {

6
      color: grey;

7
    }

8
    h1 {

9
      text-align: center;

10
      color: black;

11
      margin-bottom: 100px;

12
    }

13
    .notes {

14
      margin-top: 100px;

15
    }

16


17
    table {

18
      width: 100%;

19
    }

20
    th {

21
      text-align: left;

22
      color: black;

23
      padding-bottom: 15px;

24
    }

25
  </style>

26


27
  <h1>Envato Invoice</h1>

28


29
  <table>

30
    <thead>

31
        <tr>

32
          <th>Description</th>

33
          <th>Price</th>

34
        </tr>

35
      </thead>

36
      <tbody>

37
          <tr>

38
            <td>Monthly Subscription to Tuts+</td>

39
            <td>$15</td>

40
          </tr>

41
      </tbody>

42
  </table>

43


44
  <div class="notes">

45
    <p><strong>Notes:</strong> This invoice was paid on the 23rd of March 2016 using your credit card ending on 1234.</p>

46
  </div>

47
HTML

48
49
kit.to_file("envato_invoice.pdf")

If you run this script, the file envato_invoice.pdf will be generated. This photo shows the result of the sample invoice:

An example of an Envato invoice PDFAn example of an Envato invoice PDFAn example of an Envato invoice PDF

As you can see, PDFKit is very easy to use, if you are already familiar with HTML and CSS. You can continue customising or styling this document as you like.

Using PDFKit From a Rails Application

Now let's take a look at how to use PDFKit in the context of a Rails application, so we can dynamically generate PDF files using the data from our models. In this section we're going to build a simple rails application to generate the previous "Envato Invoice" dynamically. Start by creating a new rails app and adding three models:

1
$ rails new envato_invoices
2
$ cd envato_invoices
3
4
$ rails generate model invoice date:date client notes
5
$ rails generate model line_item description price:float invoice:references
6
7
$ rake db:migrate

Now, we have to add some sample data to the database. Add this code snippet to db/seeds.rb.

1
line_items = LineItem.create([
2
    { description: 'Tuts+ Subscription April 2016', price: 15.0 }, 
3
    { description: 'Ruby eBook', price: 9.90} ])
4
Invoice.create(
5
    client: 'Pedro Alonso', 
6
    total: 24.90, 
7
    line_items: line_items, 
8
    date: Date.new(2016, 4, 1))

Run rake db:seed in your terminal to add the sample invoice to the database.

We are also interested in generating a list of invoices and the detail of one invoice in our app, so using rails generators, run rails generate controller Invoices index show to create the controller and views.

app/controllers/invoices_controller.rb

1
class InvoicesController < ApplicationController
2
  def index
3
    @invoices = Invoice.all
4
  end
5
6
  def show
7
    @invoice = Invoice.find(params[:id])
8
  end
9
end

app/views/invoices/index.html.erb

1
<h1>Invoices</h1>
2
<ul>
3
  <% @invoices.each do |invoice| %>
4
  <li>
5
    <%= link_to "#{invoice.id} - #{invoice.client} - #{invoice.date.strftime("%B %d, %Y")} ", invoice_path(invoice) %>
6
  </li>
7
  <% end %>
8
</ul>

We need to modify rails routes to redirect to InvoicesController by default, so edit config/routes.rb:

1
Rails.application.routes.draw do
2
  root to: 'invoices#index'
3
4
  resources :invoices, only: [:index, :show]
5
end

Start your rails server and navigate to localhost:3000 to see the list of invoices:

List of invoicesList of invoicesList of invoices

app/views/invoices/show.html.erb

1
<div class="invoice">
2
  <h1>Envato Invoice</h1>
3
4
  <h3>To: <%= @invoice.client %></h3>
5
  <h3>Date: <%= @invoice.date.strftime("%B %d, %Y") %></h3>
6
7
  <table>
8
    <thead>
9
        <tr>
10
          <th>Description</th>
11
          <th>Price</th>
12
        </tr>
13
      </thead>
14
      <tbody>
15
        <% @invoice.line_items.each do |line_item| %>
16
          <tr>
17
            <td><%= line_item.description %></td>
18
            <td><%= number_to_currency(line_item.price) %></td>
19
          </tr>
20
        <% end %>
21
        <tr class="total">
22
          <td style="text-align: right">Total: </td>
23
          <td><%= number_to_currency(@invoice.total) %></span></td>
24
        </tr>
25
      </tbody>
26
  </table>
27
28
  <% if @invoice.notes %>
29
  <div class="notes">
30
    <p><strong>Notes:</strong> <%= @invoice.notes %></p>
31
  </div>
32
  <% end %>
33
</div>

The CSS for this invoice details page has been moved to app/assets/stylesheets/application.scss

1
.invoice {
2
  width: 700px;
3
  max-width: 700px;
4
  border: 1px solid grey;
5
  margin: 50px;
6
  padding: 50px;
7
8
  h1 {
9
    text-align: center;
10
    margin-bottom: 100px;
11
  }
12
  .notes {
13
    margin-top: 100px;
14
  }
15
16
  table {
17
    width: 90%;
18
    text-align: left;
19
  }
20
  th {
21
    padding-bottom: 15px;
22
  }
23
24
  .total td {
25
    font-size: 20px;
26
    font-weight: bold;
27
    padding-top: 25px;
28
  }
29
}

Then when you click on an invoice in the main listing page, you'll see the details:

Invoice viewInvoice viewInvoice view

At this point, we are ready to add the functionality to our rails application to view or download the invoices in PDF.

InvoicePdf Class to Handle PDF Rendering

In order to render invoices from our rails app to PDF, we need to add three gems to the Gemfile: PDFKitrender_anywhere, and wkhtmltopdf-binary. By default, rails only allows you to render templates from a controller, but by using render_anywhere, we can render a template from a model or background job.

1
gem 'pdfkit'
2
gem 'render_anywhere'
3
gem 'wkhtmltopdf-binary'

In order not to pollute our controllers with too much logic, I'm going to create a new InvoicePdf class inside the app/models folder to wrap the logic to generate the PDF. 

1
require "render_anywhere"
2
3
class InvoicePdf
4
  include RenderAnywhere
5
6
  def initialize(invoice)
7
    @invoice = invoice
8
  end
9
10
  def to_pdf
11
    kit = PDFKit.new(as_html, page_size: 'A4')
12
    kit.to_file("#{Rails.root}/public/invoice.pdf")
13
  end
14
15
  def filename
16
    "Invoice #{invoice.id}.pdf"
17
  end
18
19
  private
20
21
    attr_reader :invoice
22
23
    def as_html
24
      render template: "invoices/pdf", layout: "invoice_pdf", locals: { invoice: invoice }
25
    end
26
end

This class is just taking the invoice to render as a parameter on the class constructor. The private method as_html is reading the view template invoices/pdf and layout_pdf that we are using to generate the HTML that we need to render as PDF. Lastly, the method to_pdf is using PDFKit to save the PDF file in the rails public folder. 

Possibly you want to generate a dynamic name in your real application so the PDF file doesn't get overwritten by accident. You might want to store the file on AWS S3 or a private folder too, but that is outside of the scope of this tutorial.

/app/views/invoices/pdf.html.erb

1
<div class="invoice">
2
  <h1>Envato Invoice</h1>

3


4
  <h3>To: <%= invoice.client %></h3>
5
  <h3>Date: <%= invoice.date.strftime("%B %d, %Y") %></h3>

6


7
  <table>

8
    <thead>

9
        <tr>

10
          <th>Description</th>

11
          <th>Price</th>

12
        </tr>

13
      </thead>

14
      <tbody>

15
        <% invoice.line_items.each do |line_item| %>

16
          <tr>

17
            <td><%= line_item.description %></td>
18
            <td><%= number_to_currency(line_item.price) %></td>

19
          </tr>

20
        <% end %>

21
        <tr class="total">
22
          <td style="text-align: right">Total: </td>

23
          <td><%= number_to_currency(invoice.total) %></span></td>

24
        </tr>
25
      </tbody>

26
  </table>
27
28
  <% if invoice.notes %>

29
  <div class="notes">
30
    <p><strong>Notes:</strong> <%= invoice.notes %></p>

31
  </div>

32
  <% end %>

33
</div>

/app/views/layouts/invoice_pdf.erb

1
<!DOCTYPE html>
2
<html>
3
<head>
4
  <title>Envato Invoices</title>

5
  <style>

6
    <%= Rails.application.assets.find_asset('application.scss').to_s %>

7
  </style>
8
</head>

9
<body>

10
  <%= yield %>

11
</body>
12
</html>

One thing to notice in this layout file is that we are rendering the styles in the layout. WkHtmlToPdf does work better if we render the styles this way.

DownloadsController to Render the PDF Invoice

At this point we need a route and controller that call the class InvoicePdf to send the PDF file to the browser, so edit config/routes.rb to add a nested resource:

1
Rails.application.routes.draw do
2
  root to: "invoices#index"
3
4
  resources :invoices, only: [:index, :show] do
5
    resource :download, only: [:show]
6
  end
7
end

If we run rake routes, we see the list of routes available in the application:

1
          Prefix Verb URI Pattern                              Controller#Action
2
            root GET  /                                        invoices#index
3
invoice_download GET  /invoices/:invoice_id/download(.:format) downloads#show
4
        invoices GET  /invoices(.:format)                      invoices#index
5
         invoice GET  /invoices/:id(.:format)                  invoices#show

Add app/controllers/downloads_controller.rb:

1
class DownloadsController < ApplicationController
2
3
  def show
4
    respond_to do |format|
5
      format.pdf { send_invoice_pdf }
6
    end
7
  end
8
9
  private
10
11
  def invoice_pdf
12
    invoice = Invoice.find(params[:invoice_id])
13
    InvoicePdf.new(invoice)
14
  end
15
16
  def send_invoice_pdf
17
    send_file invoice_pdf.to_pdf,
18
      filename: invoice_pdf.filename,
19
      type: "application/pdf",
20
      disposition: "inline"
21
  end
22
end

As you can see, when the request is asking for a PDF file, the method send_invoice_pdf is processing the request. The method invoice_pdf is just finding the invoice from the database by id, and creating an instance of InvoicePdf. Then send_invoice_pdf is just calling the method to_pdf, to send the generated PDF file to the browser. 

One thing to note is that we are passing the parameter disposition: "inline" to send_file. This parameter is sending the file to the browser, and it will be displayed. If you want to force the file to be downloaded, then you'll need to pass disposition: "attachment" instead.

Add a download button to your invoice show template app/views/invoices/show.html.erb:

1
  <%= link_to "Download PDF",

2
    invoice_download_path(@invoice, format: "pdf"),

3
    target: "_blank",

4
    class: "download" %>

Run the application, navigate to the invoice details, click on download, and a new tab will open displaying the PDF Invoice.

Invoice view PDF dynamically generatedInvoice view PDF dynamically generatedInvoice view PDF dynamically generated

Render PDF as HTML in Development

When you're working on the markup for your PDF, having to generate a PDF every time you want to test a change can be slow sometimes. For this reason, being able to view the HTML that will be converted to PDF as plain HTML can be really useful. We only need to edit /app/controllers/downloads_controller.rb.

1
class DownloadsController < ApplicationController
2
3
  def show
4
    respond_to do |format|
5
      format.pdf { send_invoice_pdf }
6
7
      if Rails.env.development?
8
        format.html { render_sample_html }
9
      end
10
    end
11
  end
12
13
  private
14
15
  def invoice
16
    Invoice.find(params[:invoice_id])
17
  end
18
19
  def invoice_pdf
20
    InvoicePdf.new(invoice)
21
  end
22
23
  def send_invoice_pdf
24
    send_file invoice_pdf.to_pdf,
25
      filename: invoice_pdf.filename,
26
      type: "application/pdf",
27
      disposition: "inline"
28
  end
29
30
  def render_sample_html
31
    render template: "invoices/pdf", layout: "invoice_pdf", locals: { invoice: invoice }
32
  end
33
end

Now the show method is also responding for HTML requests in development mode. The route for a PDF invoice would be something like http://localhost:3000/invoices/1/download.pdf. If you change it to http://localhost:3000/invoices/1/download.html, you will see the invoice in HTML using the markup that is used to generate the PDF.

Given the code above, generating PDF files using Ruby on Rails is straightforward assuming you're familiar with the Ruby language and the Rails framework. Perhaps the nicest aspect of the entire process is that you don't have to learn any new markup languages or specifics about PDF generation.

I hope this tutorial has proved useful. Please leave any questions, comments, and feedback in the comments and I'll be happy to follow-up.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.