sghtmltopdfのアップデートとcrates.ioへの公開

先月 wkhtmltopdf / wicked_pdf の後継を目指して、sghtmltopdf を公開した。 waka.hatenablog.com

Redditで宣伝してみたり、Ruby Weekly に載ってから issue や PR が一気に増えた。
実際の Rails アプリで使ってみた人たちから「このHTMLがこう崩れる」という報告がたくさん来てめっちゃ嬉しい。

Rust と Ruby 以外の別言語の binding を作ってくれた人もいてありがたい。
Python: https://github.com/nakagami/pysghtmltopdf
Elixir: https://github.com/jtippett/ex_htmltopdf

というわけで、この1カ月ちょいで v0.1.0 から v0.5.1 にバージョンが上がったので、アップデートしたものをまとめる。
細かい変更は CHANGELOG に全て載ってる。

Tailwind CSS v4 がちゃんと使えるようになった

いちばん多かった報告がこれ。 Tailwind v4 でビルドした CSS を食わせると、ほぼスタイルなしの PDF が出てしまっていた。

原因を追っていくと、v4 は比較的新しい CSS の仕様をまとめて使っていて、当時まだ対応できていなかった。

  • 出力全体が @layer で包まれていて、ブロックごと捨てていた
  • px-* や mx-auto が padding-inline や margin-inline といった論理プロパティで出てくる
  • space-y-* が calc() の中に calc() を入れ子にしている

これらに加えて、CSS Nesting、:has() / :is() / :where()、color-mix() にも対応した。
@layer は優先順位の制御までは実装しておらず、中身をソース順に展開しているだけ。
印刷用の CSS はだいたい1つのバンドルになるだろうから、これで困るケースはまず無いと思っている。

SVG とカラー絵文字

<img src> や background-image で参照した SVG を、ラスタライズせずベクタのまま PDF に埋め込めるようになった。
中身は typst が公開してくれている usvg crate と svg2pdf crate で、この部分は julik さんが PR を送ってくれた。

カラー絵文字も色付きで出るようになった。
v0.2.0 の時点では、カラー絵文字のフォントがグリフの輪郭を持っていないせいで文字が消え、しかもフォントが丸ごと埋め込まれて PDF が 9.9MB になる、という残念な状態だった。
ひとまず豆腐と警告を出すようにしていたのを、v0.5.0 で cmer さんがちゃんと描画するところまで持っていってくれた。
Type 3 フォントとして書き出しているので、PDF 上でも絵文字をコピーしたり検索したりできる。

カラー絵文字の対応にあたって、仕様を調べたりしていたものは issue にまとめている。絵文字は難しい github.com

改ページまわりを直した

改ページはこのプロジェクトの売りなので、ここのバグは優先して潰した。

  • ページをまたぐと、マージンの相殺で詰めたはずの間隔が開いてしまう
  • 1ページより高い flex コンテナの、2ページ目以降が消える
  • grid を分割すると、2ページ目以降が真っ白になる
  • ページをまたいだ番号付きリストで、番号が飛ぶ

どれも出力はされるけど中身がサイレントに消える系で、報告してもらわないと気づけなかったと思う。感謝

テーブルとテキスト

請求書っぽい HTML でよく踏むものが多かった。

  • 1行目が rowspan="2" のセルだけだと、2行目のセルが消える
  • display: table の直下に display: table-cell を置く昔ながらの段組みが描画されない
  • <span>one</span> <span>two</span> が onetwo になる
  • text-align: right でテキストだけ右に寄って、横に置いたロゴ画像が左に残る

あと、line-height: normal を固定の 1.2em ではなく、フォント自身のメトリクスから決めるようにした。
これは日本語フォントだと 1.2em では足りなくて、文字が行からはみ出してテーブルの罫線と重なっていたため。
ブラウザと同じ挙動に寄せた結果そうしたのだけど、ブラウザで line-height を指定していない日本語の文書だとなぜ行間が2割ほど広がるのかが理解できた。

Rails まわりとヘッダー/フッター

  • sghtmltopdf_image_tag と sghtmltopdf_stylesheet_link_tag が、asset pipeline を通したローカルの画像や @font-face のフォントを確実に読めるようにした
  • Rails の既定で読める範囲を Rails.root 全体から public/ と asset pipeline のロードパスに絞った
  • ヘッダー/フッターの HTML に data: URL の画像を入れられるようにした(ロゴを入れたいという要望があった)
  • ヘッダー/フッターの [topage](総ページ数)が空になるバグを直した
  • --header-html-content / --footer-html-content で、ヘッダー/フッターの HTML を一時ファイルなしで直接渡せるようにした
  • Intel Mac 向けの precompiled gem も配るようにした。これで x86_64-linux / aarch64-linux / x86_64-linux-musl / aarch64-linux-musl / x86_64-darwin / arm64-darwin の計6環境がサポートされた

crates.io に公開した

v0.5.1 で、Rust のコアを sghtmltopdf として crates.io に出した。 https://crates.io/crates/sghtmltopdf

cargo install sghtmltopdf で CLI が入るし、Rust のライブラリとしても使える。

[dependencies]
sghtmltopdf = "0.5"

公開する前に、どこまでを公開 API にするかを整理した。
Ruby 以外の言語のバインディングを作るときは、Converter に CLI と同じオプション列を渡せば、全部のオプションがそのまま使える。

あと、エラーメッセージとコード中のコメントを全部英語にした。 海外からのコントリビュートが多い且つ要望も複数あったので、crates.io への公開と合わせてやれてよかった。

たくさんのPRのおかげで結構品質上がった感じがするし、ドキュメント/コードコメントも整備されたので、リアクションがいくつかあった JS の実行が出来ないか腰を据えて検討していきたいと思っている。
QuickJS を処理パイプラインに組み込んで、PDFレイアウト作る前にスクリプト実行してDOM変更して再カスケードすれば案外いけるんじゃないか・・?となっているので組み込んで試してみる。