losLab PDF Library dokáže vyprodukovať bajtovo identický PDF výstup pre identický vstup, akonáhle zavoláte SetDeterministicDocumentID(1). V predvolenom nastavení je pole /ID v trailer-i MD5 digest hodinového času, takže dva behy toho istého generátora sa líšia prinajmenšom v týchto bajtoch. Deterministický režim namiesto toho odvodzuje /ID zo stabilného seedu, čím obnovuje reprodukovateľné buildy
Príznak sa zvyčajne prejaví v CI skôr, než ho niekto začne cielene hľadať. Šablóna sa nezmenila, vstupný záznam sa nezmenil, fonty sa nezmenili, a vygenerovaný PDF má pri každom behu pipeline iný hash. Build cache nikdy netrafí. Content-addressable úložisko hromadí nový blob za každý nočný build. Diff na úrovni bajtov sa rozsvieti pri súboroch, ktorých sa nikto nedotkol. Keď diff dotiahnete až k skutočným bajtom, takmer vždy pôjde o tú istú hŕstku hex číslic sedaacich v trailer-i súboru
Na čo slúži pole ID v trailer-i
Trailer /ID je značka identity súboru, nie kontrolný súčet obsahu. ISO 32000-1 §14.4 ho definuje ako pole dvoch bajtových reťazcov: prvý prvok je trvalý identifikátor priradený pri vytvorení dokumentu a má prežiť každú neskoršiu úpravu, druhý prvok je meniaci sa identifikátor, ktorý writer obnovuje pri každej zmene súboru. Spolu umožňujú systému rozhodnúť, či sú dva súbory revíziami jedného dokumentu, alebo dvoma nesúvisiacimi dokumentmi. §7.5.5 robí túto položku v praxi prakticky povinnou, keďže trailer musí niesť /ID vždy, keď nesie aj /Encrypt
Špecifikácia nehovorí nič o tom, ako hodnotu vypočítať. Odporúčaním je digest vecí ako aktuálny čas, cesta k súboru, veľkosť súboru a slovník informácií o dokumente, pričom práve hodinový čas je ingrediencia, ktorá robí výsledok unikátnym. To je presne tá vlastnosť, ktorú chcete pre identitu, a presne tá vlastnosť, ktorá ničí reprodukovateľnosť, a preto to musí byť explicitný prepínač, nie tichá zmena správania
Prečo rovnaký build vytvára pri každom behu iný PDF?
Pretože predvolený identifikátor sa odvodzuje z okamihu generovania. Historicky losLab PDF Library zostavovala reťazce /ID z MD5 aktuálnej časovej pečiatky, takže dokument vytvorený dvakrát o sekundu neskôr nesie dva rozdielne trvalé identifikátory, aj keď každý ostatný bajt v súbore je identický. Náklady po prúde sú reálne: build systém, ktorý indexuje artefakty podľa hashu, nikdy nemôže znovu použiť PDF krok, deduplikujúce objektové úložisko drží jednu kópiu na build namiesto jednej kópie na dokument, a recenzent pozerajúci sa na binárny diff musí dokázať, že jedinou zmenou je šum, skôr než začne dôverovať zvyšku diffu. Deterministická generácia /ID existuje na to, aby tento šum odstránila, v rovnakom duchu ako práca na stabilite layoutu opísaná v poznámkach o object streams a cross reference streams
Prechod na reprodukovateľný identifikátor
Deterministický režim je opt-in, je nastavovaný na jeden dokument, a je predvolene vypnutý, takže existujúci výstup zostáva nezmenený, kým oň nepožiadate. SetDeterministicDocumentID prijíma 0 alebo 1 a vracia 1, ak bola hodnota prijatá, 0 pri čomkoľvek mimo rozsahu; GetDeterministicDocumentID hlási aktuálny stav. SetDocumentIDSeed dodáva explicitný reťazec seedu, ktorý má prednosť pred všetkým ostatným, a odovzdanie prázdneho seedu vráti späť odvodený seed. GetDocumentFileID po uložení prečíta späť /ID[0], takže ho môžete logovať alebo naň asserovat
var
Lib: TPDFlib;
FileID: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed('invoice-4471-rev3');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Invoice 4471');
Lib.SaveToFile('invoice.pdf');
FileID := Lib.GetDocumentFileID; // identical on every run
finally
Lib.Free;
end;
end;
K obnoveniu dochádza v momente uloženia, nie vtedy, keď prepnete príznak, takže zapnutie deterministického režimu neskoro počas zostavovania dokumentu sa stále prejaví. To tiež znamená, že zmenený seed sa do súboru dostane pri ďalšom úplnom uložení: nastavte seed A, uložte, nastavte seed B, uložte, a tieto dva súbory budú niesť rozdielne identifikátory, zatiaľ čo obnovenie seedu A obnoví pôvodnú hodnotu. Explicitný seed je správna voľba vždy, keď má váš dokument prirodzený stabilný kľúč, ako je číslo faktúry, revízia záznamu alebo identifikátor git commitu, pretože oddeľuje identifikátor od náhodných metadát
Odkiaľ pochádza seed, keď žiadny nedodáte?
Bez explicitného seedu losLab PDF Library odvodí seed zo stavu dokumentu, ktorý by mal byť invariantný naprieč identickými regeneráciami: hlavička verzie PDF, počet strán a každá položka v slovníku informácií o dokumente. Hodnoty reťazcov a mien sa berú doslovne, iné typy objektov prispievajú svojou serializovanou formou, a celá vec sa zahashuje do reťazcov /ID. Dôležitý dôsledok je, že CreationDate a ModDate sú súčasťou slovníka informácií, a teda zámerne aj súčasťou seedu. Dva behy získajú rovnaký identifikátor len vtedy, keď skutočne vyprodukujú rovnaké metadáta dokumentu
Lib.SetDeterministicDocumentID(1);
// No SetDocumentIDSeed: the seed is derived from document state,
// so the timestamps in the Info dictionary have to be pinned.
Lib.SetInformation(2, 'Quarterly Report'); // Title
Lib.SetInformation(5, 'reporting-service 4.2'); // Creator
Lib.SetInformation(7, 'D:20260101000000Z'); // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z'); // ModDate
Lib.SaveToFile('report.pdf');
Upnutie ModDate pomocou kľúča 8 plní dvojitú úlohu, a práve toto je časť, ktorá ľudí zaskočí. Samotné deterministické /ID nerobí súbor bajtovo identickým, pretože cesta ukladania orazítkuje ModDate aktuálnym časom, pokiaľ volajúci hodnotu explicitne nenastavil. Nastavenie kľúča 8 označí hodnotu ako dodanú volajúcim a potlačí toto orazítkovanie. Ak chcete reprodukovateľný súbor, nielen reprodukovateľný identifikátor, zaobchádzajte s časovými pečiatkami metadát ako s build vstupmi: odvoďte ich zo zdrojového záznamu alebo z fixnej epochy, nikdy z Now
Prečo prepísanie ID poruší šifrovaný PDF?
Pretože /ID[0] nie sú v šifrovanom dokumente len metadáta, sú to kľúčové materiály. ISO 32000-1 §7.6.3.3 Algoritmus 2 vkladá prvý prvok identifikátora súboru do výpočtu šifrovacieho kľúča pre štandardný bezpečnostný handler pri revíziách 2 až 4, spolu s doplneným heslom, hodnotou /O a bitmi oprávnení. Odvodený kľúč potom vyprodukuje validačný reťazec /U, ktorý reader kontroluje pri otváraní, a súborový kľúč sa odvodí a zapamätá pri volaní Encrypt, alebo pri načítaní šifrovaného dokumentu, obe udalosti nastanú pred uložením. Prepísanie identifikátora počas uloženia by preto vyprodukovalo štrukturálne platný súbor, ktorého kontrola /U pri opätovnom otvorení zlyhá: nie jemné poškodenie, ale dokument, ktorý nikto nedokáže otvoriť, vrátane vás. Preto je deterministické obnovenie obmedzené na dokumenty, ktoré nenesú stav šifrovania, a preto si šifrovaný dokument ponecháva akékoľvek /ID, ktoré už mal, deterministický režim alebo nie, a nastavenie na tejto ceste jednoducho nemá žiadny účinok. Súvisiace spracovanie revízií a sémantika oprávnení sú pokryté v prehľade auditovania PDF šifrovania a oprávnení. Všimnite si tiež, že cesta obnovy šifrovania obnovuje iba /ID[1], identifikátor zmeny, presne tak, ako to zamýšľa §14.4
Prečo si inkrementálne uloženia zachovávajú pôvodný identifikátor
Druhá hranica je režim pripájania. Inkrementálna aktualizácia ponecháva každý skorší bajt súboru nedotknutý a zapisuje novú revíziu za neho, a práve trvalosť /ID[0] naprieč §14.4 hovorí konzumentovi, že nová revízia patrí k tomu istému dokumentu ako tá stará. Jeho prepísanie by toto prepojenie prerušilo, protirečilo by revíziám už sediacim v súbore a zasahovalo by do sémantiky podpisov, keďže podpis pokrýva rozsah bajtov konkrétnej revízie konkrétneho dokumentu. losLab PDF Library preto obnovuje deterministický identifikátor iba pri úplných uloženiach a nikdy počas režimu pripájania, čo zachováva garanciu opísanú v článku o inkrementálnych aktualizáciách PDF a pripájaní do streamu nedotknutú
Jedno hrdlo fľaše pre generovanie identifikátorov
Všetka generácia /ID v losLab PDF Library teraz prechádza jedinou internou rutinou, NewFileIDString, čo je to, čo robí deterministický prepínač dôveryhodným namiesto záplaty na jednej ceste kódu. Vytvorenie prázdneho dokumentu, lenivé vytvorenie chýbajúceho poľa /ID na požiadanie a cesta obnovy odtlačku šifrovania — všetky ju volajú, takže existuje presne jedno miesto, kde by mohol hodinový čas znovu preniknúť dovnútra. Znamená to tiež, že budúce varianty, ako identifikátor odvodený z obsahu, sú zmenou jednej funkcie, nie auditom celého serializátora
function BuildQuote(const Seed: WideString): AnsiString;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed(Seed);
Lib.SetInformation(7, 'D:20260101000000Z');
Lib.SetInformation(8, 'D:20260101000000Z');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Quote 8812');
Result := Lib.SaveToString;
finally
Lib.Free;
end;
end;
// Regression guard: two independent builds, one byte sequence.
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
WriteLn('reproducible')
else
WriteLn('nondeterminism leaked into the output');
Zapojte toto porovnanie do svojej testovacej sady skôr, než sa niekde inde spoľahnete na reprodukovateľný výstup, pretože nahlas zlyhá vo chvíli, keď nejaká nová funkcia znovu zavedie časovú pečiatku. Reprodukovateľnosť je vlastnosť, ktorá inak ticho degraduje, a jediný assert nad dvoma uloženiami v pamäti stojí pri každom builde takmer nič
API deterministického identifikátora ukázané tu je súčasťou losLab PDF Library pre Delphi a C++Builder, spolu s úplnou referenciou informácií o dokumente, šifrovania a inkrementálneho ukladania