読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでPrawnを使ってPDFを作成する

Rails gem PDF Generation

Prawn(プローン)は、RubyもしくはRailsでプログラムだけでPDFを作成するGemです。
今回は、RailsでPrawnを使って注文票のPDFを作成する手順を説明します。

f:id:nipe880324:20140906214909p:plain:w480


他にもPDFを作成するGemがあり、「Ruby/RailsのPDF作成Gemまとめ 」でまとめています。

対象読者

  • Rails+Prawnを使ったPDFの作成方法を知りたい
  • 一通りRailsの基礎について分かっている

動作確認

  • Ruby 2.1.2
  • Rails 4.1
  • Prawn 1.2.1
  • Mac OS X 10.9.4

目次

  1. 準備(注文詳細画面の作成)
  2. Prawnの導入
  3. Prawnで本格的なPDFの作成

1. 準備(注文詳細画面の作成)

まず、注文書のPDFを作成するために簡単な注文詳細画面を作成しましょう。
作成する画面は次の画面です。
f:id:nipe880324:20140906204302p:plain:w480

Prawnの使い方のみを知りたい方は、次の項目まで飛ばしてください。
また、実際にやりながら読みたい方は、GitHubに準備(注文詳細画面の作成)後のソースコードを配置してますので下記コマンドでローカルに持ってきてください。

$ git clone git@github.com:yanagi0324/rails_samples.git
$ cd pdf_order_sample  # 準備後のファイル

この章では、簡略化のために「注文(Order)」と「注文明細(LineItem)」のみで必要最小限しか実装していないので正しい設計ではありませんので注意してください。

まず、Railsプロジェクトを作成します。

$ rails new prawn_test
$ cd prawn_test

次に、Scaffoldで注文(Order)を作成します。

$ rails g scaffold Order purchased_at:datetime

さらに、注文明細(LineItem)のモデルを作成します。
DBから直接データをいれるため、Controller と View は作りません。

$ rails g model LineItem order_id:integer product_name:string price:integer quantity:integer

注文(Order)と注文明細(LineItem)モデルのアソシエーションを追加します。
注文は、複数の注文明細を持つので、has_many :line_itemsを追加します。

# app/models/order.rb
class Order < ActiveRecord::Base
  has_many :line_items
end

注文明細からは、単一の注文に属するので、belongs_to :orderを追加します。

# app/models/line_item.rb
class LineItem < ActiveRecord::Base
  belongs_to :order
end

では、マイグレーションをします。

$ rake db:migrate

画面操作部分を必要最低限しか作成しなので、seedでデータを追加します。

# db/seeds.rb
order1 = Order.create! purchased_at: 3.days.ago
order2 = Order.create! purchased_at: 2.days.ago
order3 = Order.create! purchased_at: 1.days.ago

LineItem.create! order_id: order1.id, product_name: "ノートPC", price: 50000, quantity: 1
LineItem.create! order_id: order1.id, product_name: "DVDプレイヤー", price: 5000, quantity: 1
LineItem.create! order_id: order1.id, product_name: "電子書籍Reader", price: 10000, quantity: 1 
LineItem.create! order_id: order1.id, product_name: "単三電池", price: 100, quantity: 3 

データを投入します。

$ rake db:seed


デフォルトでは画面がシンプルすぎるので、Twitter Bootstrapを追加します。

# Gemfile
...
gem 'therubyracer' # javascript runtime。lessをコンパイルするために必要
gem 'less-rails' # Railsでlessを使えるようにする。Bootstrapがlessで書かれているため
gem 'twitter-bootstrap-rails' # Bootstrapの本体

では、Gemをインストールします。

$ bundle install

実際にBootstrapを適用していきます。

$ rails g bootstrap:install   # Bootstrap関連のファイルを追加
$ rails g bootstrap:themed Orders -f   # OrderのViewにBootstrapを強制的に適用

ひとまず、$ rails sで現在の画面を確認します。
さすが、Bootstrap!なかなかいい感じですね。
f:id:nipe880324:20140906203401p:plain:w480

注文詳細画面を開くと注文の明細が表示されていませんので、表示するように修正しましょう。
f:id:nipe880324:20140906203415p:plain:w480

# app/views/orders/show.html.erb
<%- model_class = Order -%>
<div class="page-header">
  <h1><%=t '.title', :default => model_class.model_name.human.titleize %></h1>
</div>

<!-- 修正部分 開始 -->
<dl class="dl-horizontal">
  <dt><strong>注文番号:</strong></dt>
  <dd><%= @order.id %></dd>
  <dt><strong>注文日:</strong></dt>
  <dd><%= @order.purchased_at %></dd>
</dl>

<table class="table table-striped">
  <thead>
    <tr>
      <th>#</th>
      <th>品名</th>
      <th>単価</th>
      <th>数量</th>
      <th>値段</th>
    </tr>
  </thead>

  <tbody>
    <% @order.line_items.each_with_index do |line_item, idx| %>
      <tr>
        <td><%= idx + 1 %></td>
        <td><%= line_item.product_name %></td>
        <td><%= number_to_currency line_item.price %></td>
        <td><%= line_item.quantity %></td>
        <td><%= number_to_currency line_item.total_price %></td>
      </tr>
    <% end %>  
  </tbody>
</table>
<!-- 修正部分 終了 -->

<%= link_to t('.back', :default => t("helpers.links.back")),
              orders_path, :class => 'btn btn-default'  %>
<%= link_to t('.edit', :default => t("helpers.links.edit")),
              edit_order_path(@order), :class => 'btn btn-default' %>
<%= link_to t('.destroy', :default => t("helpers.links.destroy")),
              order_path(@order),
              :method => 'delete',
              :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
              :class => 'btn btn-danger' %>

上記のViewで、「注文明細の数量と値段から合計金額を計算するline_item.total_price」を使っているので実装します。

# app/models/line_item.rb
class LineItem < ActiveRecord::Base
  belongs_to :order

  def total_price
    price * quantity
  end
end

また、上記のViewで金額表示のフォーマットするために、number_to_currencyメソッドを使っていますが、Railsのデフォルトのロケールは「アメリカ」なのでドルで表示されてしまいます。ロケールを「日本」に変えましょう。

# config/application.rb
...
...
    # config.i18n.default_locale = :de
    config.i18n.default_locale = :ja
  end
end

では、日本語のロケールファイルをconfig/locales/ja.ymlで作成し、
GitHub - ja.ymlから内容を取得し、コピーしましょう。

最後に、ブラウザ上いっぱいに表示されてしまうため、layoutファイルを修正しましょう。

# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>PrawnTest</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>

<!-- 修正箇所 開始 -->
<div class="container">
  <%= yield %>
</div>
<!-- 修正箇所 終了 -->

</body>
</html>

config/配下を修正したので、サーバーを再起動して画面を確認しましょう。
注文明細も表示されるようになりました。また、画面いっぱいに表が表示されなくなりました。
f:id:nipe880324:20140906204042p:plain:w480

後は、注文詳細画面に注文の合計金額を表示するように修正します。
まずは、注文(Order)の合計金額を計算するメソッドを追加します。

# app/models/order.rb
class Order < ActiveRecord::Base
  has_many :line_items

  def total_price
    line_items.to_a.sum { |line_itme| line_itme.total_price }
  end
end

それでは、注文詳細画面に合計を表示する列を追加します。

# app/views/orders/show.html.erb
<%- model_class = Order -%>
<div class="page-header">
  <h1><%=t '.title', :default => model_class.model_name.human.titleize %></h1>
</div>

<dl class="dl-horizontal">
  <dt><strong>注文番号:</strong></dt>
  <dd><%= @order.id %></dd>
  <dt><strong>注文日:</strong></dt>
  <dd><%= @order.purchased_at %></dd>
</dl>

<table class="table table-striped">
  <thead>
    <tr>
      <th>#</th>
      <th>品名</th>
      <th>単価</th>
      <th>数量</th>
      <th>値段</th>
    </tr>
  </thead>

  <tbody>
    <% @order.line_items.each_with_index do |line_item, idx| %>
      <tr>
        <td><%= idx + 1 %></td>
        <td><%= line_item.product_name %></td>
        <td><%= number_to_currency line_item.price %></td>
        <td><%= line_item.quantity %></td>
        <td><%= number_to_currency line_item.quantity * line_item.price %></td>
      </tr>
    <% end %>

    <!-- 修正部分 開始 -->
    <tr class="warning">
      <td colspan="3" />
      <td>合計</td>
      <td><%= number_to_currency @order.total_price %></td>
    </tr>    
    <!-- 修正部分 終了 -->
  </tbody>
</table>

<%= link_to t('.back', :default => t("helpers.links.back")),
              orders_path, :class => 'btn btn-default'  %>
<%= link_to t('.edit', :default => t("helpers.links.edit")),
              edit_order_path(@order), :class => 'btn btn-default' %>
<%= link_to t('.destroy', :default => t("helpers.links.destroy")),
              order_path(@order),
              :method => 'delete',
              :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
              :class => 'btn btn-danger' %>

これで、一通りの準備は終わりました。
画面を確認してみましょう。合計が表示されてますね。
f:id:nipe880324:20140906204302p:plain:w480



2. Prawnの導入

インストール

毎度ながらGemfileに以下を追加します。

# Gemfile
...
# For PDF generation
gem 'prawn'        # PDF作成
gem 'prawn-table'  # PDFでテーブルを作成

Gemをインストールします。

$ bundle install

Prawn 1.2で大幅にPrawnが変わったようなのでWarningが表示されますがエラーではないのであまり気にしすぎないでください。

PDF文書の作成

URLのフォーマットが.pdfのときにPDF出力処理を呼びます。
まずは、PDF表示用のリンクをViewに追加します。

# app/views/orders/show.html.erb
...
# 一番したの行に追加
<%= link_to "PDFで表示",
              order_path(@order, format: "pdf"),
              :class => 'btn btn-primary' %>

そして、Controllerにrespond_toでPDF文書を作成するロジックを追加します。

# app/controllers/orders_controller.rb
  ...
  # GET /orders/1
  # GET /orders/1.json
  def show
    respond_to do |format|
      format.html # show.html.erb
      format.pdf do
        # PDF文書を作成
        pdf = Prawn::Document.new
        # PDFに "Hello, Prawn!!" と表示する
        pdf.text "Hello, Prawn!!"

        # 画面にPDFを表示する
        # disposition: "inline" によりPDFはダウンロードではなく画面に表示される
        send_data pdf.render,
          filename:    "#{@order.id}.pdf",
          type:        "application/pdf",
          disposition: "inline"
      end
    end
  end
  ...

では、サーバーを再行動して、注文詳細画面の "PDFで表示"ボタン を押してみましょう。
"Hello, Prawn!!" とPDFに表示されています。成功です :)
f:id:nipe880324:20140906210304p:plain:w480

Controllerで、"Hello, Prawn" を "こんにちは、プローン" に変えてみます。

# app/controllers/orders_controller.rb
  ...
  # GET /orders/1
  # GET /orders/1.json
  def show
        ... 
        # PDFに "こんにちは、プローン" と表示する
        pdf.text "こんにちは、プローン"

        ...
      end
    end
  end
  ...

では、画面を確認してみます。しかし、アンダーバーでうまく表示されていません。
これは、日本語のフォントを持っていないためPDFに表示されないのが原因です。
そのため、Railsプロジェクトに日本語のフォントを追加します。
f:id:nipe880324:20140906210612p:plain:w480

日本語フォントの追加

IPAフォントにアクセスし、
「2書体パック(IPAex明朝(Ver.002.01)、IPAexゴシック(Ver.002.01)) IPAexfont00201.zip(9.31 MB)」をダウンロードします。

その後、vender/fontsを作成し、その配下に

  • ipaexg.ttf (明朝)
  • ipaexm.ttf (ゴシック)

を配置します。

では、追加したフォントをPrawnから使うために、fontメソッドを追加します。

# app/controllers/orders_controller.rb
  ...
  # GET /orders/1
  # GET /orders/1.json
  def show
    respond_to do |format|
      format.html # show.html.erb
      format.pdf do
        # PDF文書を作成
        pdf = Prawn::Document.new

        # フォントを設定(明朝体)
        pdf.font "vendor/fonts/ipaexm.ttf" 

        # PDFに "こんにちは、プローン" と表示する
        pdf.text "こんにちは、プローン"
        ...
    end
  end
  ...

画面を開き直しましょう。日本語が表示されているはずです。
f:id:nipe880324:20140906211729p:plain:w480



3. Prawnで本格的なPDFの作成

Prawnを使ってPDFを出力することができました。さて、注文詳細画面をPDFで作成していきましょう。
まずは、作成するPDFのイメージを確認するために、注文詳細画面を確認します。
f:id:nipe880324:20140906204302p:plain:w480

PDF作成クラスの作成

PrawnはプログラムでごりごりとPDFを作成していくので、Controller内でそれを記載すると可読性や保守性が下がるので、PDF作成用のクラスを作成します。

格納するフォルダとファイルを作ります。

$ mkdir app/pdfs
$ touch app/pdfs/order_pdf.rb

PDF作成クラスを作成します。Prawn::Documentを継承していることが重要です。
後の中身はControllerから移動しただけです。

# app/pdfs/order_pdf.rb
class OrderPDF < Prawn::Document

  def initialize(order)
    super()

    font "vendor/fonts/ipaexm.ttf"
    text "こんにちは, Prawn!! on order_pdf.rb"
  end
end

では、Controllerからこのクラスを呼び出すように修正しましょう。

# app/controllers/orders_controller.rb
  ...
  # GET /orders/1
  # GET /orders/1.json
  def show
      format.html # show.html.erb
      format.pdf do
        # PDF文書を作成
        pdf = OrderPDF.new(@order)
        ...
    end
  end
  ...

とりあえず、ここまで上手くいっているか確認するために、サーバーを再起動して画面を確認します。
しっかりと表示されていますね。
f:id:nipe880324:20140906212815p:plain:w480

本格的にPDFを記載

では、作成したapp/pdfs/order_pdf.rbにゴリゴリPDF出力を記載します。
ソースとコメント、参考文献のPrawn manual、画面をそれぞれ見ながら書き方を理解してください。

ソースを上から書きながら、逐次画面表示を確認すると理解しやすいのでオススメです。

class OrderPDF < Prawn::Document

  def initialize(order)
    super()

    # 複数メソッドで利用できるようにするため
    # インスタンス変数に代入
    @order = order

    # 全体のフォントを設定
    font "vendor/fonts/ipaexg.ttf"
    
    # ヘッダー部分の表示
    header
    # ヘッダーリード部分の表示
    header_lead
    # テーブル部分の表示
    table_content
  end

  def header
    # size 28 で "Order"という文字を表示
    text "Order", size: 28

    # stroke(線)の色を設定し、線を引く
    stroke_color "eeeeee"
    stroke_line [0, 680], [530, 680]
  end

  def header_lead
    # カーソルを指定
    y_position = cursor - 30

    # bounding_boxで記載箇所を指定して、textメソッドでテキストを記載
    bounding_box([100, y_position], :width => 270, :height => 50) do
      font_size 10.5
      text "注文番号:  #{@order.id}"
      move_down 3
      text " 注文日:  #{@order.purchased_at}"
    end
  end

  def table_content
    # tableメソッドは2次元配列を引数(line_item_rows)にとり、それをテーブルとして表示する
    # ブロック(do...end)内でテーブルの書式の設定をしている
    table line_item_rows do
      # 全体設定
      cells.padding = 8          # セルのpadding幅
      cells.borders = [:bottom,] # 表示するボーダーの向き(top, bottom, right, leftがある)
      cells.border_width = 0.5   # ボーダーの太さ

      # 個別設定
      # row(0) は0行目、row(-1) は最後の行を表す
      row(0).border_width = 1.5
      row(-2).border_width = 1.5
      row(-1).background_color = "f0ad4e"
      row(-1).borders = []

      self.header     = true  # 1行目をヘッダーとするか否か
      self.row_colors = ['dddddd', 'ffffff'] # 列の色
      self.column_widths = [50, 200, 100, 70, 100] # 列の幅
    end
  end

  # テーブルに表示するデータを作成(2次元配列)
  def line_item_rows
    # テーブルのヘッダ部
    arr = [["#", "品名", "単価", "数量", "値段"]]

    # テーブルのデータ部
    @order.line_items.map.with_index do |item, i|
      arr << [i+1, item.product_name, item.price, item.quantity, item.total_price]
    end

    # テーブルの合計部
    arr << ["", "", "", "合計", @order.total_price]
    return arr
  end
end

では、画面を確認してみましょう。
このように、PrawnではコードだけでPDFを作成していきます。
f:id:nipe880324:20140906214909p:plain:w480