PDFium Component dělá z reprodukovatelného PDF výstupu jediné volání: TPdf.SaveAsReproducible přepíše dokument tak, že dvojité uložení téhož obsahu dá bajtově identické soubory. Odstraní /CreationDate a /ModDate z informačního slovníku dokumentu, nezapisuje žádný přívěskový /ID, drží pořadí objektů stabilní a hlásí, co změnila, v TPdfReproducibleReport. Zašifrované soubory se odmítají a podepsané potřebují výslovný souhlas
Potřeba se obvykle ukáže v build pipeline. Někdo dává vygenerovaná PDF do repozitáře nebo testovací sady golden file a každý běh produkuje diff, i když se na žádné stránce nic nezměnilo. Nebo archiv deduplikuje podle SHA-256 a potichu uloží padesát kopií téže faktury, protože každé uložení vyrazilo do souboru nový čas. PDF zapisovač, který vkládá aktuální čas ze systémových hodin, je pro desktopovou aplikaci v pořádku a nepřátelský ke všemu, co bere bajty za identitu
Proč dvojité uložení téhož PDF dá různé bajty?
Dvě uložení nezměněného dokumentu se liší kvůli přesně dvěma druhům dat: časovým razítkům v informačním slovníku dokumentu a identifikátoru souboru v přívěsku. Všechno ostatní, co pečlivý zapisovač vypustí — čísla objektů, pořadí objektů, uspořádání křížových odkazů — se dá udělat deterministické a cesta kompletního přepisu v PDFium Component to už dělá: objekty se zapisují ve vzestupném čísle objektu a tabulka xref následuje týž pořadí
Časová razítka jsou zjevný viník. ISO 32000-1 §14.3.3 definuje /CreationDate a /ModDate ve slovníku /Info a nativní uložení PDFium zapisuje čerstvé /ModDate pokaždé, když dokument serializuje. Identifikátor souboru je ten jemnější. ISO 32000-1 §14.4 popisuje /ID jako dvouprvkové pole bajtových řetězců, kde první prvek identifikuje původní soubor a druhý se mění, jak se soubor reviduje. Zapisovače ty řetězce typicky odvozují hashováním něčeho o souboru a zahrnuje-li toto něco časová razítka bajtů, identifikátor zdědí nedeterminismus
Použití TPdf.SaveAsReproducible v Delphi
TPdf.SaveAsReproducible má dvě přetížení, jedno zapisující do TStream a jedno do názvu souboru, a obě vrátí Boolean s out Report: TPdfReproducibleReport a volitelným parametrem AllowSignedDocument, který defaultně stojí na False. Návratová hodnota zrcadlí Report.Succeeded; je-li False, Report.ErrorMessage říká proč. Souborové přetížení zapisuje do dočasného souboru vedle cíle a cílem nahradí jen po úspěšném uložení, takže neúspěšný běh nenechá napůl napsané PDF tam, kde býval výstup vašeho předchozího buildu
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;
Pole reportu stojí za logování v CI. StrippedTimestampCount bývá 2 u dokumentu, jehož /Info nese obě data, a 0 u dokumentu bez slovníku /Info úplně. PrunedObjectCount počítá objekty, které v nativním uložení byly, ale nejsou dosažitelné z katalogu ani ze slovníku /Info; reprodukovatelný přepis je odhodí, takže očekávejte výstup stejné velikosti nebo menší než obyčejné uložení
Proč odstranění časových razítek nestačilo
První implementace odstranila obě razítka a stejně vyprodukovala dva různé soubory. Stránky seděly, slovník /Info seděl a přívěsky se lišily v druhém prvku /ID. Kořenem bylo pořadí: přepis nejdřív žádá PDFium o nativní uložení, pak ty bajty post-processuje a stavěč přívěsku spočítal /ID hashováním nativního výstupu. Ten nativní výstup pořád obsahoval čerstvé /ModDate, takže hash, a tedy i identifikátor, se měnil při každém běhu, i když se razítko samo do finálního souboru nikdy nedostalo
Je to dobrá ukázka obecného pravidla pro reprodukovatelné buildy: odstranění nedeterministické hodnoty nestačí, pokud se z ní něco už odvodilo. Opravou bylo /ID úplně přestat nést. TPdf.SaveAsReproducible zapisuje přívěsek obsahující jen /Size, /Root a, má-li zdroj jeden, /Info. Když v přívěsku nezůstane nic časově závislého, srovnají se dvě po sobě jdoucí uložení téhož dokumentu bajt za bajtem stejná — přesně tak se to chování ověřuje
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;
Odebírání klíčů slovníku bez poškození řetězců
Razítka se odstraňují skenem těla objektu /Info znalcem tokenů, ne textovým hledáním. Naivní bajtová náhrada za /ModDate zasáhne i řetězec /Subject nebo /Keywords, který tu text náhodou obsahuje, a vystřižení bajtů z doslovného řetězce potichu poškodí metadata místo zvolat nahlas. PDFium Component nejdřív neutralizuje doslovné řetězce a komentáře v skenovací kopii těla, pak přijme shodu jen tam, kde začíná skutečný jmenný token, použije gramatiku PDF objektů k nalezení konce hodnoty toho klíče a přeskočí celou dvojici klíč-hodnota
Praktický důsledek je, že každá jiná položka v /Info — titul, autor, producent, vlastní klíče — přežije netknutá. Odstraní se jen ty dva klíče definované jako data a jen ze slovníku, na který přívěsková položka /Info doopravdy ukazuje
Co stojí vynechání /ID u šifrovaných a podepsaných PDF?
Vynechání /ID je bezpečné pro obyčejný výstup PDF 1.x, ale vylučuje šifrování a mění to, co se děje s podpisy. V ISO 32000-1 je přívěskový /ID volitelný, dokud není přítomna položka /Encrypt, protože standardní security handler míchá první identifikační řetězec do šifrovacího klíče. Soubor bez /ID nemůže být správně zašifrovaný soubor, takže TPdf.SaveAsReproducible se to ani nesnaží: nese-li přívěsek /Encrypt, vrátí False a Report.ErrorMessage říká, že dokument je zašifrovaný. Dešifrujte nejdřív, pokud to vaše pipeline dovolí, nebo držte šifrované dodávky mimo kontroly bajtové identity. Cílíte-li na PDF 2.0, vezměte na vědomí, že ISO 32000-2 pravidlo zpřísní a vyžaduje /ID v každém souboru, takže reprodukovatelný výstup sedí nejpřirozeněji s cíli PDF 1.x; jak komponenta zachází s cíli verzí, rozebírá článek o přesné shodě verzí PDF v Delphi
Podepsané dokumenty jsou jiný problém. TPdf.SaveAsReproducible je kompletní, neinkrementální přepis, takže se posunou bajtové posuny a /ByteRange stávajících podpisů už nepokrývá bajty, které byly podepsány. Reprodukovatelný výstup podepsaného souboru je proto vědomé rozhodnutí vzdát se platnosti stávajících podpisů výměnou za stabilní bajty, proto metoda vyhodí EPdfError, když je SignatureCount větší než nula, nepředáte-li AllowSignedDocument = True
if Pdf.SignatureCount > 0 then
// Explicitní přihlášení se: přepis posune bajtové posuny, takže stávající
// podpisy je potřeba počítat s tím, že ověření potom selže
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report, True)
else
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report);
Je-li podepsaný originál záznamem pravdy, nechte ho být a reprodukovatelný výstup používejte jen pro odvozené kopie, třeba normalizovanou podobu, kterou diffujete v CI. Pro přidávání do chráněného souboru bez narušení toho, co už tam je, viz proč se zašifrované PDF nedá potichu záplatovat v Delphi
Hranice, které znát dřív, než se spoléháte na bajtovou identitu
Bajtová identita drží u téhož dokumentu uloženého stejným buildem komponenty a pokrývá přesně to, čeho se metoda dotkne. TPdf.SaveAsReproducible odstraní jen ta dvě data /Info; stream XMP metadat s vlastním xmp:CreateDate nebo xmp:ModifyDate projde jako bajty, které už dokument drží. To je stabilní napříč uloženími, ale znamená to, že data XMP a /Info už nepopisují totéž, což hraje roli u profilů PDF/A, které vyžadují, aby souhlasila; pravidla vysvětluje článek o rovnocennosti PDF/A Info a XMP metadat. Jiný binární soubor PDFium nebo verze komponenty může objekty serializovat jinak, takže obojí upněte v jakékoli pipeline srovnávající hashe napříč stroji
V těch mezích vám metoda dá to, co potřebují CI diffy, testy golden file a archivy adresované obsahem: týž vstup, tyž bajty. TPdf.SaveAsReproducible a TPdfReproducibleReport dodává PDFium Component for Delphi and C++Builder spolu se zbytkem své ukládací, optimalizační a shodové API
FAQ
Mění TPdf.SaveAsReproducible obsah stránek?
Ne. Odstraní klíče /CreationDate a /ModDate ze slovníku /Info, vynechá přívěskový /ID a odhodí objekty nedosažitelné z katalogu ani /Info; obsah stránek se zapisuje beze změny
Můžu z zašifrovaného PDF vyrobit reprodukovatelný výstup?
Ne. Šifrování závisí na přívěskovém /ID, který reprodukovatelný výstup vynechává, takže metoda vrátí False s vysvětlujícím ErrorMessage pro zašifrované zdroje
Proč podepsané PDF potřebuje AllowSignedDocument?
Reprodukovatelné uložení je kompletní přepis, který posune bajtové posuny, takže stávající podpisy přestanou sedět na svých podepsaných bajtových rozsazích. Předání AllowSignedDocument = True potvrzuje, že ten kompromis přijímáte