prawnで請求書っぽいPDFを作る

本記事は[iCARE Advent Calendar 2020](https://qiita.com/advent-calendar/2020/icare) の5日目です。

# はじめに

本記事では[prawn](https://github.com/prawnpdf/prawn)を用いた請求書っぽいPDFの作成方法を解説します。

RailsでPDF作成する場合だと[wicked_pdf](https://github.com/mileszs/wicked_pdf)も使えますが、wicked_pdfがhtmlライクにPDF作成ができるのに対して、prawnはゴリゴリの[DSL](http://e-words.jp/w/DSL.html)なので、学習コストがかかりますが、wicked_pdfよりも柔軟にPDFを作ることができるので、個人的にはprawnの方が好きだったりします。

## セットアップ

今回の請求書PDFではテーブル表示を行いたいので、prawn-tableを使います。

```ruby
# Gemfile

gem 'prawn'
gem 'prawn-table'
```

でbundle install。

```terminal
your_app $ bundle install
```

## 主要なメソッドの紹介

請求書の作成の前に、今回用いる主要なメソッドを紹介したいと思います。

### Prawn::Document#generate

https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-c-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等)や余白(上下左右)を指定することが出来ます。

```ruby
# 例

Prawn::Document.generate(
  'sample.pdf',
  page_size: 'A4',
  top_margin: 35,
  bottom_margin: 35,
  left_margin: 35,
  right_margin: 35
) do |pdf|
  # 処理
end
```

[Gem内の実装箇所はこちら](https://github.com/prawnpdf/prawn/blob/master/lib/prawn/document.rb)

### Prawn::Document#bounding_box

https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-i-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内にボックス要素を作成するのに用いるメソッドですね。

```ruby
# 例

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を作成して表示したりできます。

[Gem内の実装箇所はこちら](https://github.com/prawnpdf/prawn/blob/master/lib/prawn/document/bounding_box.rb)


### Prawn::Text#text

https://prawnpdf.org/docs/0.11.1/Prawn/Text.html#method-i-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内に記述したい時に使うメソッドになります。

```ruby
# 例

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

引用

https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-i-move_down

> Moves down the document by n points relative to the current position inside the current bounding box.

> 現在のbounding_box内の現在の位置を基準にして、ドキュメントをnポイント下に移動します。

ドキュメントを下に移動したい時に用いるメソッドです。

```ruby
# 例

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
```

テキストとテキストの間を空けたいときなんかに使います。

[Gem内の実装箇所はこちら](https://github.com/prawnpdf/prawn/blob/master/lib/prawn/document.rb#L470)


### 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:

```ruby
pdf.table(data) do |table|
  table.rows(1..3).width = 72
end
```

表形式でデータを表示したい場合に使えるメソッドになります。

```ruby
# 例

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
```

[Gem内の実装箇所はこちら](https://github.com/prawnpdf/prawn-table/blob/master/lib/prawn/table.rb#L110)

# 請求書を作成する

お待たせしました。今まで紹介したメソッドを用いて、
簡単な請求書めいたPDFを作成したいと思います。

## invoice_pdf_exporter.rbの作成

app/services下にinvoice_pdf_exporter.rbを作成し、
以下のコードを記述します。

```ruby
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

https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-i-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.

> 引数がない場合、これは現在選択されているフォントを返します。 それ以外の場合は、現在のフォントを設定します。 ブロックが使用されると、フォントはトランザクションで適用され、ブロックが終了するとロールバックされます。

[Gem内の実装箇所はこちら](https://github.com/prawnpdf/prawn/blob/master/lib/prawn/font.rb#L48)

ドキュメントに記載がありませんが、fontの指定をしないで実行すると、
```Prawn::Errors::IncompatibleStringEncoding```
の例外が発生します。

サンプルコードではpublicディレクトリに配置していますが、app/assets/fontsでもいいと思います。

フォントファイルは、
https://fontfree.me/
に無料フォントがありますので、お好きなものをお使いください。

今回は[ほのか明朝](http://font.gloomy.jp/honoka-mincho-dl.html)を使います。

## create_contentsメソッドを実装する

では具体的な処理を記述します。

コード全晒しです。

```ruby
  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を作成することができます。

あとはコンソール上で実行してやりましょう。

```terminal
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

>
ドキュメント内の新しいページを作成して進みます。
> 
ページサイズ、余白、およびレイアウトは、生成時に設定することもできます。
新しいページ。 これらの値は、ページ作成の新しいデフォルトになります

次のページを作成するメソッドです。

```rb
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内に写真を貼るメソッドです。横縦の幅や寄せる位置などを指定できます。

```rb
 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   
```

# まとめ

今回紹介したメソッドの他にも様々な機能がありますので、
詳しく知りたい方はドキュメントをご覧ください！

https://prawnpdf.org/manual.pdf

# iCAREテックブログもよろしくね！！
https://www.icare.jpn.com/dev_cat/