コンテンツにスキップ

圧縮とサブセット化で PDF のファイルサイズを削減する

忠実度を損なわずに、コンテンツが許す限り小さい PDF を作成したいとします。NextPDF には 2 つのサイズ調整レバーがあり、いずれもデフォルトで有効です。

  • ストリーム圧縮。 ライターは、すべてのページコンテンツストリームと、埋め込まれたすべてのフォントプログラムを FlateDecode(zlib)ストリームでラップします。NextPDF\Core\Config のフラグ compress がこの設定を保持します。ストリーミングドキュメントを構築する際は、withCompress() wither でこの設定を読み戻せます。
  • フォントサブセット化。 TrueType または CFF のフォントを埋め込むと、ライターはドキュメントが実際に使用するグリフのみを含むようにフォントプログラムを再構築し、その結果を FlateDecode で圧縮します。これは自動的に行われます。設定すべきフラグも、呼び出すべき処理もありません。20,000 グリフの CJK フェースのうち、ドキュメントで使われるのが数百グリフだけなら、ディスク上のサイズのごく一部で埋め込まれます。

先に明確にしておくと、NextPDF Core は画像のリサンプリング、画質を調整するつまみ、オブジェクトストリームの切り替え、リソースの重複排除設定を公開していません。サイズ制御として用意されているのは、上記の 2 つです。このレシピの残りの部分では、それらを正しく使う方法と、それぞれが行わないことを説明します。

前提条件:Core のインストール(composer require nextpdf/core:^3)と、サブセット化のパスでは埋め込みライセンスを持つフォントファイルが必要です。

Terminal window
composer require nextpdf/core:^3

PDF はオブジェクトのツリーです。最も大きなオブジェクトは通常、コンテンツストリーム(各ページの描画演算子)とフォントプログラム(埋め込まれたグリフのアウトライン)です。どちらも圧縮が効きやすいテキストデータやバイナリデータであるため、単一のサイズ調整レバーとして最も効果的なのは、それらを FlateDecode で圧縮することです。FlateDecode は、zlib でラップされた DEFLATE ストリーム(ISO 32000-2:2020 §7.4.4)に対する PDF 2.0 での名称であり、NextPDF が出力するフィルターです。

ライターは、NextPDF\Writer\PinnedZlibCompressor を通じて、DEFLATE 圧縮レベルを RFC 1951 の最大値である 9 に固定します。レベル 9 では、わずかに多くの CPU と引き換えに、最小のストリームが得られます。これを固定することで、出力を決定論的に保ちます。zlib ヘッダーがレベルをエンコードするため、レベルが変動するとバイト列が変わるからです。レベルを選択することはできません。同じ入力に対する 2 回の実行がバイト単位で同一のストリームを生成するように、エンジンがレベルを固定します。

2 つ目のレバーはフォントサブセット化です。ディスク上のフォントファイルには、その書体が定義するすべてのグリフが含まれていますが、「Invoice 2026」を印字するドキュメントに必要なのは、そのうち十数個だけです。NextPDF\Typography\FontSubsetter(TrueType 用)と NextPDF\Typography\CffSubsetter(CFF / OpenType 用)は、ドキュメントが実際にレンダリングしたコードポイントをたどり、複合グリフの依存関係を resolve(解決)し、必要なフォントテーブルのみを再構築します。これらは、決定論的な 6 文字のサブセットプレフィックスタグ(ISO 32000-2:2020 §9.9)を持つ有効なサブセットフォントのバイナリを出力します。ライターは、埋め込みフォントの使用グリフセットが判明している場合は常にこれを適用し、続いてサブセットを FlateDecode で圧縮します。特定のフェースをサブセット化しても削減率が 10 パーセント未満になる場合、再構築のオーバーヘッドがわずかな削減効果に見合わないため、サブセッターは代わりに元のプログラムを返します。

要点:PDF を小さく保つには、長いオプションのリストを調整するのではなく、圧縮を有効なまま(デフォルト)にし、実際のフォントファイルを埋め込んで、サブセット化で縮小できる対象を与えます。

サイズ調整のために設定できるつまみは、構成オブジェクト上の 1 つだけです。

NextPDF\Core\Config は、型付けされた wither メソッドを持つ、イミュータブルな final readonly の値オブジェクトです。関連するメンバーは次のとおりです。

  • compressbool、デフォルトは true) — FlateDecode 圧縮を有効にします。withCompress(bool $compress): self でこの設定を読み戻します。これは、フラグが変更され、その他のすべてのフィールドが保持された新しい Config を返します。

ドキュメント構築時に Config をアタッチします。

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self は、CLI スクリプトや短命のプロセス向けに一時的なレジストリを持つドキュメントを構築し、指定した Config を適用します。

2 つのメンバーがサイズ調整レバーの処理対象を決めますが、いずれもそれ自体は圧縮制御ではありません。

  • imageCacheBytesint、デフォルトは 52_428_800)はメモリ内の画像キャッシュに上限を設け、withImageCacheBytes(int $bytes): self がそれを変更します。これは、ビルド中のピークメモリを制限します。埋め込む画像のリサンプリングや再圧縮、その他の方法での縮小は行いません。これは出力サイズの制御ではなく、メモリ上限です。
  • fontsDirectorystring)と withFontsDirectory(string $dir): self は、フォントファイルのデフォルトの検索パスを設定し、これがサブセット化のパスに供給されます。

フォント関連の作業は、ドキュメント上のタイポグラフィサーフェスを通じて行います。

  • setFont(string $family, string $style = '', float $size = 12.0): static はフェースを選択します。ファミリーが埋め込み可能なフォントファイルに解決されると、ライターはレンダリングしたコードポイントを記録し、保存時にそのフェースをサブセット化できるようにします。
  • addFontDirectory(string $directory): static は、フォントファイルを検索する追加のディレクトリを登録します。

出力には標準的な 3 つの組み合わせを使います。getPdfData(): string はバイト列を返し、save(string $path): void はそれらをアトミックに書き込み、output(?string $filename, OutputDestination $dest): string は HTTP 配信を処理します。

サブセット化には、公開メソッドもフラグもありません。これは、フォントを埋め込み、それでテキストをレンダリングすることから生じる動作です。ライターがユーザーに代わって FontSubsetter / CffSubsetterNextPDF\Writer\PdfFontWriter の内部で駆動します。

この例では、圧縮を明示的に有効にし、サブセット化された埋め込みフォントを使用してドキュメントを構築し、その後にバイト列を書き込みます。呼び出し形式を示すためにエラー処理は省略しています。以下の本番向けサンプルでは、完全なガードを追加します。

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.
$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);
$doc->addFontDirectory(__DIR__ . '/fonts');
$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer
// subsets this font automatically when the document is built.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));

この例は自己完結型のプログラムです。圧縮を有効にしてドキュメントを構築し、管理下のディレクトリからフォントを埋め込み、サブセッターが使用グリフセットを得られるようにテキストをレンダリングし、その結果をアトミックに書き込みます。ビルドと保存のパスが送出する最も具体的な NextPDF 例外をキャッチし、握りつぶすのではなく、それぞれをコンテキスト付きで再送出します。NEXTPDF_FONT_DIR を、埋め込みライセンスを持つ TrueType または CFF のフェースを格納したディレクトリに向けます。プログラムは、埋め込みの前にパスを検証します。

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
use NextPDF\Exception\CompressionException;
use NextPDF\Exception\InvalidConfigException;
/**
* Resolve and validate the font directory from a server-controlled source.
*
* Reading the directory from the environment keeps the path off the request
* surface. The function rejects a missing or unreadable directory so the
* embedding path never runs against untrusted or absent input.
*/
function resolveFontDirectory(): string
{
$configured = getenv('NEXTPDF_FONT_DIR');
$dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir);
if ($real === false || !is_dir($real) || !is_readable($real)) {
throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir));
}
return $real;
}
/**
* Build a compressed, font-subsetted document and return its bytes.
*
* @param non-empty-string $fontDirectory Validated directory of embeddable fonts.
*
* @return string Raw PDF bytes.
*/
function buildCompactPdf(string $fontDirectory): string
{
// compress is true by default; pin it so the intent is explicit and the
// streaming writer path honours it regardless of any wrapper defaults.
$config = (new Config())
->withCompress(true)
->withFontsDirectory($fontDirectory)
// Bound the image cache so a build cannot exhaust memory. This is a
// memory ceiling, not an output-size control.
->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config);
$doc->addFontDirectory($fontDirectory);
$doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the
// writer turns into a font subset at build time.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026', newLine: true);
$doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program
// are FlateDecode-compressed before the bytes are returned.
return $doc->getPdfData();
}
try {
$fontDirectory = resolveFontDirectory();
$pdf = buildCompactPdf($fontDirectory);
} catch (CompressionException $e) {
// Raised if the zlib encoder hard-fails while compressing a stream.
throw new RuntimeException(
sprintf('Compression failed for a %s stream.', $e->getAlgorithm()),
previous: $e,
);
} catch (InvalidConfigException $e) {
// Raised by the output path for an invalid destination configuration.
throw new RuntimeException(
sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()),
previous: $e,
);
}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) {
throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));
}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);

想定される STDOUT です(バイト数はフォントとビルドに依存します)。

Wrote <n> bytes to <path>.
  • 圧縮はデフォルトで有効です。 新規の Config では、compresstrue に設定されています。withCompress() が必要になることはほとんどありません。明示的に設定するのは、意図を記録する場合、または生のストリームを読みたいデバッグ用ビルドでオプトアウトする場合だけにしてください。
  • 圧縮を無効にすると、ファイルは小さくなるのではなく大きくなります。 withCompress(false) は、圧縮されていないストリームを調べるための診断補助です。これがサイズの最適化になることは決してありません。圧縮を有効にした状態で出荷してください。
  • サブセット化には、実際に埋め込まれたフォントが必要です。 Base14 標準フォント(Helvetica、Times、Courier、およびそれらの関連フォント)は名前で参照され、通常のドキュメントには埋め込みプログラムを含まないため、サブセット化する対象がありません。サブセット化で縮小できるのは、フォントファイルから埋め込んだフェースのみです。
  • サブセット化は自動的かつ暗黙的に行われます。 フラグも、メソッドも、確認もありません。フォントを埋め込み、それを使ってテキストをレンダリングした場合、ライターはそのフォントをサブセット化しています。埋め込みプログラムは 6 文字のサブセットプレフィックスタグ(たとえば ABCDEF+LiberationSans)を持つため、リーダーはサブセットと完全な埋め込みを区別できます。
  • 削減がわずかな場合は、完全なフォントが保持されます。 サブセットによる削減がプログラムサイズの 10 パーセント未満になる場合、サブセッターは元のフォントを返します。これは意図的な下限です。再構築は、わずかな削減効果に見合いません。すでに非常に小さいフェースを埋め込む場合や、そのグリフのほぼすべてをレンダリングする場合に、このケースに該当することがあります。
  • imageCacheBytes は画像サイズの調整つまみではありません。 これは出力バイト数ではなく、メモリに上限を設けます。NextPDF Core は、与えられた画像データをそのまま埋め込みます。リサンプリング、ダウンサンプリング、再エンコードの処理はありません。より小さい画像が必要な場合は、埋め込む前に画像のサイズを変更し、再エンコードしてください。
  • オブジェクトストリームや重複排除の設定は存在しません。 NextPDF Core は、PDF 2.0 のオブジェクトストリームやリソースの重複排除を切り替えるトグルを公開していません。そのような設定を探さないでください。サイズ調整レバーは、ストリーム圧縮とフォントサブセット化です。

レベル 9 での圧縮は、ストリームの書き込みにおける主な CPU コストです。これにより、ビルド時間が数パーセント増える代わりに、最小の出力が得られます。コストは圧縮前のバイト数に比例するため、ページ数と埋め込みフォントデータの量が予算を左右します。サブセット化は、埋め込みフェースごとに 1 回のパスを追加し、フォントのテーブルディレクトリを解析し、使用グリフのクロージャを解決し、必要なテーブルを再構築します。大きな CJK フェースの場合、これは 2 つのレバーのうちコストが高い方ですが、ページごとではなく、フォントごとに 1 回実行されます。10 パーセントの削減下限が存在するのは、効果が見込めない場合にこのパスをホットパスから外しておくことも理由の一つです。埋め込みサブセットが 1 つの小さなドキュメントは、1500 ms の実時間と 96 MB のピーク予算に余裕で収まります。多数の画像を埋め込むビルドが、スワップするのではなくメモリ上限で速やかに失敗するように、imageCacheBytes を実際の上限に制限してください。

ビルドはプロセス内で実行されます。ドキュメントのバイト列がホスト外へ出ることはなく、ネットワーク呼び出しも行われません。外部から提供されるフォントや画像は、信頼できない入力として扱ってください。

  • フォントディレクトリを検証してください。 本番向けサンプルは、サーバーが管理する環境変数からフォントパスを読み取り、埋め込みの前に、存在しないディレクトリや読み取れないディレクトリを拒否します。リクエストフィールドからフォントパスを導出してはいけません。
  • 再配布のライセンスを持つフォントのみを埋め込んでください。 サブセットも、依然として埋め込みフォントプログラムです。そのフェースを含むドキュメントを出荷する前に、ライセンスが埋め込みを許可していることを確認してください。
  • 不正な形式のフォントは例外を送出し、暗黙のうちに破損させることはありません。 解析に失敗したフォントファイルは NextPDF\Exception\FontParsingException を送出し、zlib の致命的な失敗は NextPDF\Exception\CompressionException を送出します。最も具体的な例外をキャッチし、それに応じて対処してください。ビルドを空の catch で囲んではいけません。
  • ユーザー入力を出力パスに展開してはいけません。 サンプルは、固定パスまたはサーバーが管理するサイドチャネルに書き込み、save() 内のアトミックライターを通じて、ストリームラッパーと null バイトを拒否します。パストラバーサルを避けるため、出力パスはサーバーが管理する値から導出してください。
  • ドキュメントにシークレットを含めないでください。 クライアントに返す生成済みドキュメントに、資格情報、トークン、内部識別子を埋め込まないでください。

このレシピ自体は、規範的な標準への準拠を主張するものではありません。このレシピが使用するメカニズムは、PDF 2.0 仕様によって定義されています。FlateDecode ストリーム圧縮(ISO 32000-2:2020 §7.4.4)と、6 文字のサブセットプレフィックスによるフォントサブセットの命名(ISO 32000-2:2020 §9.9)です。NextPDF は、標準の書き込みパスの一部として両方を出力します。compress フラグ以外に、これらを構成することはありません。このページが宣言する structural な再現性プロファイルは、ライターが DEFLATE レベルを固定することを反映しており、そのため圧縮されたストリームは決定論的です。一方で、決定論的な設定を併せて構成しない限り、ドキュメントレベルの識別子は実行ごとに異なる場合があります。サブセット化の背後にある埋め込みの仕組みについては、以下にリンクされている埋め込みとサブセット化のレシピを参照してください。