wkhtmltopdfの代わりにPrintdで解決した話
2023年にwkhtmltopdfの開発が停止した。とはいえ動作はしていたから、いくつか問題を抱えながらもどうするかを後回しにしていた。その後たまたま知ったPrintdで解決したという話。
Printdはウェブページの特定の要素を印刷することができる。デモはわかりやすいけど、ページの一部を印刷している様子がわかりにくいかも。
背景
そもそものPDFを出力する理由は、最終的に紙に印刷するから。だからユーザーはPDFに限らず、印刷できればなんでもOK。
業務フローの観点からPrintdで印刷ダイアログを出すのは問題なく、むしろ好ましい動作。これによりwkhtmltopdfを使ってサーバーでPDFを出力する必要がなくなり、Printdに置き換えた。
やり方は簡単で、ユーザーに表示する画面にPDFの内容も描画して、イベントでPrintdを呼び出すだけ。とはいえPDFの内容を表示しない方がいいケースもあるのでCSSで非表示にした。
PDFの内容を含んだ長いページを描画するコストは低くない。だけど、サーバー内でHTMLを描画して古いブラウザエンジンでPDFを作って複数ファイルをzipに固めてメールで送ることに比べたら十分ペイするという見積もりでやった。
Printdじゃなくて、別タブで印刷したい内容を表示し、ユーザー自身に印刷ダイアログを出させる方法も考えたけど、ユーザーの手間が増えるのは想像に難くない。
色々雑感
サーバーの負荷が減った
サーバーでHTMLを描画する点は変わらないけど、wkhtmltopdfの内部でQTWebkitを動かす必要がなくなったので、負荷は減ったと思う。
依存するライブラリが減った
- wkhtmltopdf
- Wicked PDF
- Railsからこのライブラリ経由で使っていた
- autoprefixer
- QTWebkit向けのCSSを出力していた
- IPAフォント
- 日本語表示のために使っていた
- rubyzip
- 複数のPDFファイルをzipでまとめてメールに添付していた
環境による違いがなくなった
開発環境(Docker)と本番環境で出力されるPDFに差があり、本番で試さないと結果がわからないという状況が解消された。ブラウザによる差はあるかもしれないけど、大幅にずれることはなく、CSSで調整できる。これがwkhtmltopdfをやめる一番の理由だった。
デバッグが楽
ブラウザに描画された内容がPDFになるから、何か変更したいときにはブラウザの開発者ツールでHTMLやCSSを編集して確認できる。デバッグがかなり楽になった。
page-break-after
以前は異なるPDFを別ファイルとして出力していたけど、それは難しかったので、一枚の複数ページPDFとして出力することになった。そのため、印刷用CSSでpage-break-after: alwaysをページ区切りとして使っている。
印刷用スタイルシート
印刷用のスタイルシートを別途指定する必要があり、少し手間があった。PrintdにはCSSのルールを文字列として渡す必要がある。現在もWebpackerを使っているのでraw-loaderでCSSファイルの内容を文字列として渡している。また、ブラウザで表示するために同じスタイルシートも読み込んでいる。
ブラウザでPDFの内容を表示したりデバッグしない限り、後者のスタイルシートの読み込みは不要だけど、そうはいかないこともある。
近いうちにvite-railsに移行する予定で、raw-loaderの代わりに?inlineが使えそうだ。
以下はそのコード。stimulusを使っているが、ボタンをクリックしたら特定領域を印刷ダイアログに表示する。
import { Controller } from 'stimulus';
import { Printd } from 'printd';
import commonStyle from '!!raw-loader!../../styles/print/common.css';
import jobticketStyle from '!!raw-loader!../../styles/print/jobticket.css';
import '../../styles/print/common.css';
import '../../styles/print/jobticket.css';
export default class extends Controller {
print(event) {
event.preventDefault();
const d = new Printd({
parent: document.getElementById('parent-printable'),
});
d.print(document.getElementById('printable'), [
commonStyle,
jobticketStyle,
]);
}
}
Discussion