本記事はiCARE Advent Calendar 2020 の5日目です。
はじめに
本記事ではprawnを用いた請求書っぽいPDFの作成方法を解説します。
RailsでPDF作成する場合だとwicked_pdfも使えますが、wicked_pdfがhtmlライクにPDF作成ができるのに対して、prawnはゴリゴリのDSLなので、学習コストがかかりますが、wicked_pdfよりも柔軟にPDFを作ることができるので、個人的にはprawnの方が好きだったりします。
セットアップ
今回の請求書PDFではテーブル表示を行いたいので、prawn-tableを使います。
# Gemfile
gem 'prawn'
gem 'prawn-table'
でbundle install。
your_app $ bundle install
主要なメソッドの紹介
請求書の作成の前に、今回用いる主要なメソッドを紹介したいと思います。
Prawn::Document#generate
公式の説明文を引用します。
Creates and renders a PDF document.
When using the implicit block form, Prawn will evaluate the block within an instance of Prawn::Document, simplifying your syntax. However, please note that you will not be able to reference variables from the enclosing scope within this block.
PDFドキュメントを作成してレンダリングします。
暗黙的なブロックフォームを使用する場合、PrawnはPrawn :: Documentのインスタンス内のブロックを評価し、構文を簡素化します。 ただし、このブロック内の囲んでいるスコープから変数を参照することはできないことに注意してください。
PDFドキュメントの雛形を作成するメソッドですね。
オプションとして、ページサイズ(A4, B5等)や余白(上下左右)を指定することが出来ます。
# 例
Prawn::Document.generate(
'sample.pdf',
page_size: 'A4',
top_margin: 35,
bottom_margin: 35,
left_margin: 35,
right_margin: 35
) do |pdf|
# 処理
end
Prawn::Document#bounding_box
こちらも公式を引用
A bounding box serves two important purposes:
・ Provide bounds for flowing text, starting at a given point
・ Translate the origin (0,0) for graphics primitives
A point and :width must be provided. :height is optional. (See stretchyness below)
bounding_box には、次の2つの重要な目的があります。
・ 特定のポイントから開始して、流れるテキストの境界を指定します
・ グラフィックスプリミティブの原点(0,0)を変換します
ポイントと:widthを指定する必要があります。 :heightはオプションです。
PDF内にボックス要素を作成するのに用いるメソッドですね。
# 例
Prawn::Document.generate(
'sample.pdf',
page_size: 'A4',
top_margin: 35,
bottom_margin: 35,
left_margin: 35,
right_margin: 35
) do |pdf|
pdf.bounding_box([50, 75], width: 200, height: 300) do
# 処理
end
end
後ほど紹介しますが、テキストを書き込んだり、tableを作成して表示したりできます。
Prawn::Text#text
公式引用
If you want text to flow onto a new page or between columns, this is the method
to use. If, instead, if you want to place bounded text outside of the flow of a ?document (for captions, labels, charts, etc.), use Text::Box or its convenience method text_box.
Draws text on the page. Prawn attempts to wrap the text to fit within your current bounding box (or margin_box if no bounding box is being used). Text will flow onto the next page when it reaches the bottom of the bounding box. Text wrap in Prawn does not re-flow linebreaks, so if you want fully automated text wrapping, be sure to remove newlines before attempting to draw your string.
テキストを新しいページまたは列間で流したい場合は、これが使用する方法です。 代わりに、ドキュメントのフローの外側に境界付きテキストを配置する場合(キャプション、ラベル、グラフなど)、Text :: Boxまたはその便利なメソッドtext_boxを使用します。
ページにテキストを描画します。 Prawnは、現在のバウンディングボックス(またはバウンディングボックスが使用されていない場合はmargin_box)内に収まるようにテキストを折り返そうとします。 テキストは、バウンディングボックスの下部に到達すると、次のページに流れます。 Prawnでのテキストの折り返しは改行をリフローしないため、完全に自動化されたテキストの折り返しが必要な場合は、文字列を描画する前に必ず改行を削除してください。
小難しいことが書いてありますが、要は文字をPDF内に記述したい時に使うメソッドになります。
# 例
Prawn::Document.generate(
'sample.pdf',
page_size: 'A4',
top_margin: 35,
bottom_margin: 35,
left_margin: 35,
right_margin: 35
) do |pdf|
pdf.bounding_box([50, 75], width: 200, height: 300) do
pdf.text 'サンプルだよ'
pdf.text 'サンプル 左寄りだよ', align: :left
pdf.text 'サンプル 右寄りだよ', align: :right
pdf.text 'サンプル 文字大きいよ', size: 30
end
end
例のように、文字の開始位置を決められたり、文字サイズを変更したりすることができます。
Prawn::Document#move_down
引用
Moves down the document by n points relative to the current position inside the current bounding box.
現在のbounding_box内の現在の位置を基準にして、ドキュメントをnポイント下に移動します。
ドキュメントを下に移動したい時に用いるメソッドです。
# 例
Prawn::Document.generate(
'sample.pdf',
page_size: 'A4',
top_margin: 35,
bottom_margin: 35,
left_margin: 35,
right_margin: 35
) do |pdf|
pdf.bounding_box([50, 75], width: 200, height: 300) do
pdf.text 'サンプルだよ'
pdf.move_down(5)
pdf.text 'サンプル 左寄りだよ', align: :left
pdf.move_down(10)
pdf.text 'サンプル 右寄りだよ', align: :right
pdf.move_down(20)
pdf.text 'サンプル 文字大きいよ', size: 30
end
end
テキストとテキストの間を空けたいときなんかに使います。
Prawn::Table#table
Quote from the official Doc
If a block is passed to methods that initialize a table (Prawn::Table.new, Prawn::Document#table, Prawn::Document#make_table), it will be called after cell setup but before layout. This is a very flexible way to specify styling and layout constraints. This code sets up a table where the second through the fourth rows (1-3, indexed from 0) are each one inch (72 pt) wide:
pdf.table(data) do |table|
table.rows(1..3).width = 72
end
表形式でデータを表示したい場合に使えるメソッドになります。
# 例
Prawn::Document.generate(
'sample.pdf',
page_size: 'A4',
top_margin: 35,
bottom_margin: 35,
left_margin: 35,
right_margin: 35
) do |pdf|
pdf.table(
[['小計', "11000円"], ['消費税', "1000円"], ['合計金額', "11000円"]],
column_widths: [50, 100],
position: :right
) do |table|
table.cells.size = 10
end
end
請求書を作成する
お待たせしました。今まで紹介したメソッドを用いて、
簡単な請求書めいたPDFを作成したいと思います。
invoice_pdf_exporter.rbの作成
app/services下にinvoice_pdf_exporter.rbを作成し、
以下のコードを記述します。
require 'prawn'
class InvoicePdfExporter
FONT_PATH = Rails.root + 'public/fonts/任意のフォントファイル.ttf'
def initialize
# Prawnドキュメントを生成
# ページサイズやマージンを指定
Prawn::Document.generate(
Rails.root + 'invoice.pdf',
page_size: 'A4',
top_margin: 35,
bottom_margin: 35,
left_margin: 35,
right_margin: 35
) do |pdf|
# フォントを指定しないと Prawn::Errors::IncompatibleStringEncoding 例外が発生する
pdf.font FONT_PATH
# 本文の生成
self.create_contents pdf
end
end
end
fontメソッドは初出なので公式Docから引用します。
Prawn::Document#font
Without arguments, this returns the currently selected font. Otherwise, it sets the current font. When a block is used, the font is applied transactionally and is rolled back when the block exits.
引数がない場合、これは現在選択されているフォントを返します。 それ以外の場合は、現在のフォントを設定します。 ブロックが使用されると、フォントはトランザクションで適用され、ブロックが終了するとロールバックされます。
ドキュメントに記載がありませんが、fontの指定をしないで実行すると、
Prawn::Errors::IncompatibleStringEncoding
の例外が発生します。
サンプルコードではpublicディレクトリに配置していますが、app/assets/fontsでもいいと思います。
フォントファイルは、
https://fontfree.me/
に無料フォントがありますので、お好きなものをお使いください。
今回はほのか明朝を使います。
create_contentsメソッドを実装する
では具体的な処理を記述します。
コード全晒しです。
def create_contents(doc)
doc.text '請求書', size: 20, align: :center
doc.bounding_box([0, 555], width: 310, height: 65) do
doc.move_down 10
doc.text "合計金額 11,000円", size: 16, align: :left
end
doc.bounding_box([320, 555], width: 310, height: 65) do
doc.text "日付: 2020年10月01日", size: 12, align: :left
end
rows = [['詳細', '数量', '単価', '金額'], ['雑費', '1', '10000', '10000']]
# tableメソッドでテーブルを生成する
# rowsは多重配列
# 多重配列でない場合 Prawn::Errors::InvalidTableData 例外が発生する
doc.table(rows, column_widths: [370, 30, 60, 60], position: :center) do |table|
# セルのサイズの指定
table.cells.size = 10
# 1行目のalignを真ん中寄せにしている
table.row(0).align = :center
end
doc.bounding_box([373, 300], width: 150, height: 100) do
doc.table [['小計', "11000円"], ['消費税', "1000円"], ['合計金額', "11000円"]], column_widths: [50, 100], position: :right do |table|
table.cells.size = 10
end
end
end
サンプルコードなので金額や配列の中身をベタ書きにしていますが、
initializeメソッドの引数にデータを渡してやれば、動的なPDFを作成することができます。
あとはコンソール上で実行してやりましょう。
your_app $ rails c
$ InvoicePdfExporter.new
紹介していないけど便利なメソッド
Prawn::Document#start_new_page
Creates and advances to a new page in the document.
Page size, margins, and layout can also be set when generating a
new page. These values will become the new defaults for page creation
ドキュメント内の新しいページを作成して進みます。
ページサイズ、余白、およびレイアウトは、生成時に設定することもできます。
新しいページ。 これらの値は、ページ作成の新しいデフォルトになります
次のページを作成するメソッドです。
Prawn::Document.generate(
'sample.pdf',
page_size: 'A4',
top_margin: 35,
bottom_margin: 35,
left_margin: 35,
right_margin: 35
) do |pdf|
pdf.table(
[['小計', "11000円"], ['消費税', "1000円"], ['合計金額', "11000円"]],
column_widths: [50, 100],
position: :right
) do |table|
table.cells.size = 10
end
pdf.start_new_page
pdf.text '次のページですよ'
end
Prawn::Image#image
Add the image at filename to the current page. Currently only
JPG and PNG files are supported. (Note that processing PNG
images with alpha channels can be processor and memory intensive.)
ファイル名の画像を現在のページに追加します。
JPGおよびPNGファイルがサポートされています。 (PNGの処理に注意してください
アルファチャネルを備えた画像は、プロセッサとメモリを大量に消費する可能性があります。)
PDF内に写真を貼るメソッドです。横縦の幅や寄せる位置などを指定できます。
Prawn::Document.generate("image2.pdf", :page_layout => :landscape) do
pigs = "#{Prawn::BASEDIR}/data/images/pigs.jpg"
image pigs, :at => [50,450], :width => 450
dice = "#{Prawn::BASEDIR}/data/images/dice.png"
image dice, :at => [50, 450], :scale => 0.75
end
まとめ
今回紹介したメソッドの他にも様々な機能がありますので、
詳しく知りたい方はドキュメントをご覧ください!
iCAREテックブログもよろしくね!!