😰

Rails で PDF を作ったけど、時代遅れのやり方だったかもしれない

2024/03/21に公開

ラブグラフでエンジニアをしております横江( @yokoe24 )です!
このあいだ PDF 出力機能の実装をしました!

技術選定からその後の反省までをまとめます。

1. 技術選定

PDF出力をおこなう Rails の gem は複数あります。
まずはどの gem を使うか検討するために比較しました。
(※ Ferrum, HexaPDF, Grover は、この記事を書くにあたって見つけたものなので、当時は選定対象ではありませんでした)

gem名 スター数 main,masterへの最終コミット 最新バージョンリリース日 初回バージョンリリース日
Prawn 4.6k 2024/03/06 2024/03/05 - 2.5.0 2008/08/03
Wicked PDF 3.5k 2024/02/28 2024/02/27 - 2.8.0 2011/02/25
PDFKit 2.9k 2023/05/31 2023/05/30 - 0.8.7.3 2010/05/21
Ferrum 1.6k 2024/03/19 2024/02/17 - 0.15 2019/08/26
HexaPDF 1.2k 2024/03/20 2024/03/19 - 0.39.1 2016/10/26
Grover 805 2024/03/10 2024/03/09 - 1.1.7 2018/08/22

(表内のデータ確認日:2024/03/21)

スター数だけで言えば prawn が強いですし、歴史もあります。
しかし、コミットが現在もされているものの、最新バージョンのリリースが2020年12月から3年以上空いてしまったのは気がかりです。

wicked_pdfpdfkit は1年に1度以上は新バージョンのリリースがおこなわれているので、
この2つのどちらかが良さそうに思いました。
最終的には、README が充実していて使うまでのハードルが低そうな wicked_pdf を選びました。

1-1. WkHtmlToPDF 問題

wicked_pdfpdfkit には重大な問題があります。

これらの gem は WkHtmlToPDF という
HTML を PDF に変換する CLI ツールによって支えられているのですが、
これのメンテナンスが 2022/6/29 のコミットを最後に止まっており、
2023/1/2 にはリポジトリのアーカイブがおこなわれたのです。

Odoo によりフォークされた https://github.com/odoo/wkhtmltopdf も存在はしますが、
README にあくまで Odoo 用に調整されているとあるので、これを使うのも現実的ではないかもしれません。

その点 prawn は WkHtmlToPDF への依存がなく作られているという点では安心です。

また、まだ歴史が浅いのですが FerrumGrover という gem では
HTML レンダリングに関して Chromium を使って PDF への変換をおこなっています。
(Grover は Puppeteer をさらに経由します)
WkHtmlToPDF には依存していないので、今後これらが主流になっていく可能性があります。

このことを技術選定時にはまだ気付いていませんでした。

2. Wicked PDF の導入

2-1. インストール

wicked_pdf の導入にあたっては、
Gemfile に wicked_pdf を追加して bundle install をおこなって Gemfile.lock を更新します。

それに加えて wkhtmltopdf-binary という gem を追加すると紹介している記事もありますが、
これはあくまで WkHtmlToPDF をマシン側にインストールするための gem なので、
WkHtmlToPDF のインストールを別の手段でおこなうのであれば記載しなくても大丈夫です。

ラブグラフでは Docker を使用していますので、
Dockerfile での apt-get install の中で
fonts-noto-cjkwkhtmltopdf のインストールをするよう記述を追加することで対応しました。

fonts-noto-cjk をインストールしたのは、
PDF出力で日本語フォントを使用したいためです。

2-2. コンフィグファイルの作成

公式の README にあるように、

bundle exec rails generate wicked_pdf

をすることで
config/initializers/wicked_pdf.rb が作成されます。

デフォルトでは wkhtmltopdf-binary を使うことが想定されていますが、
今回私たちは Docker イメージ内にインストールしていますので、そのバイナリファイルの PATH を指定しています。

# config.exe_path = Gem.bin_path('wkhtmltopdf-binary', 'wkhtmltopdf')
config.exe_path = "/usr/bin/wkhtmltopdf"

また、

config.layout = "wicked_pdf.html"

とレイアウトファイルを指定した上で、
app/views/layouts/wicked_pdf.html.slim を以下のように作成しました。
(※ラブグラフでは slim と scss を使っていますが、erb などを使っている会社さんではそれ用に調整してください)

doctype 5
html lang="ja"
  head
    meta charset="utf-8"
    = wicked_pdf_stylesheet_link_tag :wicked_pdf
  body onload="number_pages"
    = yield

wicked_pdf_stylesheet_link_tag で指定しているスタイルシートとなる
app/assets/stylesheets/wicked_pdf.scss には

h1 {
  font-size: 200%;
}

h2 {
  font-size: 160%;
}

などを記し、PDFに表示される見出しのサイズがいい感じになるように調整しています。

3. PDF のレンダリング

ここまで来ればあとは公式の README 通りですのでカンタンです。

例えば UsersController に

def show
  @user = User.find(params[:id])
  
  respond_to do |format|
    format.html
    format.pdf do
      render(pdf: "user_#{@user.id}")
    end
  end
end

と書いて、 app/views/users/show.pdf.slim を記述すれば大丈夫です。
HTML と CSS を書く手法で PDF のレイアウトが作れ、
http://localhost:3000/users/1.pdf のような URL にアクセスすることで確認できます。


こんな見た目のPDFが作れます

4. これでよかったのだろうか……

今回は wicked_pdf を使って PDF を作成する方法について書きましたが、
1-1. の項で記した通り、WkHtmlToPDF に起因する問題があるので
本当に wicked_pdf を使うかはよく考える必要があります

wicked_pdf のコミッターである unixmonkey さんは、
wicked_pdf が、WkHtmlToPDF でない他のなにかのラッパーとなるような予定はなく、
もしそのなにかのラッパーを作るなら新しい gem を作るであろうと述べています。

https://github.com/mileszs/wicked_pdf/issues/1081

このことを踏まえると、いま Rails で PDF を作成するには
以下の選択肢および懸念 が考えられます。

  1. Wicked PDFPDFKit を使う。
    WkHtmlToPDF の問題はあるので、どこかのタイミングで動かなくなるかもしれないし、ブラウザエンジンは2015年で止まっているので flexbox などは使えません。ユーザー作成コンテンツを PDF 化するような機能であれば、コマンドインジェクションなどのセキュリティ懸念もありうるでしょう
  2. HexaPDFPrawn を使う。
    HTML/CSS から作る手段に比べてレイアウト作成の難易度は上がります。Prawn よりは HexaPDF のほうが、今でも高い頻度でメンテナンスされているのでおすすめです
  3. Grover を使う。
    Chromium コンテナを用意する必要に加え Node.js のインストールなど、導入はやや手間です。また、Node.js を使うぶん、全体のメモリ消費量は他の手法より高い可能性があります
  4. Ferrum を使う。
    若いライブラリではありますが、現在では最良の選択肢かもしれません。Chromium コンテナを用意する必要があるぶん、導入は少し手間ですが、Grover と異なり、CDP(Chrome DevTools Protocol)に Ruby からアクセスする機構のため、Node.js は不要となっています。
    ただし v1 を迎えていないのが個人的に気になります……
  5. 他の言語を使う。
    複雑にはなりますが、Python のライブラリである WeasyPrint を AWS Lambda などで動かし PDF を生成するといった方法も考えられます

この記事を書くまでは Wicked PDF の問題点について知らなかったのですが、
記事を書くための調査を重ねるにつれて
「Ferrum での実装に変更し直さねばー!」
という気持ちがどんどん強くなっていった横江なのでした🧎

ラブグラフのエンジニアブログ

Discussion