#153 PDFs with Prawn (revised)
- Download:
- source codeProject Files in Zip (87.2 KB)
- mp4Full Size H.264 Video (25.1 MB)
- m4vSmaller H.264 Video (13.1 MB)
- webmFull Size VP8 Video (15.3 MB)
- ogvFull Size Theora Video (30.2 MB)
If you ever need to generate PDFs in a Rails application Prawn is a great solution. A plugin called PrawnTo makes integrating Prawn with Rails applications easier but it doesn’t work with the latest version of Rails and isn’t well maintained. Thankfully adding Prawn directly isn’t that difficult and we’ll show you how in this episode.
Creating a PDF Order Form
Below is a page from a Rails application that shows the details of an order that has been placed. We’d like to offer a PDF version of this order information so that users can download or print out their orders and we’ll use Prawn to do this.
Prawn comes as a gem so to install it we just need to add it to the Gemfile
and run bundle
.
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'prawn'
Before we can generate any PDFs we need to tell Rails about the PDF MIME type. We do this by adding the type in the mime_types
initializer file. (This file should already exist in your application, but if it doesn’t then you can just add it.) This ensures that Rails knows how to respond to a PDF request.
# Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: Mime::Type.register "application/pdf", :pdf
Next we need to go into the controller action that we want a PDF version of and add a respond_to
block that returns the PDF data. For now we’ll use Prawn to generate a simple PDF document that contains the phrase “Hello World”.
def show @order = Order.find(params[:id]) respond_to do |format| format.html format.pdf do pdf = Prawn::Document.new pdf.text "Hello World" send_data pdf.render end end end
After we restart our Rails server we should be able to access our PDF document by appending .pdf
to the order’s URL and if we visit http://localhost:3000/orders/1.pdf
our browser will either show or download a PDF document.
It would be useful if the filename for this document was a little more descriptive. The send_data
method we use above takes a number of options one of which, filename
, allows us to do just that. We’ll also set the type
option as this defaults to application/octet-stream
and also set the disposition
to inline
so that the PDF is displayed in the browser by default rather than being downloaded.
def show @order = Order.find(params[:id]) respond_to do |format| format.html format.pdf do pdf = Prawn::Document.new pdf.text "Hello World" send_data pdf.render, filename: "order_#{@order.order_number}", type: "application/pdf", disposition: "inline" end end end
When we visit the PDF’s URL in the browser now the file is shown in the browser.
We need to get our PDF to display information about the order but before we do we’ll add a link to the HTML order page so that we don’t have to keep entering the URL directly into the browser. We’ll add this at the bottom of the show template.
<p><%= link_to "Printable Receipt (PDF)", order_path(@order, format: "pdf") %></p>
The link points to the same action that the link is on so we can use order_path
and pass it the current order. To make the link point to the PDF version we use the format
option.
Adding Order Information to The PDF
We can focus now on changing the content of the PDF file so that it contains the order information. We could do all this in the OrdersController
but our code will be cleaner if we generate the PDF in a separate class. We’ll put our PDF generating code in an order_pdf.rb
file in a new /app/pdfs
directory.
class OrderPdf < Prawn::Document def initialize super text "Order goes here" end end
Note that the class name is OrderPdf
rather than OrderPDF
as this makes it easier for Rails to find it. If this bothers you you can choose an alternative name, say OrderDocument
. Our class inherits from Prawn::Document
but if don’t like using inheritance you can delegate off to a separate Prawn document that you instantiate here instead. Using inheritance makes it easier to use Prawn::Document
’s methods, however.
In the initialize
method we need to call OrdersController
we’ll can now create a new OrderPdf
instead of a plain Prawn::Document
.
def show @order = Order.find(params[:id]) respond_to do |format| format.html format.pdf do pdf = OrderPdf.new send_data pdf.render, filename: "order_#{@order.order_number}", type: "application/pdf", disposition: "inline" end end end
We’ll need to restart our server for the OrderPdf class to be picked up but once we do and we reload the page we’ll see “Order goes here”.
We need to replace this text with the order information but how do we know what commands Prawn supports? Prawn has two good sources of documentation: the API documentation is a reference for the methods and classes that are available while the self-documenting manual is a PDF containing examples and detailed information about what we can do with Prawn. The manual is well-worth downloading and reading. With these sources of information it’ll be easy for us to generate whatever we want in our PDF document.
Now we can start to replace the placeholder text with actual information about the order. We’ll add the order number first. Our OrderPdf
class doesn’t have access to the current order in the controller so we’ll need to pass it in through the initializer. We don’t want to pass this up to the base class, though, so we’ll change the call to super
and use it to pass in some other document options instead.
class OrderPdf < Prawn::Document def initialize(order) super(top_margin: 70) @order = order text "Order \##{@order.order_number}" end end
In the OrdersController
we’ll pass in the current order to the OrderPdf
object.
def show @order = Order.find(params[:id]) respond_to do |format| format.html format.pdf do pdf = OrderPdf.new(@order) send_data pdf.render, filename: "order_#{@order.order_number}", type: "application/pdf", disposition: "inline" end end end
When we reload the page now we’ll see the order number displayed.
To keep the OrderPdf
class organized we’ll split each part’s generation out into a separate method and to start we’ll move the code that generates the order number. We want the order number to appear larger than it currently does and we’ll use the size
and bold
options to do that.
class OrderPdf < Prawn::Document def initialize(order) super(top_margin: 70) @order = order order_number end def order_number text "Order \##{@order.order_number}", size: 30, style: :bold end end
When we reload our PDF now the order number is much larger.
Next we’ll render out each line item. The code to do that looks like this:
class OrderPdf < Prawn::Document def initialize(order) super(top_margin: 70) @order = order order_number line_items end def order_number text "Order \##{@order.order_number}", size: 30, style: :bold end def line_items move_down 20 table line_item_rows end def line_item_rows [["Product", "Qty", "Unit Price", "Full Price"]] + @order.line_items.map do |item| [item.name, item.quantity, item.unit_price, item.full_price] end end end
To make some space between the order number and the items we’ve used move_down
to move 20 points down the document. We want the items to appear in a table and we’ve used Prawn’s table
method for this. This takes a two-dimensional array of items so if we were to call table [[1,2],[3,4]]
we’d see a two-row, two-column table containing those values. Note that we’ve put the code to generate this array in a line_item_rows
method and that the first row contains the header information.
Reloading the PDF file now gives us this:
We can customize the way the table looks by passing a block to the table
method. The block can take a table
object but if we don’t pass one in it will use instance_eval
and instance_eval
all method calls to that table object. In the block we use the row
and column
methods to scope the cells whose appearance we want to change. We’ll make the first row bold and all columns except the first right-aligned. To set the background colour of each row we use row_colors
. We pass this an array of colours and it will loop through this to set the colour of each row. Finally, as the first row of our table contains header information it’s a good idea to set header
to true
. This means that the row will be repeated if the table overflows onto another page.
def line_items move_down 20 table line_item_rows do row(0).font_style = :bold columns(1..3).align = :right self.row_colors = ["DDDDDD", "FFFFFF"] self.header = true end end
Reloading the page again will show us these changes.
Our order form looks better now but the prices don’t show the currency symbol and aren’t always formatted to two decimal places. If we had access to our view helper we could call number_to_currency
to format the prices correctly. Our class doesn’t have access to these but we can pass in the view context from the controller.
pdf = OrderPdf.new(@order, view_context)
We can now use this in the OrderPdf
class and set a @view
instance variable in the constructor so that we can access it throughout the class. With it in place we can define a new price
method to return a correctly-formatted price and use that wherever we display a price.
class OrderPdf < Prawn::Document def initialize(order, view) super(top_margin: 70) @order = order @view = view order_number line_items end # Other methods omitted. def line_item_rows [["Product", "Qty", "Unit Price", "Full Price"]] + @order.line_items.map do |item| [item.name, item.quantity, price(item.unit_price), price(item.full_price)] end end def price(num) @view.number_to_currency(num) end end
The prices are now formatted just how we want them.
There’s one more thing we’ll add to our PDF to make it complete and that is a the total price. We’ll create a simple total_price
method to generate this and call it in the initializer.
def total_price move_down 15 text "Total Price: #{price(@order.total_price)}", size: 16, style: :bold end
We now have a completed order form.
Alternatives to Prawn
That’s it for our episode on Prawn. There are several other ways of generating PDFs in Rails applications. You could, for example, use PDFKit to create a PDF document from HTML and this was covered in episode 220.