受託で作ったレポート画面を、クライアントから「保存用にPDFでもください」と言われることがよくあります。ブラウザの印刷からPDFに保存して送った翌朝、「表が8行しかないのですが、残りは?」と返ってきました。画面には46行そろっていて、消した覚えもない。書き出しの一手間だけで38行が消えていたわけです。
最初は書き出しツールの手抜きを疑って、まったく別の経路でもう一度出しました。やはり8行。欠けている行まで同じ。実装もベンダーも違う二つの経路が同じように失敗するなら、原因はどちらのツールでもありません。
原因を確かめるため、クライアントのデータは使わずに自分でサンプルを作りました。よくあるレポート画面の体裁をまねた静的ページで、明細46行、サマリーカード4枚、ヘッダーとフッターつき、中身はすべて架空です。これをブラウザの印刷と、図映 ImgIng(https://imging.ai/ )のHTML→PDFの二経路で書き出し、ページ数・用紙サイズ・本文の文字サイズ・抽出できる文字数・リンク注釈をスクリプトで突き合わせました。以下の数値はすべてこのサンプルのものです。
8行の犯人は自分のマークアップでした。表が高さ固定・overflow:auto のコンテナに入っていたのです。はみ出した行はレイアウト上そもそも場所を取っておらず、PDFにスクロールという概念はないので、描けるのは表示されている一画面分だけ。両経路ともR01からR08までを書き出し、文字の保持率は13.4%まで落ち、どちらの画面にも警告は一切出ませんでした。管理画面系のテーブルコンポーネントは既定で高さ固定のスクロール領域と固定ヘッダーを持つので、自分でoverflowを書いた覚えがなくても踏みます。
対処は書き出しの前にしかできません。高さの制限を外して表をページと一緒に伸ばし、ページが実際に長くなったのを目で確かめてから変換する。定期的に納品するレポートなら、印刷時に自然な高さへ戻すスタイルをページ側に置いておくほうが楽です。
二つめはページ送りです。印刷用のCSSを書かないと、各経路が各自の既定値で動きます。ブラウザの印刷は612×792ptのLetterで2ページ、紙幅に収めるために縮小がかかり、13pxだった表の文字が6.74ptになりました。実際のレンダリング寸法に従うもう一方は、カスタムサイズの1ページで9.75pt。この差を「描画がきれい」と読むのは誤りで、同じ箇所を拡大しても字形の鮮明さに測れる差はありません。縮小が要るか要らないかというページ方針の結果です。決着をつけるのはページ側の宣言で、@page を書いてヘッダーを各ページで繰り返し、行が途中で割れないようにしたら、両経路とも7ページのA4・ヘッダー7回で完全に一致しました。なお @page は @media print の中でも効きます。むき出しで書かないと効かないと思い込んで外に出した回がありましたが、成果物の差は30バイトほどでした。
三つめは文字が選べるかどうか。ページ全体を画像にしてPDFへ押し込めばページ送りの悩みは消えますが、その代償を測りたくて自分で対照物を作りました。同じレポートで、画像化したほうは選択できる文字が0個・995,232バイト、レイアウトを通したほうは2,996文字が選べて156,370バイト、容量は約6分の1です。受け取った側が伝票番号をコピーしたい、本文を検索したいとなったとき、前者は何も返してくれません。
四つめは描画と関係のない話で、そのファイルを出していいのかという判断です。読み込み、ディレクトリの読み取り、リソースの解決、即時プレビュー、そしてPDFを受け取ったあとの可逆的な最小化まではブラウザ内で完結し、この間の送信はゼロ。変換を押したあとにだけ、スクリプトを除去しリソースを埋め込んだ自己完結HTMLのスナップショットが、同一オリジンのエンドポイントへ1回のPOSTで送られ、サーバー側のChromium / Skiaが実物のPDFを返します。実際に1回キャプチャしたところ、そのサンプルのリクエストボディは1,604,339バイトでした。欠点という話ではなく、性質として知っておく必要がある、という話です。私は集計ページはそのまま変換し、実額や連絡先を含む明細は先に匿名化してから変換しています。
まだ解けていない点も一つ。日本語や中国語の改行位置が両者でずれます。レンダリング側に私の手元のフォントがないためで、同じリード文の折り返しが一行ぶん違いました。画面と寸分違わぬ体裁が要る場面では、いまのところフォントをページに埋め込むしか手がありません。
四つ見直しても三分ほどです。書き出したファイルの行数を数えていれば、あの朝のやり取りは起きませんでした。

