HotPDF Delphi Component proizvodi bajt identičan PDF izlaz kroz spremanja kad je svojstvo ReproducibleOutput postavljeno na True: fiksira Info /CreationDate i /ModDate na fiksni datum, zamjenjuje identifikator dokumenta vezan na zidni sat hashom iz sjemena ili iz sadržaja, zamjenjuje konstante za svaki slučajni bajt koji bi AES putevi šifriranja inače izvukli, i sortira svaki rječnik koji serijalizira. Zastavica postoji za regresijske suiteove i usporedbu build artefakata, ne za produkcijske dokumente, a razlozi za tu granicu najzanimljiviji su dio. Scenarij koji pokreće tu značajku je golden-file test. Renderirate fakturu, committate PDF i tvrdite da će sutrašnji build proizvesti iste bajtove. Nikad ne proizvede. Datoteka se otvara bez problema u svakom pregledniku, tekst je identičan, stablo stranica je identično, a diff se ipak upali na četiri ili pet mjesta. Svatko kome je palo na pamet staviti PDF generator pod regresijski test na razini bajtova udario je u taj zid, a popravak nije "izbaci vremenske žigove", nego točan popis svakog mjesta na kojem writer konzultira nešto drugo osim samog dokumenta
Zašto se dva spremanja istog PDF-a razlikuju?
Dva spremanja istog dokumenta razlikuju se jer PDF writer, HotPDF uključujući, konzultira četiri izvora entropije koji nemaju nikakve veze sa sadržajem stranica: zidni sat, identifikator dokumenta, kriptografski generator slučajnih brojeva i memorijski redoslijed unosa u rječniku. Svaki je sam po sebi legitiman. ISO 32000-1 želi da budu ondje. Oni jednostavno čine datoteku funkcijom toga kad i gdje je zapisana, a ne čega sadrži
- Sat. Info rječnik nosi
/CreationDatei/ModDate(ISO 32000-1 §14.3.3, Tablica 317) kao stringoveD:YYYYMMDDHHmmSSs nastavkom vremenske zone (§7.9.4), a XMP paket ponavlja isti trenutak kaoxmp:CreateDateixmp:ModifyDate. HotPDF oba žigoše izFCreationDate, koju konstruktor inicijalizira naNow, pa se dva spremanja razlikuju u sekundi u kojoj su zapisana - Identifikator. Niz
/IDu traileru (ISO 32000-1 §14.4) drži trajni identifikator i identifikator izmjene. HotPDF-ov zadani recept hashira ime datoteke zajedno s trenutačnim vremenom do milisekunde za prvi element, a to plusGetTickCountza drugi. Dva identifikatora, dvije svježe vrijednosti u svakom pokretanju - Slučajni bajtovi. Standardna sigurnost ovisi o identifikatoru i o istinskoj slučajnosti. Za AES-256 ključ šifriranja datoteke, soli za validaciju i ključ te svaki CBC initialization vector izvlače se iz sistemskog izvora slučajnosti (ISO 32000-2 §7.6.4.4.7 zahtijeva slučajne soli). Budući da se
/U,/UE,/Oi/OEsvi računaju iz tih bajtova, šifrirani dokument mijenja se u cijelosti čak i kad se čisti tekst ne mijenja. Stariji algoritmi uklapaju prvi element/IDu ključ (ISO 32000-1 §7.6.3.3, §7.6.3.4), pa je svježi identifikator sam dovoljan da datoteku ponovno ključa - Redoslijed. PDF rječnik je neuređeno preslikavanje, a writer koji prolazi kroz svoju memorijsku listu emitira ključeve u redoslijedu umetanja. Svaki put koda koji gradi resource rječnik drugim slijedom, ili učitan dokument parsiran iz drugog rasporeda, daje zakonitu, ali tekstualno drukčiju datoteku
Što ReproducibleOutput prikiva?
Postavljanje ReproducibleOutput := True prije BeginDoc ili prije SaveLoadedDocument zamjenjuje svaki od četiriju izvora fiksnom vrijednošću, i to u istim putovima koda koji bi inače posegnuli za satom ili generatorom slučajnosti, pa zaseban prolaz čišćenja nije potreban. Primijetite što nedostaje na gornjem popisu: sadržaj. Fontovi, page streamovi, podaci slika i cross-reference tablica već su deterministički za isti ulaz; šum živi isključivo u metapodacima i sigurnosnom sloju, zato ga jedno ciljano svojstvo može ukloniti. Svojstvo je prema zadanim postavkama False i ništa ga u biblioteci ne uključuje umjesto vas
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'golden-invoice.pdf';
Pdf.ReproducibleOutput := True; // prije BeginDoc
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-0042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Unutar BeginDoc reproducibilna grana dodjeljuje FCreationDate := EncodeDate(2026, 1, 1) i zasijava identifikator dokumenta s MD5CalcString('HotPDF-reproducible-seed') umjesto digestom imena datoteke i sata. Ta jedna dodjela pokriva i Info datume i XMP datume, jer se sva četiri renderiraju iz istog polja. Kad se datoteka konačno zapiše, BuildDocumentIdentifiers traži od ComputeCanonicalDocumentIdentifier identifikator za trailer: on izvozi cijeli objektni graf u kanonskom redoslijedu, nulira znamenke svakog D: datumskog stringa koji nađe da se vremenski žigovi ne mogu vratiti kroz hash, i uzima MD5 rezultata. Oba elementa /ID dobivaju tu vrijednost. Isti identifikator izveden iz sadržaja koristi se kad se učitan dokument šifrira bez da ikad prođe kroz BeginDoc, što je slučaj s ActivateProtection na datoteci koju ste otvorili s LoadFromFile
Slučajni bajtovi najmanje su očita zamjena. AES-256 rutina za ključ omata svoj izvor slučajnosti lokalnim pomoćnikom koji pod tom zastavicom poziva FillChar(P^, Count, $5A) za 32-bajtni ključ šifriranja datoteke i za svaku 8-bajtnu sol, a AES-128 i AES-256 enkriptori stringova i streamova prebacuju se s AESGenerateRandomIV na AESGenerateStaticIV, koji initialization vector puni s 14 * (1 + I) za slot I. Uz fiksirane ključ, soli i vektore, /U, /UE, /O, /OE i svaki šifrirani stream izlaze identični u drugom pokretanju. Konačno, SaveToStream uključuje DeterministicDictionaryOrder kad god je reproducibilna zastavica postavljena, a serializer zatim insertion-sortira svaki rječnik po sirovim bajtovima imena ključeva, kraći prefiks prvi, s izvornim indeksom kao razrješivačem jednakosti. To je isti redoslijed koji koristi dijagnostički writer, opisan u članku o ručnom uređivanju PDF-a i njegovom popravku nakon toga; reproducibilna zastavica posuđuje samo redoslijed, ne i ostatak plain-text layouta tog writera
Zašto je fiksni datum ipak propuštao zidni sat?
Popravak v2.752.2 postoji jer se fiksni datum izrade izvorno određivao u konstruktoru, a konstruktor ne može znati svojstvo koje pozivatelj još nije postavio. Normalan slijed poziva je Create, zatim ReproducibleOutput := True, pa BeginDoc. U trenutku konstrukcije FReproducibleOutput je još False, pa je FCreationDate primio Now i zadržao ga. Identifikator i slučajni bajtovi bili su ispravno prikiveni, pa su se dvije datoteke slagale gotovo svugdje, a razlikovale točno u dva datumska stringa i dva XMP polja. Premještanje dodjele u reproducibilnu granu BeginDoc, uz zasijani identifikator, smjestilo je odluku na točku gdje svojstvo ima svoju konačnu vrijednost
Regresijski test koji je to propustio vrijedi više od samog popravka. Dva spremanja koja se oba izvrše unutar iste sekunde zidnog sata slučajno zapisuju isti D: string, a usporedba bajtova prolazi za bug koji pada na svakom sporijem stroju. Ispravljeni test spava 1100 ms između dvaju spremanja pa PDF vremenski žig zajamčeno prijeđe granicu sekunde, pokreće slučaj za običan, AES-128 i AES-256 izlaz s pravim lozinkama na dvjema šifriranim varijantama, i uspoređuje dva buffera s CompareMem, prijavljujući prvi offset koji se razlikuje kako bi diff pokazao na konkretan objekt umjesto na cijelu datoteku. Usporedba bajtova dokazuje determinizam i ništa drugo, pa čuvajte zasebnu tvrdnju koja ponovno učitava šifrirani izlaz s korisničkom lozinkom i čita broj stranica; promjena koja datoteku čini stabilnom i nečitljivom istodobno ne smije se provući na krilima zelenog diffa
function SaveOnce(const Target: string): TBytes;
var
Pdf: THotPDF;
Stream: TFileStream;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := Target;
Pdf.ReproducibleOutput := True;
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.CryptKeyLength := aes256;
Pdf.ActivateProtection := True;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'reproducible save');
Pdf.EndDoc;
finally
Pdf.Free;
end;
Stream := TFileStream.Create(Target, fmOpenRead or fmShareDenyWrite);
try
SetLength(Result, Stream.Size);
if Stream.Size > 0 then
Stream.ReadBuffer(Result[0], Stream.Size);
finally
Stream.Free;
end;
end;
// u tijelu testa
A := SaveOnce(PathA);
TThread.Sleep(1100); // prisili drugu sekundu PDF vremenskog žiga
B := SaveOnce(PathB);
Assert.AreEqual<Integer>(Length(A), Length(B));
Assert.IsTrue(CompareMem(@A[0], @B[0], Length(A)),
'two saves under ReproducibleOutput must be byte-identical');
Je li reproducibilni šifrirani PDF još siguran?
Ne. Dokument šifriran pod ReproducibleOutput nije zaštićen ni u kakvom smislenom smislu, i zastavica mora biti isključena za sve što izlazi iz testnog direktorija. AES-256 ključ šifriranja datoteke trideset i dva su bajta $5A, soli su osam bajtova $5A, a initialization vectori slijede objavljeni aritmetički obrazac. Lozinka i dalje čuva omotače /UE i /OE, ali zamotani ključ je konstanta, pa svatko tko zna konstantu može dekriptirati svaki content stream bez lozinke uopće. Fiksirane soli uklanjaju i jedinstvenost po dokumentu na koju se ISO 32000-2 §7.6.4.4.7 oslanja da iste lozinke ne daju iste /U stringove kroz datoteke. Pročitajte članak o postavljanju AES-256 za ono što svojstva šifriranja obećavaju kad je izvor slučajnosti netaknut; pod reproducibilnom zastavicom ta obećanja su suspendirana
Kompromis oko identifikatora suptilniji je. ISO 32000-1 §14.4 namjerava da se drugi element /ID mijenja pri svakoj izmjeni kako bi alati mogli razlikovati ažuriranu datoteku od njezina pretka, a reproducibilno spremanje upisuje istu vrijednost u oba slota. Budući da je ta vrijednost hash kanonskog objektnog grafa, dva dokumenta s različitim sadržajem i dalje dobivaju različite identifikatore, što je bolje od konstante. Ali sjeme koje BeginDoc koristi za izvođenje ključa isti je string za svaki dokument na svakom stroju, a čitač koji se oslanja na /ID da razlikuje datoteke, recimo cache napomena ili sidecar s podacima obrasca, poistovjetit će svaku reproducibilnu datoteku koja se slučajno hashira jednako
Što zastavica ne pokriva?
ReproducibleOutput uklanja entropiju koju writer uvodi sam; ne može ukloniti entropiju koja ulazi kroz okolinu ili kroz putove koda koje ne kontrolira, a o tri je lako spotaknuti se
- Nastavak vremenske zone.
_DateTimeToPdfDatedodaje lokalni UTC offset, pa suD:20260101000000+08'00'na jednom build agentu iD:20260101000000-05'00'na drugom različiti bajtovi za isti fiksni datum. Reproducibilnost vrijedi kroz pokretanja na jednom stroju ili na strojevima koji dijele vremensku zonu; prikivite zonu agenta ako vaše golden datoteke putuju - Inkrementalna ažuriranja.
SaveIncrementalUpdateračuna svoj identifikator izmjene iz ciljne putanje,GetTickCounti trenutačnog vremena bez reproducibilne grane, jer je inkrementalna sekcija po definiciji nova izmjena. Uspoređujte pune rewriteove, a ne dodane delte - Passthrough prečac.
SaveLoadedDocumentinače kopira neizmijenjenu, nešifriranu izvornu datoteku bajt po bajt umjesto da je ponovno serijalizira. Reproducibilna zastavica isključuje taj prečac i prisiljava puni rewrite da bi pravila o redoslijedu i identifikatoru vrijedila, što znači da je reproducibilno spremanje učitane datoteke sporije od zadanog i nikad nije kopija ulaza. Diffajte ga prema prethodnom reproducibilnom spremanju, nikad prema izvorniku
Još jedna lekcija iz istog izdanja, o tome što prolazna provjera dokazuje, a što ne. PDF/X-6 testni uzorak pozvao je CharProcs.DeleteValue('A'), što je oslobodilo izravno držani stream glifa, zatim ponovno umetnuo isti pokazivač, i odvojeno predao jedan izravni ExtGState objekt i resource rječniku i patternu. Conformance validator prolazio je isprekidano na tom use-after-freeu i dvostrukom vlasništvu jer je čitao što god se slučajno našlo u oslobođenoj memoriji. Kad strukturna provjera treperi, pogledajte vlasništvo testnog ulaza prije nego pogledate validator. Reproducibilni izlaz tu disciplinu čini jeftinijom: kad su dva spremanja bajt identična, jedini preostali izvor treperenja je sam objektni graf, a strukturni diff od kataloga prema dolje će ga naći
Svojstva ReproducibleOutput, DeterministicDictionaryOrder i svojstva šifriranja opisana ovdje isporučuju se u standardnom HotPDF Delphi Component za Delphi i C++Builder, a ista zastavica pokreće i vlastiti regresijski korpus biblioteke, pa je ponašanje koje dobivate u testnom suiteu ponašanje s kojim je komponenta testirana