【Rails】PDF出力機能を実装できるGem「Prawn」の使い方と実例(出力サンプル)をご紹介

現在Railsで開発中のアプリにPDF出力機能を実装したい…

そこで、RailsでPDFを生成するためのgem(ライブラリ)について色々とググってみた結果、カスタマイズ性に優れたgem「Prawn」というgemを採用することにしました。

「Prawn」は開発コストはかかるものの、

  • レイアウトの細かい調整などカスタマイズ性に優れている
  • Rubyで書いて出力するのでRailsエンジニアにとって扱いやすい

などのメリットがあります。

当記事ではそのPrawnの使い方や、実際にPrawnを使って出力したPDFのサンプルをご紹介します。

RailsでPDF出力を実装したい方の参考になれば幸いです。

【追記】
Prawnを使って、登山計画書を作成するアプリを作ってみました。アプリのフォームから入力した情報を、あらかじめPrawnで作成しておいたPDFフォーマットに反映させることで希望通りの計画書を作成することができます。興味のある方はぜひ。
スマホで簡単!フォーム入力するだけで登山計画書(PDF)をサクッと作成・シェアできるアプリを開発してみた

目次

Prawnを導入してPDF出力するまでの流れ

ここでは、以下を前提として話を進めます。

  • Rails7.0.3 を使用
  • Railsアプリケーション作成済み($ rails new …)
  • モデル、データベース作成済み

それでは参ります。

Prawnの導入

Prawn導入のため、Gemfileに以下の内容を記述し、bundle installでgemをインストールします。

gem 'prawn'
gem 'prawn-table'
$ bundle install

コントローラーの作成

続いて、app/controllersにコントローラーを新規作成します。

今回はshow_pdf_controller.rbというコントローラー名とします。

作成したコントローラーに以下の内容を追記します。

class ShowPdfController < ApplicationController
  def index
    #ここにPDF出力を行う処理を記述する
  end
end

この後、ここにPDF出力を行う処理を記述していきます。

ルーティング設定

PDF出力するためのルーティングを設定します。

Rails.application.routes.draw do
  resources :show_pdf, only: :index
end

上記のように記述した場合、ルーティングは以下のようになります。

show_pdf_index GET  /show_pdf(.:format)  show_pdf#index

もし、特定モデルの中にルーティングをネストさせる場合は、以下のように記述します。

Rails.application.routes.draw do
  resources :posts do
    resources :show_pdf, only: :index
  end
end
post_show_pdf_index GET  /posts/:post_id/show_pdf(.:format)  show_pdf#index

こうすることで、特定のpostモデルのデータをPDFに渡すことができるようになります。

(個別の記事・投稿データなどをPDFに出力したい場合などに有効です)

PDF出力用のファイル(.rb)を作成

続いて、PDF出力用のファイル(.rb)を作成します。

ファイルの置き場所はどこでも良いですが、僕はapp/assets以下にpdfsフォルダを作成し、その中にPDF出力用ファイルshow_pdf.rbを作成することにします。

(以下のようなフォルダ構成になります)

app -- assets
         :
         └-- pdfs
               └-- show_pdf.rb

show_pdf.rbには以下の内容を記述します。

class ShowPdf < Prawn::Document
  def initialize
    super(page_size: 'A4') #A4サイズのPDFを新規作成
    stroke_axis # 座標を表示
  end
end

以降は、このファイルにPDFで出力する内容を記述していきます。

記述内容についてはPDF出力例(サンプル)をご覧ください。

日本語フォントの追加

PDF出力用ファイルはデフォルトで英語フォントしかないため、日本語用のフォントをダウンロードしておく必要があります。

まずは、以下のサイトから日本語フォント用のファイル(IPAexフォント 2書体パック)をダウンロードします。

https://forest.watch.impress.co.jp/library/software/ipaexfont

ダウンロードしたら、app/assets以下にfontsフォルダを新規作成し、そのフォルダ内にフォントファイル(.ttf)をコピーします。

PDF出力ファイルに日本語フォントを読み込むため、show_pdf.rbに以下の内容を追記します。

class ShowPdf < Prawn::Document
  def initialize
    super(page_size: 'A4') #A4サイズのPDFを新規作成
    stroke_axis # 座標を表示(これがあると便利)

    # ↓追記(日本語フォントの読み込み)
    font_families.update('JP' => { 
                            normal: 'app/assets/fonts/ipaexm.ttf', 
                            bold: 'app/assets/fonts/ipaexg.ttf' 
                        })
    font 'JP'

  end
end

これで日本語が使えるようになりました。

パスの設定(application.rb)

このままだとPDF出力ファイルshow_pdf.rbが読み込まれないので、読み込むよう設定を行います。

application.rbに以下の内容を追記してください。

module アプリ名
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.0

    ・・・
    # 以下の内容を追記
    config.eager_load_paths += %W(#{Rails.root}/app/assets/pdfs)
    # もしくは、こちら↓を追記(個人的にはこちらが好み)
    config.paths.add 'app/assets/pdfs', eager_load: true
  end
end

これで、PDFを作成する準備が整いました。

コントローラーの編集

次に、作成したPDFを出力するための処理をコントローラーに記述していきます。

show_pdf_controller.rbを以下のように編集します。

class ShowPdfController < ApplicationController
  def index
    respond_to do |format|
      format.pdf do
        show_pdf = ShowPdf.new().render
        send_data show_pdf,
          filename: "ファイル名.pdf",
          type: 'application/pdf',
          disposition: 'inline' # 外すとアクセス時に自動ダウンロードされるようになる
      end
    end
  end
end

上記の処理について順番に見ていきましょう。

まず、format.pdf do ~ end によりpdf形式での出力処理を do ~ end 間で行います。

続いてShowPdf.new().renderでインスタンスの生成を行い、変数show_pdfに代入してその出力準備を行います。

send_dataを用いることでshow_pdfに格納されたデータをファイル保存、またはインフラ表示させます。

filename:にてファイル保存時のファイル名を指定することもできます。

disposition: ‘inline’はPDFをブラウザ上に出力する際に必要なオプションで、

この記述を外すことにより、出力ではなくPDFのダウンロードが行われます。

PDF出力確認

準備が整ったところで、PDFが出力されるか確認してみましょう。

ルーティングで設定したPDF出力用のパスにアクセスしてみてください。

http://localhost:3000/show_pdf.pdf

以下のように、座標のみが表示された白紙のPDFが表示されれば成功です。

では、さっそくPDFをカスタマイズしてみましょう。

以下のように記述することで、PDFに文字やテーブルを配置することができます。

class ShowPdf < Prawn::Document
  def initialize
    super(page_size: 'A4') 
    stroke_axis

    font_families.update('JP' => { 
                            normal: 'app/assets/fonts/ipaexm.ttf', 
                            bold: 'app/assets/fonts/ipaexg.ttf' 
                        })
    font 'JP'
      
    #-------- ↓ここにコードを記述する ----------
    text "タイトル", size: 20, align: :center
    move_down 20

    text "◉サブタイトル", size: 14
    move_down 10

    schedule = [
      ["項目", "詳細"],
      ["(1)", ""],
      ["(2)", ""],
      ["(3)", ""],
      ["(4)", ""],
      ["(5)", ""],
    ]

    table schedule, cell_style: {height: 30},
    column_widths: [120, 400] do
      cells.size = 10
      row(0).border_top_width = 2
      row(0).border_bottom_width = 2
      columns(0).row(0..5).border_left_width = 2
      columns(-1).row(0..5).border_right_width = 2
      row(-1).border_bottom_width = 2
    end

  end
end

PDF出力してみると、

しっかり反映されていますね。

Prawnは座標指定(1px単位)でテキストやテーブル等の表示位置を決められるため、stroke_axisでグリッドを表示させておくと編集が楽です。

PDF出力画面へのリンクを貼る

最後に、アプリ内にPDF出力画面へのリンクを貼ります。

PDF出力画面へのリンクはルーティングで生成されたパスを用いて以下のように記述します。

<%= link_to "PDFを出力", show_pdf_index_path(format: "pdf") %>

ここで、パスの引数に(format: “pdf”)としているのは、このままではリンクをクリックした時に以下のアドレス(末尾に.pdf拡張子がついていない)にアクセスしてしまうからです。

http://localhost:3000/show_pdf

リンクの末尾に.pdfの拡張子がついていないとPDFと認識されずにエラーが吐き出されてしまいます。

そこで、アクセス先がPDFフォーマットであることを明示するためにパスの引数に(format: “pdf”)を入れることでアドレスに.pdfが付加され、問題なくPDF出力画面へアクセスすることができます。

PDF上にデータベースの値を受け渡す方法

これまでは、単純にPDFに文字やテーブルを配置するだけのやり方について説明してきましたが、

案件によってはデータベース内のデータをPDF内で利用したい場面もあるかと思います。

ここではデータベースから取得した値をPDF上に受け渡す方法について解説します。

コントローラーにPDF上で使用するレコードのインスタンスを作成

まず、コントローラーにPDF上で使用するレコードのインスタンス@recordsを作成し、ShowPdf.new(@records)とすることでPDF新規作成時にインスタンスをshow_pdf.rbに渡します。

class ShowPdfController < ApplicationController
  def index
    @records = Record.all # PDF上で使用するレコードのインスタンスを作成
    respond_to do |format|
      format.pdf do
        show_pdf = ShowPdf.new(@records).render # PDF新規作成時にインスタンスを渡す
        send_data show_pdf,
          filename: "ファイル名.pdf",
          type: 'application/pdf',
          disposition: 'inline'
      end
    end
  end
end

PDF側でインスタンスを受け取って変数に代入する

続いて、show_pdf.rb側でインスタンス@recordsを受け取れるよう、initializeメソッドに引数recordを指定します。

class ShowPdf < Prawn::Document
  def initialize(record) # ←ここでインスタンス(@records)の値を受け取る
    super(page_size: 'A4')
    stroke_axis
    @record = record # ←受け取った値をインスタンス変数として定義する。

    font_families.update('JP' => { 
                            normal: 'app/assets/fonts/ipaexm.ttf', 
                            bold: 'app/assets/fonts/ipaexg.ttf' 
                        })
    font 'JP'

    #-------- ↓ここにPDF出力用のコードを記述する ----------
     
  ・・・

    #-------------------------------------------------

  end
end

initialize(record)で受け取った値は、メソッドでも利用できるようインスタンス変数化しておきます。

これで、PDF上で特定モデルの値が利用できるようになりました。

PrawnによるPDF出力サンプルとコード

今回、PDF出力のサンプルとして登山計画書を作ってみました。(一応、趣味が登山なので)

サンプルはこちら↓

PDF出力用のコードは以下の通りです。

全体的にtableを多用しているため、tableの使い方を知りたい方は参考にしていただければと思います。

class SampleFormat < Prawn::Document
  def initialize
    super(page_size: 'A4') #新規PDF作成

    # 日本語フォントの読み込み
    font_families.update('JP' => { normal: 'app/assets/fonts/ipaexm.ttf', bold: 'app/assets/fonts/ipaexg.ttf' })
    font 'JP'

    stroke_axis #座標を表示(本番環境ではコメントアウト)
 
    #------------ タイトル ------------
    text "登山計画書 (登山届)", size: 20, align: :center
    move_down 20 # 下に20pxだけ移動

    #------------ 提出先 ------------
    submit_to = [["", "御中"]]
    table submit_to, column_widths: [150, 35] do
      cells.borders = [:bottom]
      cells.height = 30
      columns(0).size = 10
    end
    move_down 20

    #-------- 山域・山名、山行期間 --------
    info = [
      [{content: "<b>目的の山域・山名</b>", colspan: 2, inline_format: true}, {content: "", colspan: 4}],

      [{content: "<b>山行期間</b>", colspan: 2, inline_format: true}, "", "〜",{content: "", colspan: 2}],

      [{content: "役割", rowspan: 2}, {content: "氏名", rowspan: 2}, "生年月日", {content: "性別", rowspan: 2}, "現住所", "緊急連絡先(間柄)" ],
      ["年齢", "電話番号", "電話番号"]
    ]

    table info, cell_style: {height: 30},
    # widthの最大値 520
    column_widths: [30, 80, 80, 30, 200, 100] do # カラム(列)の幅を指定する
      cells.size = 10 # セル全体の文字サイズを10pxにする
      row(0).columns(2).size = 12 #1行目かつ3列目の文字サイズを12pxにする
      row(1).columns(2..4).size = 12 #2行目かつ3〜5列目の文字サイズを12pxにする
      row(0..1).height = 25 #1〜2行目のセル高さを25pxにする
      row(1).columns(2).borders = [:left, :top, :bottom] # 2行目かつ3列目の枠線表示オプション(左、上、下のみ枠線表示)
      row(1).columns(3).borders = [:top, :bottom]
      row(1).columns(4).borders = [:top, :bottom, :right]
      row(0).border_top_width = 2 # 1行目の上枠線を太くする(数値で太さ指定)
      row(0).border_bottom_width = 2 # 1行目の下枠線を太くする
      row(1).border_bottom_width = 2
      row(3).border_bottom_width = 2
      columns(0).row(0..3).border_left_width = 2
      columns(-1).row(0..3).border_right_width = 2
      row(2..3).font_style = :bold # フォントスタイルをboldにする
      # ※row(-1) columns(-1) はそれぞれ最後の行と列を表す
    end

    #-------- 登山者名簿一覧 --------
    companions = [
    [{content: "", rowspan: 2}, {content: "", rowspan: 2}, "", {content: "", rowspan: 2}, "", "" ],
      ["", "", ""],
    ] * 6 # (テーブル * 個数)で複数のテーブルを生成できる

    table companions, cell_style: {height: 30},
    # widthの最大値 520
    column_widths: [30, 80, 80, 30, 200, 100] do
      cells.size = 10
    end
    move_down 20

    #-------- ◎ 所属している山岳会・サークル --------
    club_title = [["◎", "所属している山岳会・サークル"]]
    table club_title, column_widths: [25, 210] do
      cells.borders = [:bottom]
      cells.size = 14
      cells.border_bottom_width = 1
    end
    move_down 10

    club = [
      [{content: "<b>所属:</b>", inline_format: true}, "", ""],
      [{content: "<b>団体名:</b>", inline_format: true}, "", {content: "<b>救援体制:</b> ", inline_format: true}],
      [{content: "<b>代表者:</b> ", inline_format: true}, "", {content: "<b>緊急連絡先:</b> ", inline_format: true}],
      [{content: "<b>代表者住所:</b> ", inline_format: true}, "", {content: "<b>住所:</b> ", inline_format: true}],
      [{content: "<b>代表者電話:</b> ", inline_format: true}, "", {content: "<b>電話番号:</b> ", inline_format: true}]
    ]

    table club, cell_style: {height: 25},
    column_widths: [250, 30, 240] do
      cells.size = 10
      cells.borders = [:bottom]
      cells.border_lines = [:dotted]
      row(3).height = 35
      columns(1).borders = []
      row(0).columns(-1).borders = []
    end
    move_down 20

    #------------ ◎ 行動スケジュール ------------
    schedule_title = [["◎", "行動スケジュール"]]
    table schedule_title, column_widths: [25, 125] do
      cells.borders = [:bottom]
      cells.size = 14
    end
    move_down 10

    schedule = [
      ["日程", "行動予定"],
      ["(1)", ""],
      ["(2)", ""],
      ["(3)", ""],
      ["(4)", ""],
      ["(5)", ""],
    ]

    table schedule, cell_style: {height: 30},
    column_widths: [120, 400] do
      cells.size = 10
      row(0).border_top_width = 2
      row(0).border_bottom_width = 2
      columns(0).row(0..5).border_left_width = 2
      columns(-1).row(0..5).border_right_width = 2
      row(-1).border_bottom_width = 2

    end

    escape_route = [
      ["エスケープルート\n(荒天・非常時対策)", ""]
    ]
    table escape_route, cell_style: {height: 50},
    # max_width: 520
    column_widths: [120, 400] do
      cells.size = 10
      row(0).border_top_width = 2
      row(0).border_bottom_width = 2
      columns(0).row(0).border_left_width = 2
      columns(-1).row(0).border_right_width = 2
    end
    move_down 20

    #------------ ◎ 持ち物 ------------
    belongings_title = [["◎", "持ち物"]]
    table belongings_title, column_widths: [25, 55] do
      cells.borders = [:bottom]
      cells.size = 14
    end
    move_down 10

    belongings = [
      ["基本食料", ""],
      ["非常食", ""],
      ["野営器具", ""],
      ["行動器具", ""],
      ["通信機器", ""],
      ["その他", ""]
    ]

    table belongings, cell_style: {height: 25},
    column_widths: [120, 400] do
      cells.size = 10
      row(0).border_top_width = 2
      columns(0).row(0..5).border_left_width = 2
      columns(-1).row(0..5).border_right_width = 2
      row(-1).border_bottom_width = 2
    end
    move_down 20

    #------------ ◎ 概念図 ------------
    image_title = [["◎", "概念図"]]
    table image_title, column_widths: [25, 55] do
      cells.borders = [:bottom]
      cells.size = 14
    end
    move_down 10

    bounding_box([0, cursor], width: 520, height: 220) do
      # ↓周りに枠線をつける
      transparent(1) { stroke_bounds }
    end

  end
end

上記サンプルだけではわからない実装があれば、以下のPrawnマニュアル(英語)を参考にしてみてください(英語ですが図解が多くわかりやすいです)。

Prawnマニュアル(英語)

Prawnを使って作成したアプリ

あわせて読みたい
スマホで簡単!フォーム入力するだけで登山計画書(PDF)をサクッと作成・シェアできるアプリを開発してみ... 突然ですが、皆さんは登山しますか? 「Yes」と答えたあなた、登山へ行く際は登山計画書を作成していきますか? ここ十数年の間に登山ブームの広がりとともに、登山者の...

参考資料

TechTechMedia
【Rails】PDF出力が実装できるGem「Prawn」の実装方法について簡単に解説|TechTechMedia RailsでPDF出力を実装することができるGem「Prawn」について解説しています。Web上でデータをPDF化して出力したい、PDFを商品として扱いたい、取引先がPDFを要求してきたな...
Qiita
Rails prawnを使ってPDFを作成する - Qiita はじめにRailsアプリケーションでPDFを作成する僕の中のノウハウを軽くですが公開します。Webから帳票を作成する機能って案外需要あると思うんですよねー。特に業務系のア...
Qiita
Rails5でPrawnによるPDFを作成する - Qiita はじめにRails5でPDFの作成をするのに便利なgemのPrawnの使用方法について、備忘録的にまとめておきます。どなたかの役に立てば幸いです。環境データベースはmysqlを使用ge...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。無職で暇になり始めたプログラミング(Ruby on Rails)の忘備録をまとめたブログです。最近は別にやりたいことができたのでプログラミングほぼやっていません。気が向いたらまた再開するかも。僕の日常はメインブログの方で更新しています。

コメント

コメントする

目次