PDFium Component gør reprodicerbart PDF-output til ét enkelt kald: TPdf.SaveAsReproducible omskriver dokumentet, så gem af samme indhold to gange giver byte-identiske filer. Den fjerner /CreationDate og /ModDate fra dokument-information-dictionaryen, skriver ingen trailer /ID, holder objekt-rækkefølgen stabil og rapporterer, hvad den ændrede, i en TPdfReproducibleReport. Krypterede filer nægtes, og signerede filer behøver eksplicit samtykke
Behovet viser sig normalt i en build-pipeline. Nogen checker genererede PDF'er ind i et repository eller en golden-file-testsuite, og hver kørsel frembringer en diff, selvom intet på nogen side ændrede sig. Eller et arkiv deduplicerer efter SHA-256 og gemmer stille halvtreds kopier af samme faktura, fordi hver gem stemplede et nyt tidspunkt ind i filen. En PDF-writer, der indlejrer urskiven, er fin til en desktop-app og fjendtlig over for alt, hvad der behandler bytes som identitet
Hvorfor frembringer gem af samme PDF to gange forskellige bytes?
To gem af et uændret dokument adskiller sig på grund af præcis to slags data: tidsstempler i dokument-information-dictionaryen og fil-identifikatoren i traileren. Alt andet, en omhyggelig writer udsender, objektnummerering, objekt-rækkefølge, cross-reference-layout, kan gøres deterministisk, og PDFium Components fuld-omskrivnings-vej gør det allerede: objekter skrives i stigende objektnummer, og xref-tabellen følger samme rækkefølge
Tidsstemplerne er den indlysende synder. ISO 32000-1 §14.3.3 definerer /CreationDate og /ModDate i /Info-dictionaryen, og den native PDFium-gem skriver en frisk /ModDate hver gang, den serialiserer dokumentet. Fil-identifikatoren er den subtile. ISO 32000-1 §14.4 beskriver /ID som et to-elementers array af byte-strenge, hvor første element identificerer den oprindelige fil, og det andet ændrer sig, når filen revideres. Writers udleder typisk de strenge ved at hashe noget om filen, og hvis det noget inkluderer de tidsstemplede bytes, arver identifikatoren non-determinismen
Brug af TPdf.SaveAsReproducible i Delphi
TPdf.SaveAsReproducible har to overloads, én der skriver til en TStream og én til et filnavn, og begge returnerer Boolean med en out Report: TPdfReproducibleReport og en valgfri AllowSignedDocument-parameter, der som standard er False. Returværdien spejler Report.Succeeded; er den False, siger Report.ErrorMessage hvorfor. Fil-overloaden skriver til en midlertidig fil ved siden af målet og erstatter målet først efter en vellykket gem, så en fejlet kørsel ikke efterlader en halvfærdig PDF, hvor ens tidligere build-output plejede at være
var
Pdf: TPdf;
Report: TPdfReproducibleReport;
begin
Pdf:= TPdf.Create(nil);
try
Pdf.FileName:= 'invoice-2026-10.pdf';
Pdf.Active:= True;
if Pdf.SaveAsReproducible('build\invoice-2026-10.pdf', Report) then
Writeln(Format('%d -> %d bytes, %d objects kept, %d pruned, ' +
'%d timestamps stripped', [Report.SourceByteCount,
Report.OutputByteCount, Report.KeptObjectCount,
Report.PrunedObjectCount, Report.StrippedTimestampCount]))
else
Writeln('Reproducible save failed: ', Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Rapportfelterne er værd at logge i CI. StrippedTimestampCount er normalt 2 for et dokument, hvis /Info bærer begge datoer, og 0 for ét uden en /Info-dictionary overhovedet. PrunedObjectCount tæller objekter, der var til stede i den native gem, men ikke kunne nås fra catalog'en eller /Info-dictionaryen; den reprodicerbare omskrivning dropper dem, så forvent at outputtet er samme størrelse eller mindre end en almindelig gem
Hvorfor det ikke var nok at strippe tidsstemplerne
Den første implementering fjernede begge tidsstempler og frembragte stadig to forskellige filer. Siderne matchede, /Info-dictionaryen matchede, og trailerne adskilte sig i andet element af /ID. Rodårsagen var rækkefølge: omskrivningen beder først PDFium om en native gem, post-proesserer så de bytes, og trailer-byggeren beregnede /ID ved at hashe det native output. Det native output indeholdt stadig den friske /ModDate, så hashen, og dermed identifikatoren, ændrede sig ved hver kørsel, selvom tidsstemplet selv aldrig nåede den endelige fil
Det er et godt eksempel på en generel regel for reprodicerbare builds: at fjerne en non-deterministisk værdi er ikke nok, hvis noget først blev udledt af den. Fixet var at holde op med at bære /ID helt. TPdf.SaveAsReproducible skriver en trailer, der kun indeholder /Size, /Root og, når kilden har én, /Info. Med intet tidsafhængigt tilbage i traileren sammenlignes to på hinanden følgende gem af samme dokument ens byte for byte, hvilket er præcis, hvordan adfærden verificeres
function SavesAreIdentical(Pdf: TPdf): Boolean;
var
A, B: TMemoryStream;
RA, RB: TPdfReproducibleReport;
begin
A:= TMemoryStream.Create;
B:= TMemoryStream.Create;
try
Result:= Pdf.SaveAsReproducible(A, RA) and
Pdf.SaveAsReproducible(B, RB) and
(A.Size = B.Size) and CompareMem(A.Memory, B.Memory, A.Size);
finally
B.Free;
A.Free;
end;
end;
At fjerne dictionary-nøgler uden at korruptere strenge
Tidsstemplerne fjernes med en token-bevidst scanning af /Info-objektets krop, ikke med en tekstsøgning. En naiv byte-erstatning for /ModDate ville også ramme en /Subject- eller /Keywords-streng, der tilfældigt indeholder den tekst, og at skære bytes ud af en literal streng korrupterer stille metadataet i stedet for at fejle højlydt. PDFium Component neutraliserer først literal strenge og kommentarer i en scan-kopi af kroppen, accepterer så kun et match, hvor en ægte name-token begynder, bruger PDF-objektgrammatikken til at finde, hvor den nøgles værdi slutter, og springer hele nøgle-værdi-paret over
Den praktiske konsekvens er, at hver anden post i /Info, titel, forfatter, producer, brugerdefinerede nøgler, overlever urørt. Kun de to nøgler, defineret som datoer, fjernes, og kun fra dictionaryen, som trailerens /Info-post faktisk peger på
Hvad koster udeladelse af /ID for krypterede og signerede PDF'er?
At udelade /ID er sikkert til almindeligt PDF 1.x-output, men det udelukker kryptering og ændrer, hvad der sker med signaturer. I ISO 32000-1 er trailer-/ID valgfri, medmindre en /Encrypt-post er til stede, for standard security handleren blander den første identifikatorstreng ind i krypteringsnøglen. En fil uden /ID kan ikke være en korrekt krypteret fil, så TPdf.SaveAsReproducible prøver ikke: bærer traileren /Encrypt, returnerer den False, og Report.ErrorMessage siger, at dokumentet er krypteret. Dekryptér først, hvis ens pipeline tillader det, eller hold krypterede leverancer ude af byte-identitets-tjek. Skyder man mod PDF 2.0, så bemærk, at ISO 32000-2 strammer reglen og kræver /ID i hver fil, så reprodicerbart output passer naturligst med PDF 1.x-mål; artiklen om eksakt PDF-version-conformance i Delphi dækker, hvordan komponenten håndterer versionsmål
Signerede dokumenter er et andet problem. TPdf.SaveAsReproducible er en fuld, non-inkrementel omskrivning, så byte-offsets flytter sig, og de eksisterende signaturers /ByteRange dækker ikke længere de bytes, der blev signeret. Reprodicerbart output af en signeret fil er derfor en bevidst beslutning om at give afkald på de eksisterende signaturers gyldighed til gengæld for stabile bytes, hvilket er derfor, metoden rejser EPdfError, når SignatureCount er større end nul, medmindre man giver AllowSignedDocument = True med
if Pdf.SignatureCount > 0 then
// Eksplicit opt-in: omskrivningen flytter byte-offsets, så eksisterende
// signaturer bør forventes at fejle verifikation bagefter
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report, True)
else
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report);
Er det signerede original sandhedens rekord, så behold den, som den er, og brug reprodicerbart output kun til afledte kopier, som en normaliseret rendition, man diff'er i CI. Til at tilføje til en beskyttet fil uden at forstyrre, hvad der allerede er der, se hvorfor en krypteret PDF ikke kan lappes stille i Delphi
Grænser at kende, før man stoler på byte-identitet
Byte-identitet holder for samme dokument gemt af samme komponent-build, og den dækker præcis, hvad metoden rører. TPdf.SaveAsReproducible stripper kun de to /Info-datoer; en XMP metadata-stream med sin egen xmp:CreateDate eller xmp:ModifyDate føres igennem som de bytes, dokumentet allerede holder. Det er stabilt på tværs af gem, men det betyder, at XMP- og /Info-datoerne ikke længere beskriver det samme, hvilket betyder noget for PDF/A-profiler, der kræver, at de to er enige; artiklen om PDF/A Info- og XMP-metadata-ækvivalens forklarer de regler. En anden PDFium-binær eller komponentversion kan også serialisere objekter forskelligt, så fastlås begge i enhver pipeline, der sammenligner hash på tværs af maskiner
Inden for de grænser giver metoden en det, CI-diff'er, golden-file-tests og indholds-adresserede arkiver behøver: samme input, samme bytes. TPdf.SaveAsReproducible og TPdfReproducibleReport følger med PDFium Component til Delphi og C++Builder sammen med resten af dens save-, optimerings- og compliance-API
FAQ
Ændrer TPdf.SaveAsReproducible sideindhold?
Nej. Den fjerner /CreationDate- og /ModDate-nøglerne fra /Info-dictionaryen, udelader trailer /ID og dropper objekter, der ikke kan nås fra catalog'en eller /Info; sideindholdet skrives uændret
Kan man frembringe reprodicerbart output fra en krypteret PDF?
Nej. Kryptering afhænger af trailer /ID, som reprodicerbart output udelader, så metoden returnerer False med en forklarende ErrorMessage for krypterede kilder
Hvorfor behøver en signeret PDF AllowSignedDocument?
Den reprodicerbare gem er en fuld omskrivning, der flytter byte-offsets, så eksisterende signaturer holder op med at matche deres signede byte-områder. At give AllowSignedDocument = True med bekræfter, at man accepterer det kompromis