🖨️

wkhtmltopdfの代わりにPrintdで解決した話

2024/08/16に公開

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