Gli stessi byte ogni volta: PDF riproducibili
Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3
In sintesi
Sezione intitolata “In sintesi”Si costruisca un PDF dagli stessi input due volte e ci si aspetterebbe lo stesso file. La maggior parte delle librerie PDF non può prometterlo — si ricostruisca e si confronti con diff, e i byte derivano. NextPDF può fissare le due cose che si muovono, così che gli stessi input producano gli stessi byte, ogni volta.
Perché è importante
Sezione intitolata “Perché è importante”L’output byte-identico non è una metrica di vanità. È la fondazione sotto tre cose che i team vogliono davvero.
La prima è il caching. Se una build è una funzione pura dei suoi input, l’hash del suo output è una chiave di cache. Stessi input, stesso hash, si salta il lavoro e si serve il file memorizzato. Quando i byte vagano, l’hash vaga, e la cache non fa mai centro.
La seconda è l’evidenza di manomissione. Una pipeline che può rigenerare il file esatto che ha spedito può provare, in seguito, che un documento archiviato non è stato alterato: lo si ricostruisce, si fa l’hash di entrambi, si confronta. Se anche un solo byte differisce a causa di un orologio incorporato, la prova è sparita e si è di nuovo a «fidati di me».
La terza è la CI affidabile. Un test golden-file registra un output noto-buono e fallisce quando una modifica lo altera. Quel segnale è significativo solo se un motore invariato riproduce un file invariato. Se ogni esecuzione differisce su un timestamp, il file golden è rumore, e il team impara a ignorare una build rossa — l’abitudine più costosa nel testing.
In breve
Sezione intitolata “In breve”Nel profilo deterministico di NextPDF, i due campi controllati dal motore che
altrimenti deriverebbero tra build identiche sono le date e l’/ID. Questo
presuppone che il resto della pipeline sia già stabile — gli stessi input, e una
serializzazione che non varia da sola (più su questo qui sotto):
- Date incorporate. Il document information dictionary porta
CreationDateeModDate(Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3), e i metadati XMP le rispecchiano. Si catturi «ora» al momento della build e ogni ricostruzione differisce. - L’identificatore del file. L’array
/IDè una coppia di byte string che identificano il file (Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4), memorizzata nel trailer dictionary (Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5). Le librerie di solito lo derivano dall’ora corrente più byte casuali, quindi è diverso a ogni esecuzione per progetto.
Si fissino entrambi — un timestamp fisso e un seed fisso per l’/ID — e l’output
diventa una funzione deterministica del suo contenuto. Si lasci stare il
contenuto e il file è byte-per-byte identico. Questa è la stessa disciplina che
il progetto Reproducible Builds ha stabilito per il software compilato, applicata
allo strato del documento.
Come NextPDF lo affronta
Sezione intitolata “Come NextPDF lo affronta”Il determinismo in NextPDF è un oggetto di configurazione, non un trucco di test.
Il motore espone un value object DeterministicSettings nel namespace
NextPDF\Core. È final readonly, immutabile, e fissa esattamente le due fonti
di deriva derivate dall’orologio e dal caso nominate sopra: le date e l’/ID.
Fissarle rimuove le due fonti di deriva più comuni, ma di per sé non garantisce
un output byte-identico. L’altro comportamento di serializzazione del motore —
l’ordinamento degli oggetti, il subsetting dei font e le impostazioni di
compressione — deve essere anch’esso deterministico affinché l’output si
riproduca, e NextPDF li mantiene stabili per progetto.
Il suo costruttore prende due argomenti:
public function __construct( public DateTimeImmutable $timestamp, public string $fileIdSeed,) { // ...}$timestamp è il singolo istante fisso scritto in ogni campo di data —
CreationDate, ModDate, e il loro riflesso XMP. Si passi un solo
DateTimeImmutable e il documento smette di chiedere all’orologio da parete che
ora è. $fileIdSeed è l’input che fissa l’/ID del trailer: una stringa
esadecimale di 32 caratteri. Si dia lo stesso seed e il motore deriva lo stesso
identificatore di file invece di campionare l’orologio e una fonte casuale.
L’oggetto valida il proprio input. Il seed deve essere esattamente 32 caratteri
esadecimali; qualsiasi altra cosa è rifiutata alla costruzione con una
InvalidConfigException anziché produrre silenziosamente un /ID dall’aspetto
diverso. Questa è la stessa postura rifiuta-di-indovinare che adotta il resto del
motore — un input ambiguo fallisce rumorosamente invece di cambiare in silenzio i
byte.
Con entrambi fissati, la ricetta è quella che il progetto Reproducible Builds ha reso familiare: lo si ricostruisce, lo si confronta con diff, e il diff è vuoto.
- 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.
Esempio pratico
Sezione intitolata “Esempio pratico”Una forma piccola e completa. Le impostazioni sono costruite una volta e riutilizzate, così che due esecuzioni dello stesso programma emettano lo stesso file.
<?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)Il seed è un input di build che si controlla, non un segreto. Lo si memorizzi accanto al resto della configurazione di build. Il punto è che è fisso, così che l’identificatore di file che produce sia anch’esso fisso.
Equivoco comune
Sezione intitolata “Equivoco comune”La prima trappola è «Ho rimosso il timestamp, quindi la mia build è ora
riproducibile». Di solito non lo è, perché l’array /ID è la più silenziosa
delle due fonti. Le date sono visibili in un pannello di metadati e facili da
ricordare; l’/ID del trailer è invisibile alla maggior parte dei lettori ed è
rigenerato dall’orologio e da una fonte casuale a ogni esecuzione. Una build che
fissa solo le date produce comunque un file diverso ogni volta. Si devono tenere
fermi entrambi.
La seconda trappola è trattare il determinismo come una funzionalità di sicurezza
di per sé. Un /ID fissato rende un file riproducibile; non lo rende
firmato, e di per sé non prova che due build coincidano. Un confronto
byte-per-byte o un hash prova che le build coincidono; fissare l’/ID rimuove
solo una fonte di differenza spuria. E nessuno dei due prova che una terza parte
garantisca per il file. Riproducibilità e firma sono strati complementari, non
sostituti.
Limiti e confini
Sezione intitolata “Limiti e confini”Il determinismo fissa le parti mobili del motore stesso. Non fissa i tuoi
input. Se il tuo contenuto incorpora un timestamp vivo, attinge a un font che è
cambiato su disco, o renderizza un valore che dipende dalla data corrente,
l’output cambia perché l’input è cambiato — e questo è corretto.
DeterministicSettings rimuove il non-determinismo del motore, non il tuo. Una
build riproducibile richiede comunque input riproducibili.
| Edition | Availability |
|---|---|
| Core | Pieno supporto. |
| Pro | Not in this edition |
| Enterprise | Not in this edition |
Documenti correlati
Sezione intitolata “Documenti correlati”- Test golden-file — la tecnica di CI che dipende dall’output byte-identico, e perché un motore deterministico è la sua precondizione.
- Aggiornamenti incrementali — come un PDF
cresce per accodamento, dove l’array
/IDconta di nuovo per relazionare un file alle sue versioni precedenti. - Metadati e pacchetto XMP — dove vivono le date incorporate, e come il pacchetto XMP rispecchia il document information dictionary.
- L’anatomia di un file PDF — il
trailer, la cross-reference table, e dove si trova l’array
/IDnella struttura del file.
Glossario
Sezione intitolata “Glossario”- Byte-identico — due file che coincidono esattamente, byte per byte. La forma più forte di «lo stesso», e quella che un hash o un diff può verificare.
/ID(identificatore di file) — l’array di due byte string che identifica un PDF e le sue versioni (ISO 32000-2 §14.4), memorizzato nel trailer dictionary (§7.5.5). Di solito derivato dall’orologio più byte casuali, motivo per cui cambia a ogni build non fissata.- Document information dictionary — la struttura che porta
CreationDateeModDate(ISO 32000-2 §14.3.3). Una delle due fonti di non-determinismo che una build deterministica deve fissare. - File golden — un output noto-buono registrato con cui un test si confronta; significativo solo quando un motore invariato riproduce un file invariato.
- Build riproducibile — una build il cui output è una funzione deterministica dei suoi input, così che ricostruire dagli stessi input produca gli stessi byte. Il termine viene dal progetto Reproducible Builds per il software compilato.