Przejdź do głównej zawartości
getnextpdf.com

Te same bajty za każdym razem: powtarzalne pliki PDF

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

Zbuduj PDF z tych samych danych wejściowych dwukrotnie, a spodziewałbyś się tego samego pliku. Większość bibliotek PDF nie może tego obiecać — odbuduj i porównaj, a bajty się rozjadą. NextPDF potrafi ustalić te dwie rzeczy, które się zmieniają, więc te same dane wejściowe wytwarzają te same bajty, za każdym razem.

Identyczne bajt po bajcie wyjście to nie metryka próżności. To fundament pod trzema rzeczami, których zespoły naprawdę chcą.

Pierwszą jest buforowanie. Jeśli budowa jest czystą funkcją swoich danych wejściowych, skrót jej wyjścia jest kluczem bufora. Te same dane wejściowe, ten sam skrót, pomiń pracę i podaj zapisany plik. Gdy bajty błądzą, błądzi skrót, a bufor nigdy nie trafia.

Drugą jest wykrywalność manipulacji. Potok, który potrafi ponownie wygenerować dokładny plik, który wysłał, może później dowieść, że zarchiwizowany dokument nie został zmieniony: odbuduj go, policz skrót obu, porównaj. Jeśli choć jeden bajt się różni z powodu osadzonego zegara, dowód przepada, a wracasz do „zaufaj mi”.

Trzecią jest godne zaufania CI. Test na pliku wzorcowym zapisuje znane dobre wyjście i zawodzi, gdy zmiana je zmienia. Ten sygnał ma znaczenie tylko wtedy, gdy niezmieniony silnik odtwarza niezmieniony plik. Jeśli każde uruchomienie różni się znacznikiem czasu, plik wzorcowy to szum, a zespół uczy się ignorować czerwoną budowę — najdroższy nawyk w testowaniu.

W deterministycznym profilu NextPDF dwoma kontrolowanymi przez silnik polami, które inaczej błądziłyby między identycznymi budowami, są daty oraz /ID. Zakłada to, że reszta potoku jest już stabilna — te same dane wejściowe oraz serializacja, która nie zmienia się sama z siebie (więcej o tym poniżej):

  • Osadzone daty. Słownik informacji o dokumencie niesie CreationDate oraz ModDate (Spec: ISO 32000-2, §14.3.3), a metadane XMP je odzwierciedlają. Uchwyć „teraz” przy budowie, a każda odbudowa się różni.
  • Identyfikator pliku. Tablica /ID to para łańcuchów bajtów identyfikujących plik (Spec: ISO 32000-2, §14.4), przechowywana w słowniku trailera (Spec: ISO 32000-2, §7.5.5). Biblioteki zwykle wyprowadzają ją z bieżącego czasu plus losowych bajtów, więc jest z założenia inna przy każdym uruchomieniu.

Ustal oba — stały znacznik czasu i stałe ziarno dla /ID — a wyjście staje się deterministyczną funkcją swojej treści. Pozostaw treść w spokoju, a plik jest identyczny bajt po bajcie. To ta sama dyscyplina, którą projekt Reproducible Builds ustanowił dla oprogramowania kompilowanego, zastosowana do warstwy dokumentu.

Determinizm w NextPDF to obiekt konfiguracyjny, a nie testowa sztuczka. Silnik udostępnia obiekt wartości DeterministicSettings w przestrzeni nazw NextPDF\Core. Jest final readonly, niezmienialny i ustala dokładnie te dwa wyprowadzone z zegara i losowości źródła rozjazdu nazwane powyżej: daty oraz /ID. Ustalenie ich usuwa dwa najczęstsze źródła rozjazdu, ale samo w sobie nie gwarantuje identycznego bajt po bajcie wyjścia. Inne zachowanie serializacji silnika — kolejność obiektów, podzbiory czcionek oraz ustawienia kompresji — też musi być deterministyczne, by wyjście się odtwarzało, a NextPDF utrzymuje je stałe z założenia.

Jego konstruktor bierze dwa argumenty:

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

$timestamp to pojedyncza stała chwila zapisywana do każdego pola daty — CreationDate, ModDate oraz ich lustra w XMP. Podaj jeden DateTimeImmutable, a dokument przestaje pytać zegar ścienny, która jest godzina. $fileIdSeed to dane wejściowe, które ustalają trailerowe /ID: 32-znakowy łańcuch szesnastkowy. Podaj to samo ziarno, a silnik wyprowadzi ten sam identyfikator pliku, zamiast próbkować zegar i źródło losowe.

Obiekt waliduje własne dane wejściowe. Ziarno musi mieć dokładnie 32 znaki szesnastkowe; cokolwiek innego jest odrzucane przy konstrukcji za pomocą InvalidConfigException, zamiast po cichu wytworzyć inaczej wyglądające /ID. To ta sama postawa odmowy zgadywania, którą przyjmuje reszta silnika — niejednoznaczne dane wejściowe zawodzą głośno, zamiast po cichu zmieniać bajty.

Gdy oba są ustalone, przepis jest tym, który projekt Reproducible Builds spopularyzował: odbuduj go, porównaj, a różnica jest pusta.

  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.
Powtarzalna budowa: identyczne dane wejściowe plus ustalony znacznik czasu i ustalone ziarno /ID wytwarzają te same bajty, co potwierdza krok odbudowy-i-porównania.

Mały, kompletny kształt. Ustawienia są konstruowane raz i wielokrotnie używane, więc dwa uruchomienia tego samego programu emitują ten sam plik.

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

Ziarno to dane wejściowe budowy, które kontrolujesz, a nie sekret. Przechowuj je obok reszty swojej konfiguracji budowy. Sens jest taki, że jest stałe, więc identyfikator pliku, który wytwarza, też jest stały.

Pierwszą pułapką jest „usunąłem znacznik czasu, więc moja budowa jest teraz powtarzalna”. Zwykle nie jest, ponieważ tablica /ID to cichsze z dwóch źródeł. Daty są widoczne w panelu metadanych i łatwe do zapamiętania; trailerowe /ID jest niewidoczne dla większości czytników i jest regenerowane z zegara i źródła losowego przy każdym uruchomieniu. Budowa, która ustala tylko daty, wciąż wytwarza inny plik za każdym razem. Musisz utrzymać oba w bezruchu.

Drugą pułapką jest traktowanie determinizmu jako funkcji bezpieczeństwa samej w sobie. Ustalone /ID czyni plik powtarzalnym; nie czyni go podpisanym i samo w sobie nie dowodzi, że dwie budowy się zgadzają. Porównanie bajt po bajcie lub skrót dowodzi, że budowy się zgadzają; ustalenie /ID jedynie usuwa jedno źródło pozornej różnicy. A żadne z nich nie dowodzi, że strona trzecia ręczy za plik. Powtarzalność i podpisywanie to uzupełniające się warstwy, a nie substytuty.

Determinizm ustala własne ruchome części silnika. Nie ustala twoich danych wejściowych. Jeśli twoja treść osadza żywy znacznik czasu, pobiera czcionkę, która zmieniła się na dysku, lub renderuje wartość zależną od bieżącej daty, wyjście się zmienia, ponieważ zmieniły się dane wejściowe — i to jest poprawne. DeterministicSettings usuwa niedeterminizm silnika, a nie twój. Powtarzalna budowa wciąż wymaga powtarzalnych danych wejściowych.

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
  • Testowanie na plikach wzorcowych — technika CI, która zależy od identycznego bajt po bajcie wyjścia, oraz dlaczego deterministyczny silnik jest jej warunkiem wstępnym.
  • Aktualizacje przyrostowe — jak PDF rośnie przez dołączanie, gdzie tablica /ID znowu ma znaczenie dla powiązania pliku z jego wcześniejszymi wersjami.
  • Metadane i pakiet XMP — gdzie żyją osadzone daty oraz jak pakiet XMP odzwierciedla słownik informacji o dokumencie.
  • Anatomia pliku PDF — trailer, tablica odwołań krzyżowych oraz to, gdzie w strukturze pliku znajduje się tablica /ID.
  • Identyczny bajt po bajcie — dwa pliki, które pasują dokładnie, bajt po bajcie. Najsilniejsza postać „tego samego” i ta, którą może zweryfikować skrót lub porównanie.
  • /ID (identyfikator pliku) — tablica dwóch łańcuchów bajtów, która identyfikuje PDF i jego wersje (ISO 32000-2 §14.4), przechowywana w słowniku trailera (§7.5.5). Zwykle wyprowadzana z zegara plus losowych bajtów, dlatego zmienia się przy każdej nieustalonej budowie.
  • Słownik informacji o dokumencie — struktura, która niesie CreationDate oraz ModDate (ISO 32000-2 §14.3.3). Jedno z dwóch źródeł niedeterminizmu, które musi ustalić deterministyczna budowa.
  • Plik wzorcowy — zapisane znane dobre wyjście, z którym test porównuje; znaczące tylko wtedy, gdy niezmieniony silnik odtwarza niezmieniony plik.
  • Powtarzalna budowa — budowa, której wyjście jest deterministyczną funkcją jej danych wejściowych, więc odbudowa z tych samych danych wejściowych daje te same bajty. Termin pochodzi z projektu Reproducible Builds dla oprogramowania kompilowanego.