現在Railsで開発中のアプリにPDF出力機能を実装したい…
そこで、RailsでPDFを生成するためのgem(ライブラリ)について色々とググってみた結果、カスタマイズ性に優れたgem「Prawn」というgemを採用することにしました。
「Prawn」は開発コストはかかるものの、
- レイアウトの細かい調整などカスタマイズ性に優れている
- Rubyで書いて出力するのでRailsエンジニアにとって扱いやすい
などのメリットがあります。
当記事ではそのPrawnの使い方や、実際にPrawnを使って出力したPDFのサンプルをご紹介します。
Railsで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出力のサンプルとして登山計画書を作ってみました。(一応、趣味が登山なので)
サンプルはこちら↓
- heroku経由で表示 → https://tozan-plan.herokuapp.com/sample_format.pdf
- 直ダウンロード → [wpdm_package id=’4131′]
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マニュアル(英語)
- 通常マニュアル → https://prawnpdf.org/manual.pdf
- table用マニュアル → https://prawnpdf.org/prawn-table-manual.pdf
コメント