1 ページしかない、しかし高さ 9000pt という PDF を画像に変換したら小さい文字がつぶれた、という相談を受けました。相手は解像度が足りないと考えて 216 DPI を 300 DPI に上げて再実行。結果は「ほぼ同じ」ではなく、完全に同じファイルです。1456×16380 ピクセル、1.10 MB、4.05 秒、三つとも一致しました。原因はパース処理でもメモリでもなく最後のエンコード段にあり、そこはちょうど私が語れる範囲でもあります。
先に立場を書いておくと、私が担当しているのはクライアント側のエンコード / デコード周りで、PDF エンジンそのものは私の担当ではありません。ページの解析処理については「たぶんこうなっている」としか言えず、キャンバスとエンコーダの話なら実数を出せます。サンプルは他人のベンチマークを引用したくなかったので自作しました。A4 4 ページの図表混在文書 490 KB と、800×9000pt の超長 1 ページ、この二つです。
通常の 4 ページ、コストは体積ではなく時間
まず 4 ページのほう、WebP で全ページ出力。144 DPI は 1 ページ 1192×1686 で合計 317 KB・2.0 秒、216 DPI は 1788×2529 で 494 KB・3.0 秒、300 DPI は 2483×3512 で 647 KB・6.1 秒。216 → 300 でピクセル数は 93% 増、体積は 31% 増、時間はちょうど倍です。体積が主なコストだと思って測り始めたのですが違いました。文書系ページは平坦な白が大半を占めるため増えたピクセルはエンコーダがほぼ吸収してしまい、素直に線形に伸びるのは所要時間のほうでした。同じ 216 DPI でフォーマットだけ変えた結果も同じことを別角度から示しています。WebP 494 KB・3.0 秒、AVIF 552 KB・10.3 秒、PNG 1.28 MB・1.0 秒、JPG 1.36 MB・1.0 秒。AVIF が WebP より 12% 大きく 3.4 倍遅いのは予想と逆で、平坦な面と鋭い文字エッジで構成されたページでは AVIF のイントラ予測が効きにくい一方、エンコード時間だけは通常どおりかかる、ということだと理解しています。可逆の PNG が JPEG より 6% 小さいのも裏返しで、JPEG は文字の輪郭のリンギングにビットレートを使ってしまう。いずれもこのサンプルでの結果で、写真主体の PDF なら逆になる可能性が高いですが、そちらは測っていないので断定しません。
2 つの設定で、出てきたのは同一のファイル
面白いのは超長ページのほうです。
| 設定 | 出力サイズ | 体積 | 所要時間 |
|---|---|---|---|
| 216 DPI / 全ページ | 1456×16380 | 1.10 MB | 4.06 秒 |
| 300 DPI / 全ページ | 1456×16380 | 1.10 MB | 4.05 秒 |
| 300 DPI / 1 枚の長い画像 | 1456×16380 | 1.40 MB | 4.05 秒 |
216 DPI なら本来 2400×27000 になるはずが実際の幅は 1456、1456 / (800 / 72) で逆算すると 131 DPI、選択肢の中で最も低い 144 よりさらに低い値です。鍵は 16380 という数字で、WebP の最大辺長 16383 のわずか 3 ピクセル手前に収まっています。メモリ不足による品質低下ではなく、エンコーダがそれ以上長い辺を受け付けないということ。縦横比は 16380/1456 = 11.25 で元ページの 9000/800 と完全に一致しており、両方向が同時に縮小され、下端の切り落としも引き伸ばしも起きていません。3 行目は説明できていません。同じ寸法・同じ内容なのに長い画像の経路は 1.40 MB、全ページ経路は 1.10 MB で 27% 差がある。「長い画像の経路のほうが保守的な品質設定なのだろう」と書きかけて消しました。それは観察を言い換えただけで説明になっていないからです。
タイル描画が必須になる理由
タイル描画が必須になる理由もここに繋がります。2400×27000 は 6480 万ピクセル、RGBA なら 259 MB のキャンバスが常駐し、モバイル Safari のキャンバス面積上限はこれよりかなり低い。そこでページを安全な高さで分割し 1 枚ずつ描画・エンコードして元の座標で合成するのですが、最初の実装で二つ間違えました。一つは、初期版が全タイルを描画してから合成していたため、合成の瞬間に N 枚のタイルと合成先が同時に生存してピーク使用量が分割しない場合より悪化したこと。タイルは描き終えた時点で即座に解放しないと、一度の大きな確保が N 回の小さな確保に変わるだけです。もう一つは継ぎ目で、各タイルが自分の高さを個別に丸めると誤差が蓄積して 1 ピクセルのずれや文字の重なりが出ます。各タイルの開始位置を前のタイルの終了位置と一致させ、元座標系で計算して丸めは一度だけ行う必要があります。どちらも継ぎ目を目視で追って見つけたもので、洗練されたデバッグ手法があったわけではありません。
実務としては、A4 などの通常サイズは 216 で十分、印刷や後段の OCR がある場合のみ 300 にして、増えるのは体積より時間だと見込んでおく。超長ページで DPI を上げても意味はなく、出力幅を測って 幅 / (ページ幅pt / 72) で実効 DPI を出す、それが実際に得られた解像度です。解像度を上げたいならページを分割するしかありません。一つ結論が出ていないのは、上限に当たった場合にタイルを個別画像として出力し利用側で結合してもらえば解像度は保てるものの、「1 ページ = 1 ファイル」という出力形態が崩れる、という点です。どちらがマシなのか未だに決めきれていません。同種の実装をされた方がいれば、どちらを選んだか伺いたいところです。
上記の数値は https://imging.ai/pdf-to-image/ で計測しました。