Ga naar inhoud
getnextpdf.com

Elke keer dezelfde bytes: reproduceerbare PDF's

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

Bouw twee keer een PDF uit dezelfde invoer en je verwacht hetzelfde bestand. De meeste PDF-bibliotheken kunnen dat niet beloven — herbouw en diff, en de bytes lopen uiteen. NextPDF kan de twee dingen die bewegen vastpinnen, zodat dezelfde invoer elke keer dezelfde bytes oplevert.

Byte-identieke uitvoer is geen ijdelheidsmaat. Het is het fundament onder drie dingen die teams werkelijk willen.

Het eerste is caching. Als een build een pure functie is van zijn invoer, is zijn uitvoer-hash een cachesleutel. Dezelfde invoer, dezelfde hash, sla het werk over en serveer het opgeslagen bestand. Wanneer de bytes ronddwalen, dwaalt de hash rond, en de cache treft nooit raak.

Het tweede is manipulatiebewijs. Een pijplijn die het exacte bestand dat hij uitleverde opnieuw kan genereren, kan later bewijzen dat een gearchiveerd document niet is gewijzigd: herbouw het, hash beide, vergelijk. Als zelfs één byte verschilt door een ingebedde klok, is het bewijs weg en zit je weer op “vertrouw mij”.

Het derde is betrouwbare CI. Een golden-file-test legt een bekende-goede uitvoer vast en faalt wanneer een wijziging die verandert. Dat signaal is alleen betekenisvol als een ongewijzigde engine een ongewijzigd bestand reproduceert. Als elke run verschilt op een tijdstempel, is de golden file ruis, en het team leert een rode build te negeren — de duurste gewoonte in testen.

In het deterministische profiel van NextPDF zijn de twee door de engine beheerde velden die anders tussen identieke builds zouden uiteenlopen de datums en de /ID. Dit gaat ervan uit dat de rest van de pijplijn al stabiel is — dezelfde invoer, en een serialisatie die niet vanzelf varieert (meer daarover hieronder):

  • Ingebedde datums. De document information dictionary draagt CreationDate en ModDate (Spec: ISO 32000-2, §14.3.3), en de XMP-metadata spiegelt ze. Leg “nu” vast tijdens het bouwen en elke herbouw verschilt.
  • De bestandsidentifier. De /ID-array is een paar byte-strings die het bestand identificeren (Spec: ISO 32000-2, §14.4), opgeslagen in de trailer dictionary (Spec: ISO 32000-2, §7.5.5). Bibliotheken leiden hem meestal af uit de huidige tijd plus willekeurige bytes, zodat hij bij elke run anders is, bij ontwerp.

Pin beide vast — een vaste tijdstempel en een vaste seed voor de /ID — en de uitvoer wordt een deterministische functie van zijn inhoud. Laat de inhoud met rust en het bestand is byte voor byte identiek. Dit is dezelfde discipline die het Reproducible Builds-project vestigde voor gecompileerde software, toegepast op de documentlaag.

Determinisme in NextPDF is een configuratieobject, geen testhack. De engine stelt een DeterministicSettings-value object beschikbaar in de NextPDF\Core-namespace. Het is final readonly, immutable, en pint precies de twee klok- en willekeur-afgeleide bronnen van afwijking vast die hierboven zijn genoemd: de datums en de /ID. Ze vastpinnen verwijdert de twee meest voorkomende afwijkingsbronnen, maar het garandeert op zichzelf geen byte-identieke uitvoer. Het overige serialisatiegedrag van de engine — objectvolgorde, font-subsetting en compressie-instellingen — moet ook deterministisch zijn om de uitvoer te laten reproduceren, en NextPDF houdt die bij ontwerp stabiel.

Zijn constructor neemt twee argumenten:

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

$timestamp is het enkele vaste moment dat naar elk dateveld wordt geschreven — CreationDate, ModDate en hun XMP-spiegel. Geef één DateTimeImmutable mee en het document houdt op de wandklok te vragen hoe laat het is. $fileIdSeed is de invoer die de trailer-/ID vastpint: een 32-tekens hexadecimale string. Geef dezelfde seed en de engine leidt dezelfde bestandsidentifier af in plaats van de klok en een willekeurige bron te bemonsteren.

Het object valideert zijn eigen invoer. De seed moet precies 32 hexadecimale tekens zijn; al het andere wordt bij constructie afgewezen met een InvalidConfigException in plaats van stilletjes een anders-uitziende /ID te produceren. Dit is dezelfde weiger-te-gokken-houding die de rest van de engine inneemt — een dubbelzinnige invoer faalt luid in plaats van stilletjes de bytes te veranderen.

Met beide vastgepind is het recept dat wat het Reproducible Builds-project vertrouwd maakte: herbouw het, diff het, en de diff is leeg.

  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.
Reproduceerbare build: identieke invoer plus een vastgepinde tijdstempel en een vastgepinde /ID-seed produceren dezelfde bytes, wat een herbouw-en-diff-stap bevestigt.

Een kleine, complete vorm. De instellingen worden één keer geconstrueerd en hergebruikt, zodat twee runs van hetzelfde programma hetzelfde bestand uitsturen.

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

De seed is een build-invoer die je beheert, geen geheim. Bewaar hem naast de rest van je build-configuratie. Het punt is dat hij vast is, zodat de bestandsidentifier die hij produceert ook vast is.

De eerste valkuil is “ik heb de tijdstempel verwijderd, dus mijn build is nu reproduceerbaar.” Meestal is dat niet zo, omdat de /ID-array de stillere van de twee bronnen is. Datums zijn zichtbaar in een metadatapaneel en makkelijk te onthouden; de trailer-/ID is onzichtbaar voor de meeste readers en wordt bij elke run opnieuw gegenereerd uit de klok en een willekeurige bron. Een build die alleen de datums vastpint, produceert nog steeds elke keer een ander bestand. Je moet beide stil houden.

De tweede valkuil is determinisme op zichzelf als een beveiligingsfunctie behandelen. Een vastgepinde /ID maakt een bestand reproduceerbaar; het maakt het niet ondertekend, en het bewijst op zichzelf niet dat twee builds overeenkomen. Een byte-voor-byte-vergelijking of een hash bewijst dat de builds overeenkomen; de /ID vastpinnen verwijdert alleen één bron van schijnverschil. En geen van beide bewijst dat een derde partij voor het bestand instaat. Reproduceerbaarheid en ondertekenen zijn complementaire lagen, geen vervangers.

Determinisme pint de eigen bewegende delen van de engine vast. Het pint jouw invoer niet vast. Als je inhoud een levende tijdstempel inbedt, een lettertype trekt dat op schijf is veranderd, of een waarde rendert die van de huidige datum afhangt, verandert de uitvoer omdat de invoer veranderde — en dat is correct. DeterministicSettings verwijdert het non-determinisme van de engine, niet dat van jou. Een reproduceerbare build vereist nog steeds reproduceerbare invoer.

Deterministic byte-identical output — edition availability
EditionAvailability
Core

Full support. DeterministicSettings ships in the open-source core: pin the timestamp and the /ID seed and the same content rebuilds to the same bytes — no edition gate.

ProNot in this edition
EnterpriseNot in this edition
  • Golden-file-testing — de CI-techniek die afhangt van byte-identieke uitvoer, en waarom een deterministische engine de voorwaarde ervoor is.
  • Incrementele updates — hoe een PDF groeit door toevoegen, waar de /ID-array opnieuw van belang is om een bestand aan zijn eerdere versies te relateren.
  • Metadata en het XMP-packet — waar de ingebedde datums leven, en hoe het XMP-packet de document information dictionary spiegelt.
  • De anatomie van een PDF-bestand — de trailer, de cross-reference-tabel, en waar de /ID-array in de bestandsstructuur zit.
  • Byte-identiek — twee bestanden die exact overeenkomen, byte voor byte. De sterkste vorm van “hetzelfde”, en degene die een hash of een diff kan verifiëren.
  • /ID (bestandsidentifier) — de array van twee byte-strings die een PDF en zijn versies identificeert (ISO 32000-2 §14.4), opgeslagen in de trailer dictionary (§7.5.5). Meestal afgeleid uit de klok plus willekeurige bytes, en daarom verandert hij bij elke niet-vastgepinde build.
  • Document information dictionary — de structuur die CreationDate en ModDate draagt (ISO 32000-2 §14.3.3). Een van de twee bronnen van non-determinisme die een deterministische build moet vastpinnen.
  • Golden file — een vastgelegde bekende-goede uitvoer waar een test tegen vergelijkt; alleen betekenisvol wanneer een ongewijzigde engine een ongewijzigd bestand reproduceert.
  • Reproduceerbare build — een build waarvan de uitvoer een deterministische functie van zijn invoer is, zodat herbouwen uit dezelfde invoer dezelfde bytes oplevert. De term komt van het Reproducible Builds-project voor gecompileerde software.