Home  ›  Playgrounds  ›  Reproducible output (v1.8.0)

Reproducible output — since 1.8.0

pdfnative 1.8.0 writes identical bytes for identical input once the creation instant is pinned — per call with layout.creationDate, or once per process with setDefaultCreationDate; every date is written in UTC, so the bytes do not depend on the host. /CreationDate, the XMP dates, the {date} header and footer placeholder and the trailer /ID all derive from that one instant, which means a document can be hashed, diffed and gated in CI. Pick a preset and build it twice: the page shows the SHA-256 of each build. Then flip one input for the second build and watch the hashes diverge.

The same pin on the other surfaces — copy-only: the CLI and the MCP server do not run in the browser.

The CLI pins every PDF written in one run with --creation-date, or with SOURCE_DATE_EPOCH when the flag is absent; compare diffs two PDFs by text and structure when bytes are not the point.

# pin the instant for every PDF written in this run (render, batch)
pdfnative --creation-date 2026-01-01T00:00:00Z render --input document.json --output invoice.pdf

# or through the environment (integer seconds), honoured when the flag is absent
SOURCE_DATE_EPOCH=1767225600 pdfnative render --input document.json --output invoice.pdf

# compare content rather than bytes, e.g. across engine versions
pdfnative compare invoice.pdf invoice-rebuilt.pdf --mode both --format json

Build A — Build B — Verdict Build to compare the two SHA-256 digests.
View underlying code

Diagnostics (layout.onDiagnostic) and build log

Build to see what each of the two builds stamped and whether they raised a diagnostic. Every preset wires layout.onDiagnostic; these builds raise none.
What the pin covers:
  • layout.creationDate — the per-call pin (since 1.3.0). It sets /Info /CreationDate and /ModDate, xmp:CreateDate under PDF/A, the {date} placeholder of headerTemplate / footerTemplate, and the trailer /ID, an MD5 of title, creation date and object count.
  • setDefaultCreationDate(date) — the process-wide default (1.8.0) for every build that passes no creationDate of its own; getDefaultCreationDate() reads it back and setDefaultCreationDate(null) restores the clock. Precedence: layout.creationDate → the default → new Date().
  • UTC everywhere — dates are formatted with the UTC getters and a literal +00'00' offset, so a pinned build is identical in every time zone; the same document built under two TZ values is compared in the engine's own test suite.
  • The rest is pure — pagination, shaping, subsetting and object numbering are deterministic functions of the input, so pinning the one instant pins the whole file.
Honest limits:
  • Encrypted output is never reproducible — encryption keys, salts and every AES-CBC IV come from a CSPRNG by design.
  • Signatures and incremental updates write their own /ID — each revision regenerates the second /ID entry; signingTime, RFC 3161 tokens and revocation data carry independent meaning and are not covered by the pin.
  • An engine upgrade may change bytes for identical input — a fix to a font table or a line-breaking rule is a legitimate byte change. Rebaseline once per upgrade, or compare content instead of bytes with the CLI's compare.
  • Pin TZ too for anything you format yourself with local-time getters before handing it to the engine: the engine writes UTC, your own strings may not.
  • Compression in the browser — without setDeflateImpl or setDeflateRawImpl the browser build wraps streams in stored-block FlateDecode containers; still deterministic, but different bytes from a Node build that compresses with zlib. Inject the same compressor everywhere — zipnative's codec, for example (use cases, Case 5) — or compare hashes across hosts on the same code path.

See the Typography guide for the layout options these presets use, and the CLI, MCP and React guides for the other surfaces' pins.