DelphiでPDFのファイルサイズを削減するために、losLab PDF Libraryはファイル肥大化の3大要因に対処する3つのAPI提供しています。SubsetEmbeddedFontsは埋め込まれたTrueTypeフォントプログラムをドキュメントで実際にレンダリングされるグリフのみに書き換え、DownsampleImagesはターゲットDPIを超えるラスタ画像を再サンプリングし、NormalizeLZWStreamsはレガシーなLZWDecode圧縮をFlateDecodeに置き換えます。各APIは変更されたオブジェクトの数を返すため、戻り値が0の場合はサイレントエラーではなく何も処理が行われなかった(no-op)ことを意味します
なぜ結合されたPDFは元のファイルよりもサイズが大きくなるのか?
結合されたPDFやプログラムによって自動生成されたPDFのサイズが大きくなる主な理由は、フォントの完全埋め込み、表示解像度を大幅に超える画像のサンプリング、およびレガシーなLZWフィルタで圧縮されたストリームの3つです。ISO 32000-1 §9.9では、PDF生成ツールがフォントプログラム全体を埋め込むことを許可しており、安全なデフォルト設定であるため、ほとんどのツールがそのように動作します。完全なArialのFontFile2プログラムは数百キロバイトに達します。これを12個のソースファイルにそれぞれ埋め込んで結合すると、誰も入力していない文字のグリフ輪郭データのコピーが12個もファイル内に含まれることになります。結合自体が無駄を発生させるのではなく、結合によって無駄が一箇所に集中し、全体のファイルサイズとして顕在化するのです
2つ目の要因は画像です。ページの4分の1のフレームに配置された幅4800ピクセルのスキャン画像は、300 DPIの印刷システムが処理できる量の約40倍ものピクセルデータを保持しています。3つ目の要因は目立ちませんが、LZWDecodeフィルタが適用されたストリームです。ISO 32000-1 §7.4.4ではLZWDecodeとFlateDecodeの両方を規定していますが、通常はFlateの方が圧縮率が高いとされています。実際、同じデータであればFlateの方が常にサイズが小さくなり、LZWDecodeが残っているドキュメントは、その履歴のどこかで1990年代のツールを経由したものに限られます。本記事では、これらの課題を個別に解決するlosLab PDF Libraryの3つの最適化プロセスについて解説し、それらを1つのパイプラインに統合する方法を紹介します
SubsetEmbeddedFontsによるフォントのサブセット化
SubsetEmbeddedFontsは、読み込まれたドキュメント内のすべての埋め込みTrueTypeフォントを、実際に使用されている文字のみに縮小します。保持する文字のリストをコンテンツストリーム自体から自動的に取得するため、引数を渡す必要はありません。内部的には、この処理は各ページのコンテンツストリームをGetTextRunsで走査し、各フォントリソースで参照されている文字コードを走ラクし、保持リストを構築し、元のフォントプログラムをWindowsのFontSubエンジン(CreateFontPackage)に渡してサブセットを生成します。書き換えられたプログラムは元のFontFile2ストリームをその場で置き換え、BaseFont名にはLOSABC+のような接頭辞が付与されます。これはISO 32000-1 §9.6.4で規定されている、6文字の大文字とプラス記号からなるサブセットフォント用の命名規則です。この接頭辞の存在により、この呼び出しは「べき等」になります。すなわち、処理を2回実行してもすでにサブセット化されたフォントは認識されてスキップされるため、同じファイルを繰り返し処理する可能性のあるバッチ処理に組み込んでも安全です
APIの制限事項を理解する上で、2つの実装詳細を知っておく必要があります。第1に、この処理はFontFile2をターゲットにしているため、埋め込まれたTrueTypeプログラムのみが対象となります。Type 1や生のCFFとして埋め込まれたフォントは、破損のリスクを避けるために処理されずそのまま残されます。第2に、WindowsのFontSubエンジンに依存しているため、SubsetEmbeddedFontsはWindows環境でのみ動作します。また、読み込まれたドキュメント内のフォントは新規作成時のフラグ管理を経由していないため、フォントが処理対象であるかどうかの判定は、埋め込みフラグのヒューリスティックに依存するのではなく、実際のFontDescriptor → FontFile2の参照チェーンを解決することによって行われます。解決されたストリームが存在すればフォントは対象となり、存在しなければエラーを出さずにスキップされます
実質的なトレードオフとして、サブセットフォントにはサブセット化の実行時に存在していたグリフのみが含まれます。後続のツールや開発者自身のコードが後から同じフォントでテキストを追加した場合、サブセットに含まれていない文字は輪郭データを持たないため、文字化け(豆腐)としてレンダリングされます。フォントのサブセット化は、編集段階より前ではなく、常にコンテンツの変更が完了した最後のステップとして実行してください。後で再利用するためにフォントを抽出する場合も同様の注意が必要です。PDFlibPasによるテキスト、画像、フォントの抽出に関する記事では、抽出されたサブセットプログラムで何ができ、何ができないかを説明しています
DownsampleImagesはどのように縮小する画像を決定するのか?
DownsampleImages(MaxDPI, Quality, Filter)は、保守的なDPI推定値を使用して、確実にオーバーサンプリングされていると判断できる画像のみを再サンプリングします。PDFの画像XObjectはピクセルサイズを保持していますが、信頼できる物理解像度は持っておらず、元の画像から取得されたDPIタグがロード・編集・保存サイクルを経てそのまま残っていることは稀です。そのため、このプロセスではSrcDPI = PixelWidth / 8.5としてDPIを推定します。これは実質的に「この画像がLetterページの全幅に配置された場合、解像度はどのようになるか」という基準で計算します。推定値がMaxDPIを超える画像のみが処理対象となります。これは意図的な動作です。ページ上に小さく配置された画像は、推定値よりも実際のDPIが高くなるため、印刷品質レベルの資産を誤って劣化させることを防ぐために、過剰なトリガーを避けて保守的に処理します
Qualityには1から100の値を指定してJPEGの再エンコード品質を選択します。0を指定した場合は可逆のPNG形式(Flate)として保存されます。Filterは再サンプリングアルゴリズムを選択し、0はボックス平均、1はバイリニアを示します。スキャンされたオフィスのドキュメントの場合、DownsampleImages(150, 75, 1)が実用的な初期設定です。再印刷する可能性のあるドキュメントの場合は、MaxDPIを300に引き上げるか、このプロセス自体を完全にスキップしてください。ダウンサンプリングは3つのプロセスのうち唯一の非可逆な処理であるため、エンドユーザーが任意で無効化できる設定項目にしておくのが一般的です
NormalizeLZWStreamsによるレガシーLZWストリームの変換
NormalizeLZWStreamsは、リスクのない優れた最適化手段です。すべてのLZWDecodeストリームをその場でロスレス解凍し、FlateDecodeで再圧縮して、変換されたストリームの数を返します。単一の/Filter /LZWDecodeエントリと、フィルタチェーンの配列内にLZWが含まれているケースの両方に対応しており、配列内のLZWの部分のみが置き換えられ、チェーンの他のフィルタはそのまま維持されます。予測子パラメータ(Predictor、Columns、Colors、BitsPerComponent)はストリームのDecodeParmsから読み込まれてデコンプレッサに渡されるため、予測子エンコードされた画像データも正確に変換されます。両方のフィルタはビット単位で一致する可逆コーデックであるため、デコードされるバイトデータは変換前後で完全に同一です。コンテナの圧縮方式のみが変更されるため、この処理はすべてのファイルに対して無条件で実行しても安全です
LZWストリームが含まれていないドキュメントの場合、この呼び出しは単に0を返し、ファイルには一切変更を加えません。ライブラリの回帰テストでもこの動作が明示的にチェックされており、新規作成されたFlateのみのファイルでは変換数が0になることが確認されています。この動作保証は、2024年に生成された新しいファイルと1998年の古いファイルが混在する何千もの多様なファイルをパイプラインで自動処理する際に非常に重要です
Delphiにおけるファイルサイズ最適化パイプラインの全体像
これら3つの処理は1つの「読み込み・最適化・保存」関数にまとめることができます。処理対象のオブジェクトタイプがそれぞれ異なっているため(フォント、画像XObject、ストリームフィルタ)、実行順序による影響はほとんどありません。ただし、編集順序の制約があるフォントのサブセット化を最初に実行するのが最も理にかなっています
var
Lib: TPDFlib;
Fonts: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
begin
Fonts := Lib.SubsetEmbeddedFonts;
// Fonts = number of FontFile2 programs rewritten;
// 0 means nothing embedded, or everything already subsetted
Lib.SaveToFile('merged-report-subset.pdf');
end;
finally
Lib.Free;
end;
end;
ライブラリ自身がテストを行うのと同様に、パイプラインの検証はラウンドトリップテストで行います。v3.130の回帰テストでは、ドキュメントを作成して保存し、それを再ロードして最適化を実行して再保存し、次の3つのアサーションを検証します(出力サイズが小さくなっていること、返されたカウント値が期待通りであること、および最適化されたファイルを再ロードしてパースおよびレンダリングが正常に行えること)。この「作成・最適化・再ロード」のループを自社のテスト用ファイルで再現し、処理前後でのテキスト抽出結果を比較するテストを作成することは、本番稼働後に顧客から「請求書ファイルが壊れている」という報告を受けるのを防ぐための有意義なステップです
function OptimizePDF(const Src, Dst: string): Boolean;
var
Lib: TPDFlib;
Fonts, Images, Streams: Integer;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Src, '') <> 1 then
Exit;
Fonts := Lib.SubsetEmbeddedFonts; // TrueType FontFile2 -> subset
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Log Fonts/Images/Streams: three zeros mean the file was already lean
finally
Lib.Free;
end;
end;
このパイプラインは、結合ワークフローのどの位置に配置すべきでしょうか。それは、結合処理の最中ではなく「結合の後」です。最初にファイルを結合し、その結果に対して最適化を実行することで、各埋め込みフォントはソースファイルごとではなく、使用されているすべての文字の論理和(ユニオン)に対して一度だけサブセット化されます。結合の処理能力がボトルネックになる場合、PDFlibPasはオブジェクトの完全なパースを回避するバイトレベルの高速パスを提供しています。これについては、バイト参照シフトによる高速なPDF結合に関する記事で説明しています。また、メモリに収まらないほど大容量のファイルを処理する場合は、直接ファイルアクセスによる大容量PDFの結合と分割でストリーミング方式について解説しています。いずれのアプローチも、結合された出力に対する最終的な最適化処理と完璧に組み合わせることができます
// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
3つの処理がカバーしない範囲
losLab PDF Libraryの最適化機能は、ドキュメントの意味論を変更する処理をあえて除外しています。SubsetEmbeddedFontsは結合されたソース間で重複するフォントを1つのプログラムに統合することはせず、それぞれを独立して縮小します(重複排除はリスクの高い別の変換処理です)。DownsampleImagesは、人間が見て明らかにフレームサイズに対して過大であっても、保守的な推定DPIが閾値を下回っている画像はスキップします。また、これらの処理はドキュメントの構造には一切触れないため、不要な孤立オブジェクトが多数蓄積して肥大化したファイルは、ストリームレベルの最適化ではなく、再シリアル化を伴う完全保存によって解決する必要があります。これらの制限の範囲内で、フォントのサブセット化、画像のダウンサンプリング、およびLZWからFlateへの変換を組み合わせることで、それぞれ1回のAPI呼び出しでPDF肥大化の典型的な3大要因を排除できます。これら3つの関数は、上述した結合、抽出、およびレンダリングAPIとともに、Delphi、C#、およびVB.NET向けのlosLab PDF Libraryの一部として提供されています