毎回同じバイト — 再現可能な PDF
Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3
同じ入力から PDF を 2 度作れば、同じファイルを期待するでしょう。ほとんどの PDF ライブラリはそれを約束できません — 作り直して差分を取れば、バイトはずれます。NextPDF は動く 2 つのものを固定できるので、同じ入力が、毎回、同じバイトを生成します。
なぜこれが重要か
「なぜこれが重要か」という見出しのセクションバイト単位で同一の出力は、虚栄の指標ではありません。それは、チームが実際に欲しがる 3 つのものの下にある基盤です。
第 1 は キャッシュ です。ビルドが入力の純粋な関数なら、その出力ハッシュがキャッシュキーになります。同じ入力、同じハッシュ、作業を飛ばして格納されたファイルを供する。バイトが彷徨えば、ハッシュが彷徨い、キャッシュは決してヒットしません。
第 2 は 改竄の証跡 です。出荷したまさにそのファイルを再生成できるパイプラインは、後に、アーカイブされた文書が改変されていないことを証明できます。作り直し、両方をハッシュし、比較する。埋め込まれた時計のせいで 1 バイトでも異なれば、その証明は消え、あなたは「私を信じて」へ逆戻りです。
第 3 は 信頼できる CI です。ゴールデンファイルテストは、既知の良好な出力を記録し、変更がそれを変えたときに失敗します。そのシグナルが意味を持つのは、変更されていないエンジンが変更されていないファイルを再現するときだけです。すべての実行がタイムスタンプで異なれば、ゴールデンファイルはノイズであり、チームは赤いビルドを無視することを学びます — テストにおける最も高くつく習慣です。
NextPDF の決定論的プロファイルでは、同一のビルド間でさもなくばずれるであろう、エンジンが制御する 2 つのフィールドは、日付と /ID です。これは、パイプラインの残りがすでに安定していることを前提とします — 同じ入力、そしてそれ自身では変化しないシリアライゼーション(これについては下記でさらに)です。
- 埋め込み日付。 Document Information ディクショナリは
CreationDateとModDateを携え(Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3)、XMP メタデータがそれらを反映します。ビルド時に「今」を捕捉すれば、作り直すたびに異なります。 - ファイル識別子。
/ID配列はファイルを識別する 1 組のバイト文字列で(Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4)、トレーラーディクショナリに格納されます(Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5)。ライブラリは通常それを現在時刻にランダムバイトを加えて導出するので、設計上、実行ごとに異なります。
両方を固定すれば — 固定されたタイムスタンプと /ID のための固定されたシード — 出力はそのコンテンツの決定論的関数になります。コンテンツをそのままにすれば、ファイルはバイト単位で同一です。これは、Reproducible Builds プロジェクトがコンパイル済みソフトウェアのために確立したのと同じ規律を、文書のレイヤーに適用したものです。
NextPDF のアプローチ
「NextPDF のアプローチ」という見出しのセクションNextPDF における決定論は、テストのハックではなく設定オブジェクトです。エンジンは NextPDF\Core 名前空間に DeterministicSettings 値オブジェクトを公開します。それは final readonly で、不変であり、上で名付けた、時計とランダムに由来するずれの 2 つの源 — 日付と /ID — をまさに固定します。それらを固定することは最も一般的な 2 つのずれの源を除去しますが、それ自体ではバイト単位で同一の出力を保証しません。エンジンの他のシリアライゼーションの振る舞い — オブジェクトの順序、フォントのサブセット化、そして圧縮設定 — も、出力が再現するには決定論的でなければならず、NextPDF はそれらを設計上安定に保ちます。
そのコンストラクタは 2 つの引数を取ります。
public function __construct( public DateTimeImmutable $timestamp, public string $fileIdSeed,) { // ...}$timestamp は、すべての日付フィールド — CreationDate、ModDate、そしてそれらの XMP の反映 — に書かれる単一の固定された瞬間です。1 つの DateTimeImmutable を渡せば、文書は壁掛け時計に今何時かを尋ねるのをやめます。$fileIdSeed は、トレーラーの /ID を固定する入力です。32 文字の 16 進文字列です。同じシードを与えれば、エンジンは時計とランダムソースをサンプリングする代わりに、同じファイル識別子を導出します。
オブジェクトは自身の入力を検証します。シードはちょうど 32 文字の 16 進でなければならず、それ以外は構築時に InvalidConfigException で拒否され、見た目の異なる /ID をひそかに生成することはありません。これは、エンジンの残りが取るのと同じ推測を拒む姿勢です — 曖昧な入力は、バイトをひそかに変えるのではなく、声高に失敗します。
両方を固定すれば、レシピは Reproducible Builds プロジェクトがおなじみにしたものです。作り直し、差分を取り、その差分は空です。
- Fix the inputsThe same content, fonts, and settings that produced the original document.
- Pin the timestampOne DateTimeImmutable feeds CreationDate, ModDate, and the XMP dates — no wall clock.
- Pin the /ID seedA 32-character hex seed derives the trailer /ID instead of a clock-plus-random value.
- BuildThe output is now a pure function of content; the two moving parts are held still.
- Rebuild and diffRegenerate from the same inputs and compare bytes — an empty diff is the proof.
小さく完全な形です。設定は一度構築され再利用されるので、同じプログラムの 2 回の実行が同じファイルを送出します。
<?php
declare(strict_types=1);
use NextPDF\Core\DeterministicSettings;use NextPDF\Exception\InvalidConfigException;
// One fixed instant for every date field — never the wall clock.$timestamp = new DateTimeImmutable('2026-01-01T00:00:00+00:00');
// A 32-character hex seed pins the trailer /ID. Same seed, same /ID.$fileIdSeed = '0123456789abcdef0123456789abcdef';
try { $deterministic = new DeterministicSettings( timestamp: $timestamp, fileIdSeed: $fileIdSeed, );} catch (InvalidConfigException $e) { // A malformed seed (not exactly 32 hex chars) is refused here, // before any document is built — not silently coerced. error_log($e->getMessage());
throw $e;}
// Hand $deterministic to the document configuration. With both moving// parts pinned, building the same content twice yields identical bytes://// sha256(build_one) === sha256(build_two)シードは、あなたが制御するビルド入力であって、秘密ではありません。残りのビルド設定の隣に格納してください。要点は、それが 固定 されていることで、だからそれが生成するファイル識別子も固定されます。
よくある誤解
「よくある誤解」という見出しのセクション最初の罠は「私はタイムスタンプを除去した、だから私のビルドはいま再現可能だ」です。たいていそうではありません。なぜなら /ID 配列が 2 つの源のうち静かなほうだからです。日付はメタデータパネルに見え、覚えやすい。トレーラーの /ID はほとんどのリーダーに不可視で、実行ごとに時計とランダムソースから再生成されます。日付だけを固定したビルドは、それでも毎回異なるファイルを生成します。両方を静止させなければなりません。
第 2 の罠は、決定論をそれ自体でセキュリティ機能として扱うことです。固定された /ID はファイルを 再現可能 にします。それはファイルを 署名済み にはせず、それ自体では 2 つのビルドが一致することを証明しません。バイト単位の比較やハッシュが、ビルドが一致することを証明します。/ID を固定することは、見かけの差異の 1 つの源を除去するだけです。そしてそのどちらも、第三者がそのファイルを保証することを証明しません。再現可能性と署名は補完的なレイヤーであって、代替ではありません。
限界と境界
「限界と境界」という見出しのセクション決定論はエンジン自身の動く部分を固定します。それは あなたの 入力を固定しません。あなたのコンテンツがライブのタイムスタンプを埋め込んだり、ディスク上で変わったフォントを引いたり、現在の日付に依存する値をレンダリングしたりすれば、出力は 入力 が変わったから変わります — そしてそれは正しいことです。DeterministicSettings はエンジンの非決定性を除去するのであって、あなたのそれを除去しません。再現可能なビルドは、なお再現可能な入力を必要とします。
| Edition | Availability |
|---|---|
| Core | Full support. |
| Pro | Not in this edition |
| Enterprise | Not in this edition |
関連ドキュメント
「関連ドキュメント」という見出しのセクション- Golden-file testing — バイト単位で同一の出力に依存する CI 技法、そしてなぜ決定論的なエンジンがその前提条件なのか。
- Incremental updates — PDF が追記によってどう成長するか、そして
/ID配列が、ファイルをその以前のバージョンに関連づけるためにふたたび重要になる場所。 - Metadata and the XMP packet — 埋め込み日付がどこに存在し、XMP パケットがどう Document Information ディクショナリを反映するか。
- The anatomy of a PDF file — トレーラー、クロスリファレンステーブル、そして
/ID配列がファイル構造のどこに位置するか。
- バイト単位で同一(Byte-identical) — バイトごとに正確に一致する 2 つのファイル。「同じ」の最も強い形で、ハッシュや差分が検証できるもの。
/ID(ファイル識別子) — PDF とそのバージョンを識別する、2 つのバイト文字列の配列(ISO 32000-2 §14.4)。トレーラーディクショナリに格納される(§7.5.5)。通常は時計にランダムバイトを加えて導出されるので、固定されていないビルドごとに変わる。- Document Information ディクショナリ(Document information dictionary) —
CreationDateとModDateを携える構造(ISO 32000-2 §14.3.3)。決定論的なビルドが固定しなければならない非決定性の 2 つの源の 1 つ。 - ゴールデンファイル(Golden file) — テストが照合する、記録された既知の良好な出力。変更されていないエンジンが変更されていないファイルを再現するときだけ意味を持つ。
- 再現可能なビルド(Reproducible build) — その出力が入力の決定論的関数であるビルドで、同じ入力からの作り直しが同じバイトを生む。この用語は、コンパイル済みソフトウェアのための Reproducible Builds プロジェクトに由来する。