Teknisk artikel

Deterministiskt PDF-ID i Delphi för reproducerbara byggen

losLab PDF Library kan producera byte-identisk PDF-utdata för identisk indata så snart du anropar SetDeterministicDocumentID(1). Som standard är trailer-arrayen /ID en MD5-digest av väggklockan, så två körningar av samma generator skiljer sig åt i åtminstone de bytarna. Deterministiskt läge härleder i stället /ID från ett stabilt frö, vilket återställer reproducerbara byggen

Symptomet dyker vanligtvis upp i CI innan någon letar efter det. Mallen har inte ändrats, indataposten har inte ändrats, typsnitten har inte ändrats, och den genererade PDF-filen hashar ändå olika på varje pipelinekörning. Byggcachar träffar aldrig. Innehållsadresserbar lagring samlar en ny blob per nattlig bygge. Byte-nivå-regressionsdiffar tänds på filer ingen har rört. Följ diffen ner till de faktiska bytarna och det är nästan alltid samma handfull hexsiffror som sitter i filtrailern

Vad trailerns ID-array är till för

Trailern /ID är en filidentitetsmarkör, inte en kontrollsumma av innehållet. ISO 32000-1 §14.4 definierar den som en array av två bytesträngar: det första elementet är den permanenta identifieraren som tilldelas när dokumentet skapas och som är tänkt att överleva varje senare redigering, och det andra elementet är den föränderliga identifieraren som en skrivare uppdaterar varje gång filen ändras. Tillsammans låter de ett system avgöra om två filer är revisioner av ett dokument eller två orelaterade dokument. §7.5.5 gör posten i praktiken i det närmaste obligatorisk, eftersom trailern måste bära /ID när den också bär /Encrypt

Inget i specifikationen säger hur värdet ska beräknas. Rekommendationen är en digest av saker som aktuell tid, filsökväg, filstorlek och dokumentinformationsordboken, och väggklockan är ingrediensen som gör resultatet unikt. Det är exakt den egenskap du vill ha för identitet och exakt den egenskap som förstör reproducerbarhet, vilket är varför detta behöver vara en explicit brytare snarare än en tyst beteendeändring

Varför producerar samma bygge en annan PDF varje gång?

Eftersom standardidentifieraren härleds från ögonblicket för generering. Historiskt byggde losLab PDF Library /ID-strängarna från en MD5 av den aktuella tidsstämpeln, så ett dokument skapat två gånger en sekund isär bär två olika permanenta identifierare även när varje annan byte i filen är identisk. Kostnaden i efterföljande led är verklig: ett byggsystem som nyckelsätter artefakter efter hash kan aldrig återanvända ett PDF-steg, en deduplicerande objektlagring håller en kopia per bygge i stället för en kopia per dokument, och en granskare som tittar på en binär diff måste bevisa att den enda ändringen är brus innan resten av diffen kan litas på. Deterministisk /ID-generering finns för att ta bort det bruset, i samma anda som arbetet med layoutstabilitet som beskrivs i anteckningarna om objektströmmar och korsreferensströmmar

Att växla till en reproducerbar identifierare

Deterministiskt läge är opt-in, per dokument, och avstängt som standard så befintlig utdata förblir oförändrad tills du begär det. SetDeterministicDocumentID tar emot 0 eller 1 och returnerar 1 när värdet accepterades, 0 för allt utanför intervallet; GetDeterministicDocumentID rapporterar det aktuella tillståndet. SetDocumentIDSeed tillhandahåller en explicit frösträng som vinner över allt annat, och att skicka ett tomt frö återgår till det härledda fröet. GetDocumentFileID läser tillbaka /ID[0] efter sparningen så att du kan logga det eller assertera 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;

Uppdateringen sker vid sparningstillfället, inte när du slår om flaggan, så att aktivera deterministiskt läge sent i ett dokumentbygge får ändå effekt. Det betyder också att ett ändrat frö når filen vid nästa fullständiga sparning: sätt frö A, spara, sätt frö B, spara, och de två filerna bär olika identifierare, medan att återställa frö A återställer det ursprungliga värdet. Ett explicit frö är rätt val närhelst ditt dokument har en naturlig stabil nyckel såsom ett fakturanummer, en postrevision eller en git commit-identifierare, eftersom det frikopplar identifieraren från tillfällig metadata

Varifrån kommer fröet när du inte tillhandahåller ett?

Utan ett explicit frö härleder losLab PDF Library ett från dokumenttillstånd som bör vara invariant över identiska omgenereringar: PDF-versionshuvudet, sidantalet, och varje post i dokumentinformationsordboken. Sträng- och namnvärden tas ordagrant, andra objekttyper bidrar med sin serialiserade form, och det hela hashas in i /ID-strängarna. Den viktiga konsekvensen är att CreationDate och ModDate är en del av informationsordboken och därför en del av fröet med avsikt. Två körningar tjänar bara samma identifierare när de genuint producerar samma 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');

Att fixera ModDate med nyckel 8 gör dubbel nytta, och det här är delen som fångar folk på fel fot. Ett deterministiskt /ID ensamt gör inte filen byte-identisk, eftersom sparvägen stämplar ModDate med aktuell tid om inte den anropande koden har satt det explicit. Att sätta nyckel 8 markerar värdet som tillhandahållet av anroparen och undertrycker den stämpeln. Om du vill ha en reproducerbar fil snarare än bara en reproducerbar identifierare, behandla metadata-tidsstämplar som byggindata: härled dem från källposten eller från en fast epok, aldrig från Now

Varför bryter omskrivning av ID:t en krypterad PDF?

Eftersom /ID[0] inte bara är metadata i ett krypterat dokument, det är nyckelmaterial. ISO 32000-1 §7.6.3.3 algoritm 2 matar in det första elementet i filidentifieraren i krypteringsnyckelberäkningen för standardsäkerhetshanteraren vid revisionerna 2 till 4, tillsammans med det utfyllda lösenordet, /O-värdet och behörighetsbitarna. Den härledda nyckeln producerar sedan /U-valideringssträngen som en läsare kontrollerar vid öppning, och filnyckeln härleds och cachas när du anropar Encrypt eller när ett krypterat dokument laddas, båda vilka sker före sparningen. Att skriva om identifieraren under sparningen skulle därför avge en strukturellt giltig fil vars /U-kontroll misslyckas vid återöppning: inte en subtil korruption utan ett dokument ingen kan öppna, inklusive du. Det är därför den deterministiska uppdateringen är begränsad till dokument som inte bär krypteringstillstånd, och varför ett krypterat dokument behåller vilket /ID det redan hade, deterministiskt läge eller inte, och inställningen har helt enkelt ingen effekt på den vägen. Den relaterade revisionshanteringen och behörighetssemantiken täcks i genomgången av PDF-kryptering och behörighetsgranskning. Notera också att krypteringens återställningsväg endast uppdaterar /ID[1], ändringsidentifieraren, precis som §14.4 avser

Varför inkrementella sparningar behåller den ursprungliga identifieraren

Den andra gränsen är append-läge. En inkrementell uppdatering lämnar varje tidigare byte i filen orörd och skriver en ny revision efter den, och beständigheten hos /ID[0] över §14.4 är det som talar om för en konsument att den nya revisionen tillhör samma dokument som den gamla. Att skriva om det skulle bryta den länken, motsäga de revisioner som redan sitter i filen, och störa signatursemantiken, eftersom en signatur täcker ett byteintervall av en specifik revision av ett specifikt dokument. losLab PDF Library uppdaterar därför den deterministiska identifieraren endast vid fullständiga sparningar och aldrig under append-läge, vilket håller garantin som beskrivs i artikeln om PDF-inkrementella uppdateringar och append till ström intakt

En flaskhals för identifierargenerering

All /ID-generering i losLab PDF Library kanaliseras nu genom en enda intern rutin, NewFileIDString, vilket är det som gör den deterministiska brytaren pålitlig snarare än en lapp på en kodväg. Skapande av tomt dokument, lat skapande av en saknad /ID-array vid behov, och krypteringens fingeravtrycksåterställningsväg anropar alla den, så det finns exakt ett ställe där väggklockan skulle kunna läcka tillbaka in. Det betyder också att framtida varianter, som en innehållshärledd identifierare, är en ändring i en funktion snarare än en granskning av hela serialiseraren

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');

Koppla in den jämförelsen i din testsvit innan du förlitar dig på reproducerbar utdata någon annanstans, för den misslyckas högljutt i samma ögonblick någon ny funktion återinför en tidsstämpel. Reproducerbarhet är en egenskap som annars förfaller tyst, och en enda assertion över två sparningar i minnet kostar nästan ingenting att köra på varje bygge

Det deterministiska identifierar-API:et som visas här levereras med losLab PDF Library för Delphi och C++Builder, tillsammans med den fullständiga referensen för dokumentinformation, kryptering och inkrementell sparning