Zum Inhalt springen
getnextpdf.com

Jedes Mal dieselben Bytes: reproduzierbare PDFs

Spec: ISO 32000-2, §14.4Spec: ISO 32000-2, §14.3.3

Bauen Sie ein PDF zweimal aus denselben Eingaben, und Sie würden dieselbe Datei erwarten. Die meisten PDF-Bibliotheken können das nicht versprechen – bauen Sie neu und vergleichen Sie, und die Bytes wandern. NextPDF kann die zwei Dinge fixieren, die sich bewegen, sodass dieselben Eingaben dieselben Bytes erzeugen, jedes Mal.

Byte-identische Ausgabe ist keine Eitelkeitskennzahl. Sie ist das Fundament unter drei Dingen, die Teams tatsächlich wollen.

Das erste ist Caching. Wenn ein Build eine reine Funktion seiner Eingaben ist, ist sein Ausgabe-Hash ein Cache-Schlüssel. Dieselben Eingaben, derselbe Hash, überspringen Sie die Arbeit und liefern Sie die gespeicherte Datei aus. Wenn die Bytes umherschweifen, schweift der Hash umher, und der Cache trifft nie.

Das zweite ist Manipulationsnachweis. Eine Pipeline, die genau die Datei neu erzeugen kann, die sie ausgeliefert hat, kann später beweisen, dass ein archiviertes Dokument nicht verändert wurde: neu bauen, beide hashen, vergleichen. Wenn auch nur ein Byte wegen einer eingebetteten Uhr abweicht, ist der Beweis dahin, und Sie sind zurück bei „vertrau mir”.

Das dritte ist vertrauenswürdige CI. Ein Golden-File-Test zeichnet eine bekanntermaßen gute Ausgabe auf und schlägt fehl, wenn eine Änderung sie verändert. Dieses Signal ist nur dann bedeutsam, wenn eine unveränderte Engine eine unveränderte Datei reproduziert. Wenn jeder Lauf an einem Zeitstempel abweicht, ist die Golden-File-Datei Rauschen, und das Team lernt, einen roten Build zu ignorieren – die teuerste Angewohnheit im Testen.

In NextPDFs deterministischem Profil sind die zwei von der Engine kontrollierten Felder, die andernfalls zwischen identischen Builds abweichen würden, die Daten und die /ID. Dies setzt voraus, dass der Rest der Pipeline bereits stabil ist – dieselben Eingaben und eine Serialisierung, die nicht von selbst variiert (mehr dazu unten):

  • Eingebettete Daten. Das Document-Information-Dictionary trägt CreationDate und ModDate (Spec: ISO 32000-2, §14.3.3), und die XMP-Metadaten spiegeln sie. Erfassen Sie „jetzt” zur Build-Zeit, und jeder Neuaufbau weicht ab.
  • Der Dateibezeichner. Das /ID-Array ist ein Paar von Byte-Strings, das die Datei identifiziert (Spec: ISO 32000-2, §14.4), gespeichert im Trailer-Dictionary (Spec: ISO 32000-2, §7.5.5). Bibliotheken leiten ihn üblicherweise aus der aktuellen Zeit plus Zufallsbytes ab, sodass er von Natur aus bei jedem Lauf anders ist.

Fixieren Sie beide – einen festen Zeitstempel und einen festen Seed für die /ID –, und die Ausgabe wird zu einer deterministischen Funktion ihres Inhalts. Lassen Sie den Inhalt unberührt, und die Datei ist Byte für Byte identisch. Das ist dieselbe Disziplin, die das Reproducible-Builds-Projekt für kompilierte Software etabliert hat, angewendet auf die Dokumentebene.

Determinismus in NextPDF ist ein Konfigurationsobjekt, kein Test-Hack. Die Engine stellt ein DeterministicSettings-Wertobjekt im NextPDF\Core-Namespace bereit. Es ist final readonly, unveränderlich, und es fixiert genau die zwei uhr- und zufallsabgeleiteten Quellen der Abweichung, die oben genannt wurden: die Daten und die /ID. Sie zu fixieren entfernt die zwei häufigsten Abweichungsquellen, garantiert aber für sich genommen keine byte-identische Ausgabe. Das übrige Serialisierungsverhalten der Engine – Objektreihenfolge, Font-Subsetting und Komprimierungseinstellungen – muss ebenfalls deterministisch sein, damit die Ausgabe sich reproduziert, und NextPDF hält diese von Natur aus stabil.

Sein Konstruktor nimmt zwei Argumente:

public function __construct(
public DateTimeImmutable $timestamp,
public string $fileIdSeed,
) {
// ...
}

$timestamp ist der einzelne feste Zeitpunkt, der in jedes Datumsfeld geschrieben wird – CreationDate, ModDate und ihre XMP-Spiegelung. Übergeben Sie ein DateTimeImmutable, und das Dokument hört auf, die Wanduhr zu fragen, wie spät es ist. $fileIdSeed ist die Eingabe, die die Trailer-/ID fixiert: ein 32-stelliger hexadezimaler String. Geben Sie denselben Seed, und die Engine leitet denselben Dateibezeichner ab, statt die Uhr und eine Zufallsquelle abzutasten.

Das Objekt validiert seine eigene Eingabe. Der Seed muss genau 32 hexadezimale Zeichen sein; alles andere wird bei der Konstruktion mit einer InvalidConfigException abgelehnt, statt klammheimlich eine anders aussehende /ID zu erzeugen. Das ist dieselbe Verweigern-zu-raten-Haltung, die der Rest der Engine einnimmt – eine mehrdeutige Eingabe scheitert laut, statt leise die Bytes zu verändern.

Mit beiden fixiert ist das Rezept das, was das Reproducible-Builds-Projekt vertraut gemacht hat: neu bauen, vergleichen, und der Unterschied ist leer.

  1. Fix the inputsThe same content, fonts, and settings that produced the original document.
  2. Pin the timestampOne DateTimeImmutable feeds CreationDate, ModDate, and the XMP dates — no wall clock.
  3. Pin the /ID seedA 32-character hex seed derives the trailer /ID instead of a clock-plus-random value.
  4. BuildThe output is now a pure function of content; the two moving parts are held still.
  5. Rebuild and diffRegenerate from the same inputs and compare bytes — an empty diff is the proof.
Reproducible build: identical inputs plus a pinned timestamp and a pinned /ID seed produce the same bytes, which a rebuild-and-diff step confirms.

Eine kleine, vollständige Form. Die Einstellungen werden einmal konstruiert und wiederverwendet, sodass zwei Läufe desselben Programms dieselbe Datei ausgeben.

<?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)

Der Seed ist eine Build-Eingabe, die Sie kontrollieren, kein Geheimnis. Speichern Sie ihn neben dem Rest Ihrer Build-Konfiguration. Der Punkt ist, dass er fest ist, sodass der Dateibezeichner, den er erzeugt, ebenfalls fest ist.

Die erste Falle ist „Ich habe den Zeitstempel entfernt, also ist mein Build jetzt reproduzierbar.” Das ist er gewöhnlich nicht, denn das /ID-Array ist die leisere der beiden Quellen. Daten sind in einem Metadatenfenster sichtbar und leicht zu merken; die Trailer-/ID ist für die meisten Reader unsichtbar und wird bei jedem Lauf aus der Uhr und einer Zufallsquelle neu erzeugt. Ein Build, der nur die Daten fixiert, erzeugt trotzdem jedes Mal eine andere Datei. Sie müssen beide ruhig halten.

Die zweite Falle ist, Determinismus für sich genommen als Sicherheitsfunktion zu behandeln. Eine fixierte /ID macht eine Datei reproduzierbar; sie macht sie nicht signiert, und sie beweist für sich genommen nicht, dass zwei Builds übereinstimmen. Ein Byte-für-Byte-Vergleich oder ein Hash beweist, dass die Builds übereinstimmen; die /ID zu fixieren entfernt nur eine Quelle des unechten Unterschieds. Und keines davon beweist, dass ein Dritter für die Datei bürgt. Reproduzierbarkeit und Signierung sind sich ergänzende Schichten, keine Ersätze.

Determinismus fixiert die eigenen beweglichen Teile der Engine. Er fixiert nicht Ihre Eingaben. Wenn Ihr Inhalt einen lebendigen Zeitstempel einbettet, eine Schrift zieht, die sich auf der Platte geändert hat, oder einen Wert rendert, der vom aktuellen Datum abhängt, ändert sich die Ausgabe, weil sich die Eingabe geändert hat – und das ist korrekt. DeterministicSettings entfernt den Nichtdeterminismus der Engine, nicht Ihren. Ein reproduzierbarer Build erfordert immer noch reproduzierbare Eingaben.

Deterministic byte-identical output — edition availability
EditionAvailability
Core

Volle Unterstützung. DeterministicSettings ist im Open-Source-Core enthalten: Fixieren Sie den Zeitstempel und den /ID-Seed, und derselbe Inhalt baut sich zu denselben Bytes neu auf – kein Edition-Gate.

ProNot in this edition
EnterpriseNot in this edition
  • Golden-File-Testing – die CI-Technik, die von byte-identischer Ausgabe abhängt, und warum eine deterministische Engine ihre Voraussetzung ist.
  • Inkrementelle Aktualisierungen – wie ein PDF durch Anhängen wächst, wo das /ID-Array erneut wichtig wird, um eine Datei mit ihren früheren Versionen in Beziehung zu setzen.
  • Metadaten und das XMP-Paket – wo die eingebetteten Daten leben und wie das XMP-Paket das Document-Information-Dictionary spiegelt.
  • Die Anatomie einer PDF-Datei – der Trailer, die Querverweistabelle und wo das /ID-Array in der Dateistruktur sitzt.
  • Byte-identisch – zwei Dateien, die exakt übereinstimmen, Byte für Byte. Die stärkste Form von „dieselbe” und diejenige, die ein Hash oder ein Vergleich verifizieren kann.
  • /ID (Dateibezeichner) – das Array aus zwei Byte-Strings, das ein PDF und seine Versionen identifiziert (ISO 32000-2 §14.4), gespeichert im Trailer-Dictionary (§7.5.5). Üblicherweise aus der Uhr plus Zufallsbytes abgeleitet, weshalb es sich bei jedem nicht fixierten Build ändert.
  • Document-Information-Dictionary – die Struktur, die CreationDate und ModDate trägt (ISO 32000-2 §14.3.3). Eine der zwei Quellen von Nichtdeterminismus, die ein deterministischer Build fixieren muss.
  • Golden-File-Datei – eine aufgezeichnete, bekanntermaßen gute Ausgabe, gegen die ein Test vergleicht; nur bedeutsam, wenn eine unveränderte Engine eine unveränderte Datei reproduziert.
  • Reproduzierbarer Build – ein Build, dessen Ausgabe eine deterministische Funktion seiner Eingaben ist, sodass ein Neuaufbau aus denselben Eingaben dieselben Bytes ergibt. Der Begriff stammt aus dem Reproducible-Builds-Projekt für kompilierte Software.