The PDFium Component makes reproducible PDF output a single call: TPdf.SaveAsReproducible rewrites the document so that saving the same content twice yields byte-identical files. It removes /CreationDate and /ModDate from the document information dictionary, writes no trailer /ID, keeps object order stable, and reports what it changed in a TPdfReproducibleReport. Encrypted files are refused, and signed files need explicit consent
The need usually shows up in a build pipeline. Someone checks generated PDFs into a repository or a golden-file test suite, and every run produces a diff even though nothing on any page changed. Or an archive deduplicates by SHA-256 and quietly stores fifty copies of the same invoice, because each save stamped a new time into the file. A PDF writer that embeds the wall clock is fine for a desktop app and hostile to anything that treats bytes as identity
Why does saving the same PDF twice produce different bytes?
Two saves of an unchanged document differ because of exactly two kinds of data: timestamps in the document information dictionary and the file identifier in the trailer. Everything else a careful writer emits, object numbering, object order, cross-reference layout, can be made deterministic, and the PDFium Component full-rewrite path already does that: objects are written in ascending object number and the xref table follows the same order
The timestamps are the obvious culprit. ISO 32000-1 §14.3.3 defines /CreationDate and /ModDate in the /Info dictionary, and the native PDFium save writes a fresh /ModDate each time it serializes the document. The file identifier is the subtle one. ISO 32000-1 §14.4 describes /ID as a two-element array of byte strings, where the first element identifies the original file and the second changes as the file is revised. Writers typically derive those strings by hashing something about the file, and if that something includes the timestamped bytes, the identifier inherits the non-determinism
Using TPdf.SaveAsReproducible in Delphi
TPdf.SaveAsReproducible has two overloads, one writing to a TStream and one to a file name, and both return Boolean with an out Report: TPdfReproducibleReport and an optional AllowSignedDocument parameter that defaults to False. The return value mirrors Report.Succeeded; when it is False, Report.ErrorMessage says why. The file overload writes to a temporary file next to the target and replaces the target only after a successful save, so a failed run does not leave a half-written PDF where your previous build output used to be
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;
The report fields are worth logging in CI. StrippedTimestampCount is normally 2 for a document whose /Info carries both dates, and 0 for one without an /Info dictionary at all. PrunedObjectCount counts objects that were present in the native save but not reachable from the catalog or the /Info dictionary; the reproducible rewrite drops them, so expect the output to be the same size or smaller than an ordinary save
Why stripping the timestamps was not enough
The first implementation removed both timestamps and still produced two different files. The pages matched, the /Info dictionary matched, and the trailers differed in the second element of /ID. The root cause was ordering: the rewrite first asks PDFium for a native save, then post-processes those bytes, and the trailer builder computed /ID by hashing the native output. That native output still contained the fresh /ModDate, so the hash, and therefore the identifier, changed on every run even though the timestamp itself never reached the final file
It is a good example of a general rule for reproducible builds: removing a non-deterministic value is not enough if anything was derived from it first. The fix was to stop carrying /ID altogether. TPdf.SaveAsReproducible writes a trailer containing only /Size, /Root, and, when the source has one, /Info. With nothing time-dependent left in the trailer, two consecutive saves of the same document compare equal byte for byte, which is exactly how the behaviour is verified
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;
Removing dictionary keys without corrupting strings
The timestamps are removed by a token-aware scan of the /Info object body, not by a text search. A naive byte replace for /ModDate would also hit a /Subject or /Keywords string that happens to contain that text, and cutting bytes out of a literal string silently corrupts the metadata instead of failing loudly. The PDFium Component first neutralizes literal strings and comments in a scan copy of the body, then only accepts a match where a real name token begins, uses the PDF object grammar to find where that key's value ends, and skips the whole key-value pair
The practical consequence is that every other entry in /Info, title, author, producer, custom keys, survives untouched. Only the two keys defined as dates are removed, and only from the dictionary that the trailer's /Info entry actually points to
What does omitting /ID cost for encrypted and signed PDFs?
Omitting /ID is safe for plain PDF 1.x output, but it rules out encryption and changes what happens to signatures. In ISO 32000-1 the trailer /ID is optional unless an /Encrypt entry is present, because the standard security handler mixes the first identifier string into the encryption key. A file without /ID cannot be a correctly encrypted file, so TPdf.SaveAsReproducible does not try: when the trailer carries /Encrypt, it returns False and Report.ErrorMessage says the document is encrypted. Decrypt first if your pipeline allows it, or keep encrypted deliverables out of byte-identity checks. If you are targeting PDF 2.0, note that ISO 32000-2 tightens the rule and requires /ID in every file, so reproducible output fits most naturally with PDF 1.x targets; the article on exact PDF version conformance in Delphi covers how the component handles version targets
Signed documents are a different problem. TPdf.SaveAsReproducible is a full, non-incremental rewrite, so byte offsets move and the existing signatures' /ByteRange no longer covers the bytes that were signed. Reproducible output of a signed file is therefore a deliberate decision to give up the existing signatures' validity in exchange for stable bytes, which is why the method raises EPdfError when SignatureCount is greater than zero unless you pass AllowSignedDocument = True
if Pdf.SignatureCount > 0 then
// Explicit opt-in: the rewrite moves byte offsets, so existing
// signatures should be expected to fail verification afterwards
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report, True)
else
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report);
If the signed original is the record of truth, keep it as is and use reproducible output only for derived copies, such as a normalised rendition you diff in CI. For appending to a protected file without disturbing what is already there, see why an encrypted PDF cannot be silently patched in Delphi
Boundaries to know before relying on byte identity
Byte identity holds for the same document saved by the same component build, and it covers exactly what the method touches. TPdf.SaveAsReproducible strips only the two /Info dates; an XMP metadata stream with its own xmp:CreateDate or xmp:ModifyDate is carried through as whatever bytes the document already holds. That is stable across saves, but it means the XMP and /Info dates no longer describe the same thing, which matters for PDF/A profiles that require the two to agree; the article on PDF/A Info and XMP metadata equivalence explains those rules. A different PDFium binary or component version may also serialise objects differently, so pin both in any pipeline that compares hashes across machines
Within those limits, the method gives you what CI diffs, golden-file tests, and content-addressed archives need: same input, same bytes. TPdf.SaveAsReproducible and TPdfReproducibleReport ship with the PDFium Component for Delphi and C++Builder, alongside the rest of its save, optimisation, and compliance API
FAQ
Does TPdf.SaveAsReproducible change page content?
No. It removes the /CreationDate and /ModDate keys from the /Info dictionary, omits the trailer /ID, and drops objects unreachable from the catalog or /Info; page content is written unchanged
Can I produce reproducible output from an encrypted PDF?
No. Encryption depends on the trailer /ID, which reproducible output omits, so the method returns False with an explanatory ErrorMessage for encrypted sources
Why does a signed PDF need AllowSignedDocument?
The reproducible save is a full rewrite that moves byte offsets, so existing signatures stop matching their signed byte ranges. Passing AllowSignedDocument = True confirms you accept that trade-off