#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)
RailsアプリケーションでPDFを生成する必要がある場合、Prawnは優れたソリューションです。PrawnToと呼ばれるプラグインがPrawnをRailsアプリケーションに統合する作業を簡略化してくれますが、Railsの最新版では動作せず、最近はメンテナンスされていません。幸いなことにPrawnを直接追加するのはそれほど難しくはないので、今回のエピソードではその方法を紹介します。
PDFの注文票を作成する
下の画面はオーダーされた注文の詳細を表示するRailsアプリケーションです。このオーダー情報のPDF版を提供してユーザが自分のオーダーをダウンロードあるいは印刷できるようにするため、Prawnを利用します。
Prawnはgemとして提供されているので、インストールするにはGemfile
に追加して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'
PDFを生成できるようにするためには、Railsに対してPDF MIMEタイプの情報を与えます。このためにmime_types
初期化ファイルにタイプを追加します。(これはアプリケーションにすでに存在しているはずですが、もしなければただ追加すればOKです。) これによってRailsがPDFリクエストにどう応答すべきかわかっていることが保証されます。
# 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
次にPDF版を作りたいコントローラアクションに、PDFデータを返すrespond_to
ブロックを追加します。今のところは、Prawnを利用して「Hello World」とのみ書かれた簡単なPDFドキュメントを生成するようにします。
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
Railsサーバの再起動後、オーダー情報ページのURLに.pdf
を追加することでPDFドキュメントにアクセスできるはずです。http://localhost:3000/orders/1.pdf
を開くとブラウザがPDFドキュメントを表示するかダウンロードします。
このドキュメントのファイル名がもう少し内容を表すものであれば便利でしょう。上で使用しているsend_data
メソッドはいくつかのオプションをとりますが、その中の一つであるfilename
を使えばそれが可能です。それに合わせてtype
オプションを設定してデフォルトをapplication/octet-stream
とし、disposition
をinline
に設定して、デフォルトではPDFがダウンロードされるのではなくブラウザ内で表示されるようにします。
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
ブラウザでPDFのURLにアクセスするとファイルがブラウザ内で表示されます。
最終的にはPDFにオーダーに関する情報を表示させたいのですが、その前にHTMLのオーダーページにリンクを追加して、ユーザがいちいちブラウザにURLを入力しなくても済むようにします。これをshowテンプレートの最後に追加します。
<p><%= link_to "Printable Receipt (PDF)", order_path(@order, format: "pdf") %></p>
リンクはそれ自体が含まれているアクションを指すので、order_path
を使用して現在のオーダーを渡します。リンクがPDF版を指すようにformat
オプションを使用します。
PDFにオーダー情報を追加する
次にPDFファイルの内容を修正することに集中して、ファイルの中にオーダー情報を含めます。これはすべてOrdersController
の中でおこなえますが、PDFを別の独立したクラスで生成するようにしたらコードがよりすっきりするでしょう。PDFを生成するコードを、新規に作成した/app/pdfs
ディレクトリの中のorder_pdf.rb
ファイル の中に入れます。
class OrderPdf < Prawn::Document def initialize super text "Order goes here" end end
クラス名がOrderPDF
ではなくOrderPdf
であることに注意してください。これによってRailsがそれを探しやすくなります。もしこれが気に入らなければ、例えばOrderDocument
のような別の名前を選ぶこともできます。このクラスはPrawn::Document
を継承しますが、もし継承を使いたくなければ代わりにここでインスタンス化する独立したPrawnドキュメントに委譲することも可能です。しかし、継承を使えばPrawn::Document
のメソッドを使用するのが簡単になります。
initialize
メソッドの中でOrdersController
でシンプルなPrawn::Document
の代わりに新規のOrderPdf
を作成できます。
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
OrderPdfクラスを認識させるためにサーバを再起動し、ページをリロードすると「Order goes here」の表示を確認できます。
この文字をオーダー情報に置き換えるのですが、Prawnがサポートするコマンドをどうやって調べればいいでしょうか? Prawnには2つの優れたドキュメントがあります。APIドキュメントは利用できるメソッドとクラスのリファレンスで、self-documenting manualはサンプルとPrawnでできることの詳細情報を含んだPDFファイルです。このマニュアルはダウンロードして読んでみる価値が十分あります。これらの情報源があれば、簡単にPDFドキュメントの中に何でも作ることができるでしょう。
ではこれからダミーテキストを実際のオーダー情報に置き換えていきます。まずオーダー番号を追加します。OrderPdf
クラスはコントローラ内の現在のオーダーにアクセスすることができないので、initializerを介して渡す必要があります。しかしこれをbaseクラスには渡したくないので、super
の呼び出しを修正して、代わりに他のドキュメントオプションを渡すのに使います。
class OrderPdf < Prawn::Document def initialize(order) super(top_margin: 70) @order = order text "Order \##{@order.order_number}" end end
OrdersController
で、OrderPdf
オブジェクトに対して現在のオーダーを渡します。
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
ページをリロードするとオーダー番号が表示されています。
OrderPdf
クラスを整理された状態にするために、各パートの生成を別のメソッドに分けることにして、まず最初にオーダー番号を生成するコードを移動します。オーダー番号を今よりも大きくしたいので、オプションのsize
とbold
を用いてこれをおこないます。
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
PDFをリロードするとオーダー番号はずっと大きくなっています。
次に明細行を出力します。それをおこなうコードは次の通りです。
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
オーダー番号と項目行の間に少しスペースを作るために、move_down
を使ってドキュメントを下方向に20ポイント移動させます。項目を表形式で表示したいので、そのためにPrawnのtable
メソッドを使用します。これは引数として2次元の配列をとるので、table [[1,2],[3,4]]
を呼び出したらそれらの値を持った2行2列のテーブルが表示されます。この配列を生成するコードをline_item_rows
メソッドの中に置いたことと、最初の行にヘッダ情報が含まれていることに注目してください。
PDFファイルをリロードすると以下の内容が表示されます。
table
メソッドにブロックを渡すことによって表の見え方をカスタマイズできます。ブロックは引数としてtable
オブジェクトをとることができますが、もしそれを渡さなかったら、table
オブジェクトに対してinstance_eval
とinstance_eval_all
のメソッド呼び出しを使用します。ブロックではrow
とcolumn
の各メソッドを使って、外観を変更したいセルを指定します。最初の行をボールドにして、最初の列以外をすべて右揃えにします。row_colors
を使用して各行の背景色を設定します。これに色の配列を渡すと、この中でループして各行の色を設定します。最後に、このテーブルにはヘッダ情報が含まれるので、header
をtrue
に設定するのがいいでしょう。これによって、テーブルが次のページにあふれた場合にはヘッダが繰り返し表示されます。
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
再度ページをリロードするとこれらの変更が反映されています。
オーダーフォームはきれいになりましたが、価格に通貨記号が表示されておらず、小数点以下2桁を表示するよう統一されてもいません。もしビューヘルパーにアクセスできたら、number_to_currency
を呼び出して価格を正しくフォーマットすることができます。クラスからはアクセスできませんが、コントローラからview_contextを渡すことができます。
pdf = OrderPdf.new(@order, view_context)
これをOrderPdf
クラスの中で使用して、コンストラクタで@view
インスタンス変数を設定してクラスの中からアクセスできるようにします。設定ができたら新たに正しくフォーマットされた価格を返す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
価格が希望通りにフォーマットされました。
PDFを完成させるために最後にもう一つ、合計価格を追加します。これを生成するために簡単なtotal_price
メソッドを作成し、初期化ファイルから呼び出します。
def total_price move_down 15 text "Total Price: #{price(@order.total_price)}", size: 16, style: :bold end
これでオーダーフォームが完成しました。
Prawnの代替
Prawnに関する今回のエピソードは以上です。RailsアプリケーションでPDFを生成する方法は他にもいくつかあります。例えばPDFKitを使えばHTMLからPDFドキュメントを作成できますが、これについてはエピソード220で紹介しました。