HotPDF Delphi Component daje bajtno enak izhod PDF skozi shranjevanja, kadar je lastnost ReproducibleOutput nastavljena na True: datum /CreationDate in /ModDate v slovarju Info pritrdi na fiksni datum, identifikator dokumenta iz stenske ure nadomesti z zgoščeno vrednostjo iz semena ali iz vsebine, vsak naključni bajt, ki bi ga poti šifriranja AES sicer črpale, nadomesti s konstanto in vsak slovar, ki ga serializira, razvrsti. Zastavica obstaja za regresijske nize in primerjavo izdelkov gradnje, ne za dokumente v proizvodnji, in razlogi za to mejo so zanimivi del. Scenarij, ki je sprožil to funkcijo, je test zlatega izhoda. Upodobite račun, potrdite PDF v sistemu za nadzor različic in zahtevajte, da jutrišnja gradnja da iste bajte. Nikoli jih ne da. Datoteka se v vsakem pregledovalniku lepo odpre, besedilo je enako, drevo strani je enako, primerjava pa se vseeno zasveti na štirih ali petih mestih. Vsakdo, ki je skušal generator PDF postaviti pod regresijski test na ravni bajtov, je naletel na to steno, popravek pa ni »odstrani časovne žige«, ampak natančen obračun vsakega mesta, kjer pisec pogleda kam drugam kot v sam dokument
Zakaj se dve shranjevanji istega PDF razlikujeta?
Dve shranjevanji istega dokumenta se razlikujeta, ker pisec PDF, tudi HotPDF, pogleda v štiri vire entropije, ki nimajo nobene zveze z vsebino strani: stensko uro, identifikator dokumenta, kriptografski generator naključnih števil in vrstni red vnosov v slovarju v pomnilniku. Vsak od njih je sam po sebi upravičen. ISO 32000-1 jih hoče tam. Preprosto naredijo datoteko funkcijo tega, kdaj in kje je bila zapisana, namesto tega, kaj vsebuje
- Ura. Slovar Info nosi
/CreationDatein/ModDate(ISO 32000-1 §14.3.3, tabela 317) kot nizaD:YYYYMMDDHHmmSSs pripono časovnega pasu (§7.9.4), paket XMP pa isti trenutek ponovi kotxmp:CreateDateinxmp:ModifyDate. HotPDF oba odtisne izFCreationDate, ki ga konstruktor inicializira naNow, zato se shranjevanji razlikujeta v sekundi, v kateri sta bili zapisani - Identifikator. Niz
/IDv repu (ISO 32000-1 §14.4) hrani trajni identifikator in identifikator spremembe. HotPDF-ov privzeti recept za prvi element zgošča ime datoteke skupaj s trenutnim časom do milisekunde, za drugega pa zgošča to skupaj zGetTickCount. Dva identifikatorja, dve sveži vrednosti ob vsakem zagonu - Naključni bajti. Standardna zaščita je odvisna od identifikatorja in od resnične naključnosti. Pri AES-256 se ključ za šifriranje datoteke, soli za overjanje in ključ ter vsak inicializacijski vektor CBC črpajo iz sistemskega vira naključnosti (ISO 32000-2 §7.6.4.4.7 zahteva naključne soli). Ker so
/U,/UE,/Oin/OEvsi izračunani iz teh bajtov, se šifriran dokument spremeni v celoti, tudi kadar se čisto besedilo ne. Starejši algoritmi prvi element/IDvpnejo v ključ (ISO 32000-1 §7.6.3.3, §7.6.3.4), zato je svež identifikator sam dovolj, da se ključ datoteke zamenja - Vrstni red. Slovar PDF je neurejena preslikava in pisec, ki hodi po svojem seznamu v pomnilniku, izda ključe v vrstnem redu vstavljanja. Vsaka pot kode, ki slovar virov zgradi v drugačnem zaporedju, ali naložen dokument, razčlenjen iz drugačne postavitve, da zakonito, a besedilno drugačno datoteko
Kaj ReproducibleOutput pritrdi?
Nastavitev ReproducibleOutput := True pred BeginDoc ali pred SaveLoadedDocument vsakega od štirih virov nadomesti s fiksno vrednostjo, in to v istih poteh kode, ki bi sicer segle po uri ali po generatorju naključij, zato ločen prehod čiščenja ni potreben. Opazite, kaj na zgornjem seznamu manjka: vsebina. Pisave, tokovi strani, podatki slik in tabela medsebojnih sklicev so za isti vhod že deterministični; šum živi izključno v metapodatkih in v varnostni plasti, zato ga lahko odstrani ena ciljna lastnost. Lastnost je privzeto False in nič v knjižnici je ne vklopi namesto vas
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'golden-invoice.pdf';
Pdf.ReproducibleOutput := True; // pred BeginDoc
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-0042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Znotraj BeginDoc veja za ponovljivost dodeli FCreationDate := EncodeDate(2026, 1, 1) in identifikator dokumenta zasadi z MD5CalcString('HotPDF-reproducible-seed') namesto z zgoščeno vrednostjo imena datoteke in ure. Ta ena sama dodelitev pokrije oba datuma v slovarju Info in oba datuma v XMP, ker se vsi štirje izrišejo iz istega polja. Ko je datoteka nazadnje zapisana, BuildDocumentIdentifiers za identifikator v repu vpraša ComputeCanonicalDocumentIdentifier: ta izvozi celoten objektni graf v kanoničnem vrstnem redu, števke vsakega najdenega datumskega niza D: postavi na nič, da se časovni žigi ne morejo vrniti skozi zgoščeno vrednost, in vzame MD5 rezultata. Oba elementa /ID prejmeta to vrednost. Isti iz vsebine izpeljani identifikator se uporabi, kadar je naložen dokument šifriran, ne da bi sploh šel skozi BeginDoc, kar je primer ActivateProtection na datoteki, ki ste jo odprli s LoadFromFile
Naključni bajti so najmanj očitna zamenjava. Rutina ključa AES-256 svoj vir naključnosti ovije v lokalni pomožnik, ki ob zastavici pokliče FillChar(P^, Count, $5A) za 32-bajtni ključ za šifriranje datoteke in za vsako 8-bajtno sol, šifriranja nizov in tokov AES-128 ter AES-256 pa preklopijo z AESGenerateRandomIV na AESGenerateStaticIV, ki inicializacijski vektor za mesto I napolni s 14 * (1 + I). Ko so ključ, soli in vektorji vsi fiksni, se /U, /UE, /O, /OE in vsak šifriran tok ob drugem zagonu izidejo enaki. Nazadnje SaveToStream vklopi DeterministicDictionaryOrder vsakič, ko je zastavica ponovljivosti nastavljena, serializator pa nato vsak slovar uredi z vstavljanjem po surovih bajtih njegovih imen ključev, krajši predznak prvi, pri izenačenju pa odloči izvirni indeks. To je isti vrstni red, kot ga uporablja diagnostični pisec, opisan v članku o ročnem urejanju PDF in popravljanju po tem; zastavica ponovljivosti si izposodi le vrstni red in nič drugega iz besedilne postavitve tistega pisca
Zakaj je fiksni datum vseeno izdal stensko uro?
Popravek v v2.752.2 obstaja zato, ker je bil fiksni datum nastanka prvotno določen v konstruktorju, konstruktor pa ne more poznati lastnosti, ki je klicatelj še ni nastavil. Običajno zaporedje klicev je Create, nato ReproducibleOutput := True in nato BeginDoc. Ob izdelavi je FReproducibleOutput še False, zato je FCreationDate prejel Now in ga obdržal. Identifikator in naključni bajti so bili pravilno pritrjeni, zato sta se datoteki ujemali skoraj povsod in se razlikovali natanko v dveh datumskih nizih in dveh poljih XMP. Premik dodelitve v vejo za ponovljivost znotraj BeginDoc, k zasajenemu identifikatorju, je odločitev postavil na točko, kjer ima lastnost svojo končno vrednost
Regresijski test, ki je to spregledal, je vreden več kot popravek. Dve shranjevanji, ki se zgodita znotraj iste sekunde stenske ure, po naključju zapišeta isti niz D: in primerjava bajtov prestane pri napaki, ki na katerem koli počasnejšem stroju pade. Popravljeni test med obema shranjevanjema spi 1100 ms, da časovni žig PDF zagotovo prestopi mejo sekunde, preizkusi primer za navaden izhod ter za AES-128 in AES-256 z resničnimi gesli na obeh šifriranih različicah in primerja oba medpomnilnika s CompareMem, ob spodletelosti pa sporoči prvi različni odmik, tako da primerjava pokaže na konkreten objekt in ne na celo datoteko. Primerjava bajtov dokaže determinizem in nič drugega, zato obdržite ločeno trditev, ki šifriran izhod znova naloži z uporabniškim geslom in prebere število strani; sprememba, ki datoteko naredi stabilno in neberljivo hkrati, ne sme uiti na podlagi zelene primerjave
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;
// v telesu testa
A := SaveOnce(PathA);
TThread.Sleep(1100); // vsili drugo sekundo časovnega žiga PDF
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 ponovljiv šifriran PDF še varen?
Ne. Dokument, šifriran pod ReproducibleOutput, ni zaščiten v nobenem smiselnem pomenu in zastavica mora biti izklopljena za vse, kar zapusti testno mapo. Ključ AES-256 za šifriranje datoteke je dvaintrideset bajtov $5A, soli so osem bajtov $5A, inicializacijski vektorji pa sledijo objavljenemu aritmetičnemu vzorcu. Geslo še vedno varuje ovoja /UE in /OE, oviti ključ pa je konstanta, zato lahko vsakdo, ki konstanto pozna, dešifrira vsak tok vsebine povsem brez gesla. Fiksne soli odpravijo tudi edinstvenost na ravni dokumenta, na katero se ISO 32000-2 §7.6.4.4.7 zanaša, da enaka gesla v različnih datotekah ne dajo enakih nizov /U. Preberite članek o nastavitvi AES-256, kaj obljubljajo lastnosti šifriranja, ko je vir naključnosti nepoškodovan; pod zastavico ponovljivosti so te obljube začasno ukinjene
Kompromis pri identifikatorju je bolj subtilen. ISO 32000-1 §14.4 želi, da se drugi element /ID spremeni ob vsaki spremembi, da lahko orodja ločijo posodobljeno datoteko od njene predhodnice, ponovljivo shranjevanje pa v obe mesti zapiše isto vrednost. Ker je ta vrednost zgoščena vrednost kanoničnega objektnega grafa, dva dokumenta z različno vsebino vseeno dobita različna identifikatorja, kar je bolje od konstante. A seme, ki ga BeginDoc uporablja za izpeljavo ključa, je isti niz za vsak dokument na vsakem stroju, in bralec, ki po /ID ločuje datoteke, na primer predpomnilnik opomb ali spremljevalna datoteka podatkov obrazca, bo zamešal vsako ponovljivo datoteko, ki se slučajno zgošča enako
Česa zastavica ne pokriva?
ReproducibleOutput odstrani entropijo, ki jo pisec vnese sam; ne more odstraniti entropije, ki vstopa skozi okolje ali skozi poti kode, ki jih ne nadzoruje, tri od teh pa je lahko sprožiti
- Pripona časovnega pasu.
_DateTimeToPdfDatepriloži krajevni odmik od UTC, zato staD:20260101000000+08'00'na enem strežniku gradnje inD:20260101000000-05'00'na drugem različna bajta za isti fiksni datum. Ponovljivost velja med zagoni na enem stroju ali med stroji, ki si delijo časovni pas; če vaše zlate datoteke potujejo, pripnite pas strežnika - Inkrementalne posodobitve.
SaveIncrementalUpdatesvoj identifikator spremembe izračuna iz ciljne poti,GetTickCountin trenutnega časa, brez veje za ponovljivost, ker je inkrementalni odsek po definiciji nova sprememba. Primerjajte polne prepise in ne priloženih delt - Bližnjica prepustitve.
SaveLoadedDocumentobičajno nespremenjeno, nešifrirano izvorno datoteko prekopira bajt za bajt, namesto da bi jo znova serializiral. Zastavica ponovljivosti to bližnjico izklopi in vsili poln prepis, da veljajo pravila o vrstnem redu in identifikatorju, kar pomeni, da je ponovljivo shranjevanje naložene datoteke počasnejše od privzetega in nikoli ni kopija vhoda. Primerjajte ga s prejšnjim ponovljivim shranjevanjem in nikoli z izvirnikom
Še en nauk iz iste izdaje, o tem, kaj uspešno preverjanje dokaže in česa ne. Fiksna datoteka testa PDF/X-6 je poklicala CharProcs.DeleteValue('A'), kar je sprostilo neposredno držan tok glifa, nato pa znova vstavila isti kazalec, in ločeno predala en neposredni objekt ExtGState tako slovarju virov kot vzorcu. Preverjevalnik skladnosti je pri tem sproščanju med uporabo in dvojnem lastništvu vmes prestal, ker je bral, kar koli je sproščeni pomnilnik slučajno vseboval. Ko strukturno preverjanje utripa, poglejte lastništvo testnega vhoda, preden pogledate preverjevalnik. Ponovljiv izhod to disciplino poceni: ko sta dve shranjevanji bajtno enaki, je edini preostali vir utripanja objektni graf sam in strukturna primerjava od kataloga navzdol ga bo našla
Lastnosti ReproducibleOutput, DeterministicDictionaryOrder in šifriranje, opisane tukaj, se dobavljajo v standardnem HotPDF Delphi Component za Delphi in C++Builder, ista zastavica pa poganja tudi lastni regresijski korpus knjižnice, zato je vedenje, ki ga dobite v testnem nizu, vedenje, s katerim je komponenta testirana