PDF を圧縮すると実際に何が起きるのか
PDF123 の圧縮は qpdf によるストリーム再圧縮とオブジェクトストリームの詰め込みを行い、画像のダウンサンプリング、非可逆の JPEG 再エンコード、フォントのサブセット化はしない。線形化はファイルサイズをほとんど変えない。

二つの PDF は画面上では同じに見えても、50 KB と 50 MB のように三桁の開きが出ることがある。その差はほとんどテキスト演算子にはない。20 ページの契約書ならテキスト演算子はせいぜい数十 KB で、残りは画像サンプル、丸ごと埋め込まれたフォントプログラム、そしてストリームとオブジェクトがどれだけ密に詰められているかだ。
ここから先に言っておくべき帰結がある。「PDF を圧縮する」は一つの操作ではなく四つだ。小さくなる対象も違えば支払う代償も違う。これを一つとして扱うと、経験則が矛盾する。圧縮したのに小さくならない、あるいは小さくなったがページが甘くなった、という具合に。
サイズはどこから来るのか
「この PDF はなぜこんなに大きいのか」の大半は、取り込みサイズのまま埋め込まれた高解像度スキャンや写真から始まる。算数は一度やっておく価値がある。レターサイズ 1 ページを 8.5 × 11 インチ、300 DPI で取り込むと約 2550 × 3300 ピクセルになり、1 ピクセル 3 バイトなら未圧縮でおよそ 25 MB だ。そういうページが 20 枚あれば、誰かが圧縮に触れる前に 60〜80 MB になることが多い。
二つ目の出どころはフォントだ。PDF はフォントプログラムを丸ごと埋め込むことができ、どのマシンでもテキストが同じように描画される。そのコストは、実際に読まれる文字の量ではなくグリフテーブルの大きさで決まる。だからフォントのサブセット化は独立したサイズのレバーになる。
三つ目は詰め込みそのものだ。同じ内容でも、無駄のある圧縮設定で保存されていたり、オブジェクトがファイル全体に散らばってそれぞれが管理用のオーバーヘッドを抱えていたりする。それはピクセルもグリフも変えないが、ストリーム再圧縮が手を届かせられるのはまさにその部分だ。
「圧縮」は四つの別物だ
サイズを動かすレバーは四つあり、作用する層が異なり、コストはほとんど重ならない。間違ったレバーを選ぶことが、圧縮が何もしなかったように見える最も一般的な理由だ。
| レバー | 動かす対象 | サイズの効果 | 代償 |
|---|---|---|---|
| ストリーム再圧縮とオブジェクトストリーム | 内容ストリームの圧縮方式、オブジェクトの保存と詰め込み | 元ファイルがどれだけ緩いかによる | ページの見た目は変わらない |
| 画像のダウンサンプリング | ピクセル数、つまり解像度 | 大 | 解像度が恒久的に下がる |
| 非可逆の画像再エンコード | ビットマップのバイト表現 | 大 | 画質が下がる |
| フォントのサブセット化 | 埋め込まれたグリフテーブル | 中 | 使われないグリフは編集できなくなる |
| 線形化 | オブジェクトの順序 | ほぼゼロ | 代わりに初回ページ表示が速くなる |
最初の四行はすべて緩い意味で「圧縮」と呼ばれるが、内容をそのまま残すのは最初の行だけだ。その行は効果も最も誤読されやすい。サイズの大半が生の画像サンプルであるスキャンには、取り除ける冗長性がほとんどない。そこを大きく削れるのはダウンサンプリングと非可逆再エンコードで、代償としてその解像度と画質を永久に失う。
PDF123 圧縮が実装するレバーは一つだけ
PDF を圧縮する(POST /api/v1/misc/compress-pdf)が実行するのは最初の行だけだ。qpdf を、未圧縮ストリームの圧縮(--compress-streams=y)、すでに Flate で圧縮されたストリームへの二度目の処理(--recompress-flate)、オブジェクトをオブジェクトストリームへ詰め込む処理(--object-streams=generate)付きで走らせる。
画像のダウンサンプリングはしないし、ビットマップを低い JPEG 品質に再エンコードもしないし、フォントのサブセット化もしない。ページは同じに見えるはずで、削減分は Flate の再圧縮とオブジェクトストリームの詰め込みから出る。
失敗条件も同じだけ明確に書いておく。ファイルのサイズが主に JPEG としてすでに圧縮された画像データなら、Flate に絞れる余地は残っていない。その画像バイトは上の三つのスイッチが触れる範囲の外にあるからだ。だからスキャンの束は数パーセントしか小さくならないのに、同じくらいのサイズのテキスト主体の PDF ははっきり縮む、ということが起きる。
逆方向の経路はPDF を展開するで、検査用にストリームを展開し(--qdf、オブジェクトストリームは無効)、こちらもページの見た目を変えない。
別の経路を選ぶべきとき
| 必要なもの | 使う経路 |
|---|---|
| 見た目はそのまま、詰め込みのオーバーヘッドだけ落としたい | 圧縮 |
| スキャンの束が本当に大きく、画質劣化を受け入れられる | ダウンサンプリングか非可逆再エンコード。この圧縮ではない |
| 編集可能なままにしたいが、次の文字が打てない | 書き出し設定か元ファイルを変える。詳しくはフォントのサブセット化 |
| ブラウザに 1 ページ目を早く見せたい | 線形化が解決するのは初回描画で、サイズではない |
| ファイルが全く開かない | 修復。サイズではなく構造の問題 |
最後の行に一つ罠がある。ここでのPDF の線形化は独立した実装ではなく、圧縮の別名だ。どちらも同じ操作を指し、正規の id は misc/compress-pdf で linearize-pdf は別名にすぎない。ポータルはこのツールで常に linearize=true と optimizeLevel=1 を送るが、サーバーはどちらのフィールドも読まず、qpdf に --linearize が渡ることもない。実際にファイルを線形化するのは保護で、暗号化するときだ。
つまり、見た目を変えずにファイルを小さくしたいなら圧縮で完結する。非可逆の画像縮小やフォントのサブセット化が欲しいなら、この経路は間違ったレバーだ。ファイルはアカウントなしでそのまま返ってくる。オブジェクト、ストリーム、相互参照テーブルがファイルのどこにあるのかを見るには、PDF の中身はどうなっているかへ進んでほしい。