ぶちのブログ

競プロとCTFが趣味なWebエンジニアのアウトプットの場

Ruby on Railsでpdfを生成して出力するときのバットプラクティスまとめ

概要

Railsでpdfを出力しようとして、様々な方法を試したが、ほとんどが失敗に終わったので、どのように失敗したのかをまとめます。(最終的に成功した方法も書いてあります)

対象読者

特に、Railsアプリでpdfを出力する予定があり、罠を予め知っておきたい方、もしくはpdf出力で詰まった経験がある方。

環境

Ruby 2.6.0, Rails 5.2.2, 最終的にはAmazon Linux 2にデプロイ

やりたかったこと

Railsで、Modelのインスタンスからデータを適当に埋めて帳票を作成し、pdfを出力したい。最終的にユーザーにはpdfのみを見せるので、最後がpdfならば何をしても問題ないが、出来るだけ保守性を高くしたい。

なお、帳票のテンプレートは予めpdfファイルで与えられていました。テンプレートの例のごく一部を以下に示します。(実際にはこれの何倍も複雑な表組みになっております) f:id:betit0919:20190225222421p:plain

思い浮かんだ方法とその結果のまとめ

  1. spread sheetでテンプレートを作成し、Google Sheets APIを用いて値の変更をし、pdfを出力する
  2. Adobe Acrobat等のソフトで雛形のpdfにフォームを埋め込み、pdftk(及びそのラッパー)もしくはpdfrw(Pythonのライブラリ)を用いてそこに値を埋め込む
  3. Excelでテンプレートを作成し、RubyXLで値を埋め込み、LibreOfficeコマンドラインツールを用いてpdfに変換する
  4. HTMLでテンプレートを作成し、wickedpdfを用いてpdfに変換する
  5. prownpdfで一からpdfを生成する

1に関しては、ほぼ成功しましたが、現状ドキュメント等を調べた限りでは、pdfのサイズや向きを指定することができず、要件を満たしませんでした。A4で縦向きでしか出力しないのであれば問題なく実現できます。

2に関しては、最終的にフォームを編集できない形にする(flattenにするか編集不可フラグを立てる)という要件が満たせませんでした。フォームの編集が可能な状態ならば実現自体はできますが、フォントが崩れたり、開くソフトや環境によってはクラッシュする時などもあり、非常に不安定でした。安定させる方法はありますが、サーバーサイドで処理する方法は見つかりませんでした。(マルチバイト文字を用いなければマシになるかもしれません)

3に関しては、エクセル内の計算式が、Amazon Linux 2上でコマンドを叩いたときには再計算されず、数式や参照を埋め込めず、保守性が低下しそうでした。(ローカルのMacOSでは再計算できたので、もう少し頑張れば実現できる可能性はあります)

4が最終的に採用した手法です。HTMLでのテンプレートの作成がやや大変ですし、Assets周りでもハマりましたが、web上にも知見が多かったため解決できました。

5に関してはテンプレートの作成が大変すぎると判断し、見送りました。(4で上手くいったので試してもいません)

それぞれの手法の詳細

Google Sheets API

Sheet APIを叩くまではそれほど苦労しませんでした。テンプレートに直接埋め込むのではなく、別のシートに情報欄を作って、テンプレート内では情報欄の値を参照するようにすると保守しやすいと考えていました。

しかしA4縦でしか出力することができませんでした。今回はA3だったり横向きで出力する必要がある部分もあったので、没となりました。A4縦で良いのであれば、印刷範囲を予め設定することはできたと思います。(記憶が曖昧なので、間違っていたら申し訳ございません)

PDF Formを用いる方法

pdftkを用いる方法

個人的には最も有力視していた方法ですが、罠もめちゃくちゃ多い上に、最終的にもかなり微妙な結果しか得られませんでした。特にAdobeAcrobatとの相性がかなり悪い気がします。ラッパーとしてはpdf-formsが使いやすかったです。

罠については、

  1. Amazon Linux 2にpdftkを入れるのが少し大変(こちらのサイトを参考にすればインストールできました)
  2. Amazon Linux 2にフォームを設定しないと上手く埋め込めない
  3. そもそも、埋め込んだはずの文字がクリックするまで表示されないことがある(原因不明)
  4. flattenオプションとマルチバイト文字の両方を用いることができない(ライブラリの制限?)

という感じです。3,4が致命的なのですが、こちらのissueを見る限りは実現できるのかなと思いつつ、全く再現ができませんでした。

pdfrwを用いる方法

pdfrwを用いる方法の方がまだ上手くいきました。こちらのサイトが参考になります。

  1. クリックするまで文字が表示されない問題
  2. そもそもflattenオプションや編集不可オプションが見当たらない

1については以下のようなコードをかくと、解決できました。(理屈はよくわかっていません)

annotation.update(
    pdfrw.PdfDict(AP='')
)
annotation.update(
    pdfrw.PdfDict(V='{}'.format(data_dict[key]))
)

今回は編集不可にしたかったのですが、その点は上手くいきませんでした。

サーバーサイドでなければ実現できた方法

pdfrwを用いてフォームを埋めたpdfファイルを、pdftk、もしくはImageMagick(GhostScript)などで変換しようと考えましたが、それらのソフトではpdfrwで生成したpdfを読み込めませんでした。(APオプションを外すと読めるようにはなるが、当然文字が表示されない問題が再発します)

そこで、pdfrwで生成したファイルをMacOSのプレビューで開き、何らかの変更を加えて保存すると、見た目には全く変わらないファイルなのに変換ができるようになります。この辺りに鍵がある気もしましたが、サーバーサイドではそのような操作を挟めないので没となりました。

また、LibreOfficeで作った場合、AdobeAcrobatで作った場合、それらをMacOSのプレビューで開いたり編集した場合、pdfrwを使った場合など、見た目が同じでもバイナリの中身や挙動が異なるpdfファイルが色々あります。再現できる気がしませんでした。

RubyXLとLibreOfficeを用いる方法

GoogleSpreadSheetを使うのとほぼ同じ感覚です。Amazon Linux 2はLibreOfficeもサポートされていますし、RubyXLについても比較的情報が多いです。

MacOSではsofficeコマンドの--headlessオプションを用いるとやりたかったことが完全に実現できましたが、Amazon Linux 2だと計算式を再計算してくれず、情報用シートを参照する方法が使えなかったので、保守性を下げてRails側にカラムの位置情報を持たせるしかなくなり、没となりました。

WickedPdfを使う方法

最終的にはこの方法を使いました。今回のRailsアプリではWebpackerを使っていて、asset precompileを使っていなかったので、wicked_pdf_stylesheet_link_tagや`wicked_pdf_image_tag'などのヘルパーメソッドが使えませんでした。画像を予めS3にアップし直接URLを埋め込む方法と、HTML内にStyleタグを書いていく方法でお茶を濁して使っています。

wkhtmltopdf自体がflexboxに対応していないので、レイアウトの調整はややしんどいです。また、wkhtmltopdf-binaryを使う場合は、ver.0.12.4ではMacOSだとフォントのサイズが変わってしまうという問題があるので、ver.0.12.3.1を使いましょう。

prawnpdfを用いる方法

複雑な表組みを作るのは非常に骨が折れそうです。テンプレートさえ作れれば、一番綺麗にできるという理屈もわかりますが、現実的な工数では不可能だと考えました。

結論

色々試したけど、みんなやってるWickedPdfが安全安心です。A4の縦だけでいいのならば、GoogleSpreadSheetは有力です。それ以外の方法は、環境に依存する部分があったりして、闇が深かったです。