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
The MCP server reads one pin at startup for every document it builds. Precedence, highest first: the per-call creationDate → PDFNATIVE_MCP_CREATION_DATE → SOURCE_DATE_EPOCH → the clock.
{
"mcpServers": {
"pdfnative": {
"command": "npx",
"args": ["-y", "pdfnative-mcp"],
"env": { "PDFNATIVE_MCP_CREATION_DATE": "2026-01-01T00:00:00Z" }
}
}
}
# the reproducible-builds.org convention works too (integer seconds), used when the variable above is unset
SOURCE_DATE_EPOCH=1767225600 npx -y pdfnative-mcp
The React renderer takes the pin as a prop of the root — sugar over layout.creationDate — or once per process through the engine's setDefaultCreationDate.
import { Document, Heading, Text, renderToBytes, setDefaultCreationDate } from 'pdfnative-react';
// per document: a Date or an ISO 8601 string
const bytes = renderToBytes(
<Document title="Invoice 2026-0001" creationDate={new Date('2026-01-01T00:00:00Z')}>
<Heading level={1}>Invoice 2026-0001</Heading>
<Text>Identical bytes for identical input.</Text>
</Document>,
);
// or once per process, for every render that passes no creationDate of its own
setDefaultCreationDate(new Date('2026-01-01T00:00:00Z'));
—
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.
layout.creationDate— the per-call pin (since 1.3.0). It sets/Info /CreationDateand/ModDate,xmp:CreateDateunder PDF/A, the{date}placeholder ofheaderTemplate/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 nocreationDateof its own;getDefaultCreationDate()reads it back andsetDefaultCreationDate(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 twoTZvalues 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.
- 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/IDentry;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
TZtoo 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
setDeflateImplorsetDeflateRawImplthe 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.