RailsのPDF生成をwicked_pdfからferrumに移行する
wicked_pdfで実装されていたPDF出力機能をferrumに書き換えるという作業をしたので思い出しつつメモ
サンプルアプリはこちら
移行の理由
RailsでPDF生成する方法を調べるといまだ上位に出てきたりするwicked_pdf
ですが、
PDF生成に使っているwkhtmltopdf
がブラウザエンジン(QtWebKit
)の更新停滞に伴って更新されなくなり、アーカイブされています。
また、wkhtmltopdfのバイナリをインストールするためのgemであるwkhtmltopdf-binary
も更新が滞っているので環境によっては他の方法でインストールするなりする必要があります。
今後のメンテナンスコストや古いQtWebKitを使い続けるセキュリティリスクを考えて他の手段に乗り換えることなりました。
移行先の選定
既存のviewファイルを再利用するためにHTML2PDF的なものであることはマストだったので、ヘッドレスChromeでレンダリングする方向で考えることにしました。
ヘッドレスChromeのクライアントライブラリは色々ありますが、Rubyだと直接Chromeを操作するFerrumか、PuppeteerのラッパーであるGroverの二択になるかなと思います。
Groverの方がリリースが早かったためか日本語圏の情報が多かったり、社内で利用実績があったりしたのでGroverにしようかと考えていたのですが、
触ってみるとFerrumの方がPuppeteerのようなAPIでChromeDevToolsProtocolとの対応がわかりやすく感じたのと、Node.jsランタイムが不要で面倒を見なければならないものが減るのでFerrumを使うことにしました。
既存のPDF出力機能
def wicked_pdf
@users = DummyUser.all
render pdf: 'WickedPdfサンプル', template: 'pdf/index', layout: 'pdf'
end
上のようなactionがあり、
<!DOCTYPE html>
<html>
<head>
<title>FerrumSample</title>
<meta charset="UTF-8">
<%= wicked_pdf_stylesheet_link_tag "pdf" %>
</head>
<body>
<%= yield %>
</body>
</html>
/*
*= require destyle.css
*/
table {
width: 100%;
border-collapse: collapse;
border: 1px solid #ddd;
}
th, td {
padding: 8px;
text-align: left;
border: 0;
border-bottom: 1px solid #ddd;
}
@media screen {
tr:nth-child(odd) {
background-color: #f2f2f2;
}
}
<table>
<thead>
<tr>
<th>Name</th>
<th>ID</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= user.name %></td>
<td><%= user.id %></td>
<td><%= user.email %></td>
</tr>
<% end %>
</tbody>
</table>
これらのview+cssをwicked_pdfを使って以下のようなPDFとして出力できています。
これと同等のPDFをferrumで出力することを目的にしていきます。
ferrum導入
まずchromiumをインストールしておきます。
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y [略] chromium
Gemfileにferrum
を追加してbundle install
gem 'wkhtmltopdf-binary-ng'
+ gem 'ferrum'
ferrumでPDF出力するアクションを実装
wicked_pdfに寄せる調整も込みでの実装が以下になります。
def ferrum
@users = DummyUser.all
pdf = generate_pdf(template: 'pdf/index')
send_data pdf, filename: "Ferrumサンプル.pdf", type: "application/pdf", disposition: "inline"
end
private
def generate_pdf(layout: 'pdf', template: nil, **pdf_options)
# viewファイルをHTMLに変換
html_content = render_to_string(layout:, template:, formats: :html)
# HTMLをPDFに変換
html_to_pdf(html_content, **pdf_options)
end
def html_to_pdf(html_content, format: :A4, encoding: :binary, scale: 1.0, print_background: true)
browser = Ferrum::Browser.new(browser_options: { "no-sandbox": nil }, js_errors: true)
page = browser.create_page
# Ferrum::PageにHTMLをセット
page.content = html_content
# メディアタイプをscreenに設定
page.command("Emulation.setEmulatedMedia", media: "screen")
# PDFを生成
page.pdf(
format:,
encoding:,
scale: scale * 0.8,
print_background:,
)
ensure
browser&.quit
end
後述しますがメディアタイプ周りの調整のためにCSSも一部修正しました
viewファイルはそのままです。viewでwicked_pdf_stylesheet_link_tag
というwicked_pdf用helperを使っていますが、これに限らずwicked_pdfのviewヘルパーはアセットをインライン化するだけのものなので現状ちゃんと動いているなら移植したり流用したりしていいんじゃないかと思ってます。
出力はこのようになります。
大体同じにはできているかなと思います。
スタイルの調整について
ferrumで特に指定せず出力すると以下のようにスタイルが崩れるので調整します。
ここでは今回移行する上で対応した部分について書いていきます。
文字スケール
まずページ数が変わるレベルで文字サイズが大きくなることですが、
wicked_pdfの文字スケールはferrumの約0.8倍[1]なのでのでPDF生成時に調整します。
# PDFを生成
page.pdf(
format:,
encoding:,
scale: scale * 0.8, # ここ
print_background: true
)
メディアタイプ
次にPDF生成時のメディアタイプはwicked_pdfではデフォルトでscreenとなっていて、ferrumではprintになります。そのためferrumではapp/assets/stylesheets/pdf.css
の背景色が効いていません。
CSSをうまく調整できるならそれに越したことはないですがフレームワーク内の定義とかだとしんどいと思います。
そこで、メディアタイプをscreenにエミュレートします。
page.command("Emulation.setEmulatedMedia", media: "screen")
これで解決という着地をさせようとしていたんですが、screenにエミュレートするとページを跨いだtableのヘッダーが複製されないことに記事を書きながら気がつきました...。
chromiumのuser agent stylesheetによるものなので、screenでもprint向けスタイルが当たるように追加しました
/* 追加 */
@page {
/* FIXME: Define the right default values for page properties. */
size: auto;
padding: 0px;
border-width: 0px;
}
/* Allows table headers and footers to print at the top / bottom of each
page. */
thead { break-inside: avoid; }
tfoot { break-inside: avoid; }
フォント
特にサンプルアプリでは扱ってませんが、QtWebKitとChromeではフォントの優先度の決め方が異なるようで、書体のみの指定だとフォントが変わることがあります。フォントまで指定した方が良いです。
参考
-
CMオペレーターの総積から計算しています。他の環境やサイズでも比は同じだったので変わらないはず...多分 ↩︎
Discussion