losLab PDF Library kan produsere byte-identisk PDF-utdata for identisk inndata når du kaller SetDeterministicDocumentID(1). Som standard er trailer-/ID-arrayet en MD5-digest av veggklokken, så to kjøringer av samme generator skiller seg i minst de bytene. Deterministisk modus utleder /ID fra et stabilt frø i stedet, som gjenoppretter reproduserbare byggeprosesser
Symptomet dukker vanligvis opp i CI før noen begynner å lete etter det. Malen har ikke endret seg, inndataposten har ikke endret seg, fontene har ikke endret seg, og den genererte PDF-en gir likevel forskjellig hash på hver pipeline-kjøring. Byggecacher treffer aldri. Innholdsadresserbar lagring samler opp en fersk blob per nattlige bygg. Byte-nivå regresjonsdiffer lyser opp på filer ingen har rørt. Følg diffen ned til de faktiske bytene, og det er nesten alltid de samme håndfull heksadesimale sifrene som ligger i filens trailer
Hva trailer-ID-arrayet er til for
/ID i traileren er en filidentitetsmarkør, ikke en sjekksum av innholdet. ISO 32000-1 §14.4 definerer det som en array med to bytestrenger: det første elementet er den permanente identifikatoren som tildeles når dokumentet opprettes og som er ment å overleve hver senere redigering, og det andre elementet er den skiftende identifikatoren som en skriver oppdaterer hver gang filen endres. Sammen lar de et system avgjøre om to filer er revisjoner av ett dokument eller to urelaterte dokumenter. §7.5.5 gjør oppføringen effektivt obligatorisk i praksis, siden traileren må inneholde /ID når den også inneholder /Encrypt
Ingenting i spesifikasjonen sier hvordan verdien skal beregnes. Anbefalingen er en digest av ting som gjeldende tid, filbanen, filstørrelsen og dokumentinformasjonsordboken, og veggklokken er ingrediensen som gjør resultatet unikt. Det er nøyaktig egenskapen du vil ha for identitet og nøyaktig egenskapen som ødelegger reproduserbarhet, som er grunnen til at dette må være en eksplisitt bryter i stedet for en stille atferdsendring
Hvorfor produserer samme bygg en annen PDF hver gang?
Fordi standardidentifikatoren utledes fra genereringsøyeblikket. Historisk bygde losLab PDF Library /ID-strengene fra en MD5 av gjeldende tidsstempel, så et dokument opprettet to ganger ett sekund fra hverandre bærer to forskjellige permanente identifikatorer selv når hver annen byte i filen er identisk. Kostnaden nedstrøms er reell: et byggesystem som nøkler artefakter etter hash kan aldri gjenbruke et PDF-trinn, en deduplikerende objektlagring beholder én kopi per bygg i stedet for én kopi per dokument, og en anmelder som ser på en binær diff må bevise at den eneste endringen er støy før resten av diffen kan stoles på. Deterministisk /ID-generering finnes for å fjerne den støyen, i samme ånd som layoutstabilitetsarbeidet beskrevet i notatene om objektstrømmer og kryssreferansestrømmer
Å bytte til en reproduserbar identifikator
Deterministisk modus er valgfri, per dokument, og av som standard, slik at eksisterende utdata er uendret inntil du ber om det. SetDeterministicDocumentID aksepterer 0 eller 1 og returnerer 1 når verdien ble akseptert, 0 for alt utenfor det området; GetDeterministicDocumentID rapporterer gjeldende tilstand. SetDocumentIDSeed leverer en eksplisitt frøstreng som vinner over alt annet, og å sende et tomt frø gjenoppretter det utledede frøet. GetDocumentFileID leser tilbake /ID[0] etter lagringen, slik at du kan logge det eller gjøre en assert på det
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;
Oppdateringen skjer ved lagringstidspunktet, ikke når du slår på flagget, så å aktivere deterministisk modus sent i et dokumentbygg trer likevel i kraft. Det betyr også at et endret frø når filen ved neste fullstendige lagring: sett frø A, lagre, sett frø B, lagre, og de to filene bærer forskjellige identifikatorer, mens gjenoppretting av frø A gjenoppretter den opprinnelige verdien. Et eksplisitt frø er det riktige valget når dokumentet ditt har en naturlig stabil nøkkel, som et fakturanummer, en postrevisjon eller en git-commit-identifikator, fordi det frikobler identifikatoren fra tilfeldig metadata
Hvor kommer frøet fra når du ikke oppgir ett selv?
Uten et eksplisitt frø utleder losLab PDF Library ett fra dokumenttilstand som burde være invariant på tvers av identiske regenereringer: PDF-versjonshodet, sideantallet, og hver oppføring i dokumentinformasjonsordboken. Streng- og navneverdier tas ordrett, andre objekttyper bidrar med sin serialiserte form, og det hele hashes inn i /ID-strengene. Den viktige konsekvensen er at CreationDate og ModDate er del av informasjonsordboken og derfor del av frøet med hensikt. To kjøringer får bare samme identifikator når de reelt sett produserer samme dokumentmetadata
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');
Å feste ModDate med nøkkel 8 gjør dobbel nytte, og dette er delen som lurer folk. En deterministisk /ID alene gjør ikke filen byte-identisk, fordi lagringsstien stempler ModDate med gjeldende tid med mindre den som kaller har satt den eksplisitt. Å sette nøkkel 8 markerer verdien som levert av den som kaller, og undertrykker det stempelet. Hvis du vil ha en reproduserbar fil og ikke bare en reproduserbar identifikator, behandle metadata-tidsstempler som byggeinndata: utled dem fra kildeposten eller fra en fast epoke, aldri fra Now
Hvorfor bryter omskriving av ID-en en kryptert PDF?
Fordi /ID[0] ikke bare er metadata i et kryptert dokument, det er nøkkelmateriale. ISO 32000-1 §7.6.3.3 algoritme 2 mater det første elementet i filidentifikatoren inn i krypteringsnøkkelberegningen for standard sikkerhetshåndterer ved revisjon 2 til 4, sammen med det utfylte passordet, /O-verdien og tillatelsesbitene. Den utledede nøkkelen produserer deretter /U-valideringsstrengen som en leser sjekker ved åpning, og filnøkkelen utledes og caches når du kaller Encrypt eller når et kryptert dokument lastes inn, begge deler skjer før lagringen. Å omskrive identifikatoren under lagringen ville derfor gi ut en strukturelt gyldig fil hvis /U-sjekk feiler ved gjenåpning: ikke en subtil korrupsjon, men et dokument ingen kan åpne, inkludert deg selv. Det er derfor den deterministiske oppdateringen er begrenset til dokumenter som ikke bærer krypteringstilstand, og hvorfor et kryptert dokument beholder hvilken /ID det allerede hadde, deterministisk modus eller ikke, og innstillingen har ganske enkelt ingen effekt på den stien. Den relaterte revisjonshåndteringen og tillatelsessemantikken er dekket i gjennomgangen av PDF-kryptering og revisjon av tillatelser. Merk også at gjenopprettingsstien for kryptering kun oppdaterer /ID[1], endringsidentifikatoren, nøyaktig slik §14.4 tilsikter
Hvorfor beholder inkrementelle lagringer den opprinnelige identifikatoren?
Den andre grensen er tilleggsmodus (append mode). En inkrementell oppdatering lar hver tidligere byte i filen være urørt og skriver en ny revisjon etter den, og permanensen til /ID[0] på tvers av §14.4 er det som forteller en konsument at den nye revisjonen tilhører samme dokument som den gamle. Å skrive den om ville bryte den koblingen, motsi revisjonene som allerede sitter i filen, og forstyrre signatursemantikken, siden en signatur dekker et byteintervall av en spesifikk revisjon av et spesifikt dokument. losLab PDF Library oppdaterer derfor den deterministiske identifikatoren kun ved fullstendige lagringer og aldri under tilleggsmodus, som holder garantien beskrevet i artikkelen om inkrementelle PDF-oppdateringer og tillegg til strøm intakt
Ett flaskehalspunkt for identifikatorgenerering
All /ID-generering i losLab PDF Library går nå gjennom én enkelt intern rutine, NewFileIDString, som er det som gjør den deterministiske bryteren pålitelig i stedet for en lapp på én kodesti. Opprettelse av tomt dokument, lat opprettelse av en manglende /ID-array ved behov, og gjenopprettingsstien for krypteringsfingeravtrykk kaller alle den, så det finnes nøyaktig ett sted hvor veggklokken kunne lekke tilbake inn. Det betyr også at fremtidige varianter, som en innholdsutledet identifikator, er en endring i én funksjon i stedet for en revisjon av hele serialisereren
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');
Koble den sammenligningen inn i testsuiten din før du stoler på reproduserbar utdata noe annet sted, fordi den feiler høylytt i det øyeblikket en ny funksjon reintroduserer et tidsstempel. Reproduserbarhet er en egenskap som ellers forvitrer stille, og en enkelt assert over to lagringer i minnet koster nesten ingenting å kjøre på hvert bygg
Det deterministiske identifikator-API-et vist her leveres med losLab PDF Library for Delphi og C++Builder, sammen med den fullstendige referansen for dokumentinformasjon, kryptering og inkrementell lagring