業務でレポートを 1 枚の縦長画像として書き出す話が出ていて、実装の前に既存ツールが縦長画像をどう扱うのかを見ておくことにしました。テスト用に A4 縦 8 ページの資料を自分で作り、1 枚に連結して書き出します。使ったのは ImgIng(https://imging.ai/)の PDF→画像変換で、環境は Apple M4 の Mac と Chromium 149 です。社内資料を扱う前提なので、ネットワークパネルを開いたまま操作し、PDF がアップロードされていないことも確認しました。
216 DPI と 300 DPI でそれぞれ書き出したところ、2 つの WebP はバイト単位で同じでした。どちらも 1446×16376 です。216 DPI なら本来 1785×20208 になるはずなので、幅を A4 の幅 8.264 インチで割ると実効は約 175 DPI。300 を選んだ意味はまったくありませんでした。
高さが 16383 の手前で止まっていた
ページ数を増やすとはっきりします。20 ページは 578×16380 で約 70 DPI、60 ページは 192×16380 で約 23 DPI でした。高さを上限内に収めるために画像全体が等比で縮小されていて、画面には最終サイズが出るだけです。「縮小しました」という表示はありません。
16383 は WebP の上限です。WebP のヘッダーは幅と高さをそれぞれ 14 ビットで持つので、これが最大値になります。Pillow 11.3 で試すと 8×16383 の WebP は普通に書けて、8×16384 は「Image size exceeds WebP limit of 16383 pixels」で止まりました。意外だったのは、同じ連結モードで JPG や PNG を選んでも 1446×16376 になったことです。どの形式でも WebP の上限に合わせて高さを決めているのだと推測していますが、コードは読んでいないので出力サイズからの推測にすぎません。
canvas の書き出しは黙って失敗する
自前で canvas に連結して toBlob する場合はもっと厄介で、エンジンごとに失敗の仕方が違います。品質 0.8 で上限超えの書き出しを要求した結果です。
| 要求 | Chromium 149 | Firefox 151 | WebKit 26.5 |
|---|---|---|---|
| 1786×20208 を WebP | 1786×16383 の WebP(エラーなし) | null | PNG を返す |
| 1786×65535 を JPEG | 1786×65500(エラーなし) | null | 1786×65535 |
| 1786×20208 を PNG | 正常 | 正常 | 正常 |
Chromium の 2 つは縮小ではなく切り捨てです。以前のグラデーションと色帯を使ったテストで確認済みで、今回のサイズもそれと一致しました。JPEG は少し注意が要ります。仕様上は 1 辺 16 ビットで 65535 までですが、試した Pillow と Chromium はどちらも 65500 で止まりました。Pillow は 65501 で「Maximum supported image dimension is 65500 pixels」と出します。なので「JPEG の上限は 65500」と書くのは形式の話としては不正確で、実装側の上限です。コードで気づけるのは Firefox の null だけでした。
canvas 自体にも上限があります。Chromium と Firefox は 1 辺 65535、Chromium と WebKit には面積で約 2^28 の上限もあり、WebKit は幅 1786 のとき高さ 150299 まででした。縦長画像だと先に効いてくるのはたいてい WebP の 16383 です。
DPI を下げるより分割する
20 ページの資料で比べました。5 ページずつ 4 枚に分けると各 1785×12630 で 216 DPI のまま、合計約 4.35 MB です。1 枚にまとめると 578×16380 で約 70 DPI、986 KB でした。ページ下部の 7pt の小さな行は、分割版で 840×39 ピクセル、1 枚版で 272×13 ピクセルです。容量は 4 倍以上になりますが、字が読めるほうを取ります。A4 は 216 DPI で 1 ページ 2526 px なので、WebP 1 枚に入るのは 6 ページまでです。
書き出し後には 3 エンジンそれぞれの黙った失敗を検出するチェックを入れるつもりです。まだ本番コードには入れていません。
const sheet = await new Promise(ok => canvasEl.toBlob(ok, wantType, 0.8));
if (sheet === null) throw new Error('形式の上限を超えた'); // Firefox
if (sheet.type !== wantType) throw new Error(`${sheet.type} が返ってきた`); // WebKit
const bmp = await createImageBitmap(sheet);
if (bmp.height !== canvasEl.height) throw new Error(`${bmp.height}px で切られた`); // Chromium
数値はすべて M4 の 1 台と上記 3 エンジンのビルドで測ったものです。本物の Chrome、Safari、スマートフォンは試していません。リリース前に、実際のページ数のレポートで分割の閾値が合っているかもう一度測るつもりです。