社内の管理画面からレポートを PDF に書き出して営業側に渡したところ、文字が小さくて読めないと戻ってきた。画面上では普通に見えるページで、指標カードが四枚と 46 行の明細テーブルが並んでいるだけのものだ。担当者は Ctrl+P で保存しただけで、特別なことは何もしていない。
別の HTML → PDF 変換ツールに替えてみたら、今度はページ分割がなくなって縦に長い一枚になった。文字は大きくなったが、印刷するとプリンタ側が縮小するので結局同じところに戻った。二回とも外したので、ようやくツールの問題ではないのではと疑い始めた。
そこで対照実験として一通り測り直した。サンプルはスクリプトで自分で生成したもので、Tidebox という架空ブランドの越境フルフィルメント週報になっている。SKU も倉庫も金額もすべて作り物で、実在の業務データは含まれていない。レイアウトは社内のレポートページに寄せてあり、レンダリング幅 1440px、テーブル自体は 1180px ある。
ブラウザの印刷から書き出した場合、出てきたのは 3 ページで、用紙サイズは 612×792pt だった。これは A4 ではなく Letter で、デフォルト用紙はプリンタと OS に従う。私はずっと A4 だと思い込んでいたので、ここで一度認識を改めた。幅 612pt に 1180px の表は収まらないため、Chromium はページ全体を縮小してから用紙の高さで切る。結果として 13px だった表の文字は 6.74pt まで落ち、ファイルは 431,114 バイトになった。
もう一方は ImgIng(https://imging.ai/ )の HTML → PDF で、こちらは既定では用紙を当てはめず、ページの実際のレンダリング幅と高さに合わせた一枚のカスタムサイズ PDF を作る。同じファイルで 1 ページ 1080×2379pt、文字は 9.75pt、204,648 バイトだった。文字が大きいのは縮小処理が挟まらないからであって、描画がきれいだという意味ではない。同じ表の領域を 300 dpi まで拡大して見比べたが、字形の差は測れなかった。
処理がどこで走るかについても確認したので書いておく。インポート、ディレクトリの読み取り、リソースのマッピング、即時プレビュー、そして PDF を受け取った後のロスレス最小化は、すべてブラウザ内のローカルで完了し、この間は一切送信が発生しない。変換をクリックした後にだけ、スクリプトを取り除きリソースをすべてインライン化した自己完結型の HTML スナップショットが、同一オリジンの imging.cn のエンドポイントへ一度の POST で送られ、サーバ側の Chromium / Skia が実際の PDF を返す。これがこの製品で唯一サーバを経由するドキュメント機能になる。ページ内で fetch と XHR を包んで記録したところ、クリック前の非 GET リクエストは 0 件、クリック後にちょうど 1 件の POST だった。
どちらの既定値も間違いではない。私の HTML が「用紙は何ミリで、分割するのかしないのか」という問いに一度も答えていなかったので、書き出す側がそれぞれ勝手に決めていただけだ。
そこで三か所だけ宣言を足した。@page で用紙サイズと向きと余白を指定し、thead に display:table-header-group を当てて見出し行を各ページで繰り返させ、tr に break-inside:avoid を当てて複数行セルが行の途中で割れないようにする。同じ長い表を両方の経路でもう一度書き出したところ、どちらも 7 ページ、各ページ 595×842pt の A4、見出し行が 7 回繰り返し、表の文字は 8.25pt、テキスト抽出の一致率は 99.32% で、項目ごとに完全に一致した。分かれ目はツールではなく印刷 CSS を書いたかどうかだった。
よく見る説明と食い違った点が二つある。ひとつは @page をスタイルシートの最上位に裸で書く必要はないこと。@media print{} の中に入れた版も作って比べたが、成果物の差は 30 バイトで、用紙仕様もページ数も文字サイズも一致率もすべて同じだった。もうひとつは thead の行は実は冗長だということ。外しても見出しは各ページで繰り返される。もともと UA スタイルシートの既定値だからで、書いておくのは意図をコードに残すためでしかない。
表の行が途中で切断されるという話も検証したが、こちらは再現しなかった。各行に二か所の固有マーカーを埋め、同じマーカーが二ページに現れたら切断と判定する方法で、七つのサンプルとすべての書き出し経路を数えて 0 行だった。今の Chromium は入り切らない行をそのまま次ページへ送る。
本当に厄介だったのは最後の一つで、これはページ分割ではなくデータそのものが欠ける。
社内のレポートのテーブルは、たいてい height:520px; overflow:auto の固定高スクロール領域の中に入っている。主要なテーブルコンポーネントの既定がその形なので、意識すらしていなかった。この状態で書き出すと、コンテナ内で見えていた分しか残らない。46 行の明細は 8 行だけになり、テキストの一致率は 13.40% まで落ちた。
両方の経路で結果は同じ 8 行で、ツールを替えても救われない。しかも画面には何の警告も出ない。進捗は正常に完了し、品質レポートも普通に表示される。行にマーカーを埋めていなければ気づけなかった。戻ってきた理由が「文字が小さい」ではなく「30 行以上足りない」だったら、そのまま客先へ渡っていた可能性が高い。
対処は、書き出す前にそのスクロール領域を自然な高さに戻すこと。先の三か所と一緒に共通の印刷用スタイルへ入れて、新しいページは継承させ、古いページは順に直している。書き出す前に「このページにはスクロール領域がある」と教えてくれる仕組みは見つけられていないので、当面は共通スタイルと、書き出した後に行数を数える手順の二段構えで凌いでいる。

