Generating PDFs From HTML With Rails

Datetime:2016-08-23 05:08:40          Topic: HTML  Ruby on Rails           Share

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!”

require "pdfkit"

kit = PDFKit.new(<<-HTML)
  <p>Hello Envato!</p>
HTML

kit.to_file("hello.pdf")

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

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:

require "pdfkit"

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 .

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:

require "pdfkit"

kit = PDFKit.new(<<-HTML)
  <style>
    * {
      color: grey;
    }
    h1 {
      text-align: center;
      color: black;
      margin-bottom: 100px;
    }
    .notes {
      margin-top: 100px;
    }

    table {
      width: 100%;
    }
    th {
      text-align: left;
      color: black;
      padding-bottom: 15px;
    }
  </style>

  <h1>Envato Invoice</h1>

  <table>
    <thead>
        <tr>
          <th>Description</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
          <tr>
            <td>Monthly Subscription to Tuts+</td>
            <td>$15</td>
          </tr>
      </tbody>
  </table>

  <div class="notes">
    <p><strong>Notes:</strong> This invoice was paid on the 23rd of March 2016 using your credit card ending on 1234.</p>
  </div>
HTML

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:

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:

$ rails new envato_invoices
$ cd envato_invoices

$ rails generate model invoice date:date client notes
$ rails generate model line_item description price:float invoice:references

$ rake db:migrate

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

line_items = LineItem.create([
    { description: 'Tuts+ Subscription April 2016', price: 15.0 }, 
    { description: 'Ruby eBook', price: 9.90} ])
Invoice.create(
    client: 'Pedro Alonso', 
    total: 24.90, 
    line_items: line_items, 
    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

class InvoicesController < ApplicationController
  def index
    @invoices = Invoice.all
  end

  def show
    @invoice = Invoice.find(params[:id])
  end
end

app/views/invoices/index.html.erb

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

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

Rails.application.routes.draw do
  root to: 'invoices#index'

  resources :invoices, only: [:index, :show]
end

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

app/views/invoices/show.html.erb

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

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

  <table>
    <thead>
        <tr>
          <th>Description</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
        <% @invoice.line_items.each do |line_item| %>
          <tr>
            <td><%= line_item.description %></td>
            <td><%= number_to_currency(line_item.price) %></td>
          </tr>
        <% end %>
        <tr class="total">
          <td style="text-align: right">Total: </td>
          <td><%= number_to_currency(@invoice.total) %></span></td>
        </tr>
      </tbody>
  </table>

  <% if @invoice.notes %>
  <div class="notes">
    <p><strong>Notes:</strong> <%= @invoice.notes %></p>
  </div>
  <% end %>
</div>

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

.invoice {
  width: 700px;
  max-width: 700px;
  border: 1px solid grey;
  margin: 50px;
  padding: 50px;

  h1 {
    text-align: center;
    margin-bottom: 100px;
  }
  .notes {
    margin-top: 100px;
  }

  table {
    width: 90%;
    text-align: left;
  }
  th {
    padding-bottom: 15px;
  }

  .total td {
    font-size: 20px;
    font-weight: bold;
    padding-top: 25px;
  }
}

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

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.

gem 'pdfkit'
gem 'render_anywhere'
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. 

require "render_anywhere"

class InvoicePdf
  include RenderAnywhere

  def initialize(invoice)
    @invoice = invoice
  end

  def to_pdf
    kit = PDFKit.new(as_html, page_size: 'A4')
    kit.to_file("#{Rails.root}/public/invoice.pdf")
  end

  def filename
    "Invoice #{invoice.id}.pdf"
  end

  private

    attr_reader :invoice

    def as_html
      render template: "invoices/pdf", layout: "invoice_pdf", locals: { invoice: invoice }
    end
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

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

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

  <table>
    <thead>
        <tr>
          <th>Description</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
        <% invoice.line_items.each do |line_item| %>
          <tr>
            <td><%= line_item.description %></td>
            <td><%= number_to_currency(line_item.price) %></td>
          </tr>
        <% end %>
        <tr class="total">
          <td style="text-align: right">Total: </td>
          <td><%= number_to_currency(invoice.total) %></span></td>
        </tr>
      </tbody>
  </table>

  <% if invoice.notes %>
  <div class="notes">
    <p><strong>Notes:</strong> <%= invoice.notes %></p>
  </div>
  <% end %>
</div>

/app/views/layouts/invoice_pdf.erb

<!DOCTYPE html>
<html>
<head>
  <title>Envato Invoices</title>
  <style>
    <%= Rails.application.assets.find_asset('application.scss').to_s %>
  </style>
</head>
<body>
  <%= yield %>
</body>
</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:

Rails.application.routes.draw do
  root to: "invoices#index"

  resources :invoices, only: [:index, :show] do
    resource :download, only: [:show]
  end
end

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

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

Add app/controllers/downloads_controller.rb :

class DownloadsController < ApplicationController

  def show
    respond_to do |format|
      format.pdf { send_invoice_pdf }
    end
  end

  private

  def invoice_pdf
    invoice = Invoice.find(params[:invoice_id])
    InvoicePdf.new(invoice)
  end

  def send_invoice_pdf
    send_file invoice_pdf.to_pdf,
      filename: invoice_pdf.filename,
      type: "application/pdf",
      disposition: "inline"
  end
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 :

<%= link_to "Download PDF",
    invoice_download_path(@invoice, format: "pdf"),
    target: "_blank",
    class: "download" %>

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

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 .

class DownloadsController < ApplicationController

  def show
    respond_to do |format|
      format.pdf { send_invoice_pdf }

      if Rails.env.development?
        format.html { render_sample_html }
      end
    end
  end

  private

  def invoice
    Invoice.find(params[:invoice_id])
  end

  def invoice_pdf
    InvoicePdf.new(invoice)
  end

  def send_invoice_pdf
    send_file invoice_pdf.to_pdf,
      filename: invoice_pdf.filename,
      type: "application/pdf",
      disposition: "inline"
  end

  def render_sample_html
    render template: "invoices/pdf", layout: "invoice_pdf", locals: { invoice: invoice }
  end
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.





About List