The same bytes every time: reproducible PDFs
Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3
At a glance
Section titled “At a glance”Build a PDF from the same inputs twice and you would expect the same file. Most PDF libraries cannot promise that — rebuild and diff, and the bytes drift. NextPDF can pin the two things that move, so the same inputs produce the same bytes, every time.
Why this matters
Section titled “Why this matters”Byte-identical output is not a vanity metric. It is the foundation underneath three things teams actually want.
The first is caching. If a build is a pure function of its inputs, its output hash is a cache key. Same inputs, same hash, skip the work and serve the stored file. When the bytes wander, the hash wanders, and the cache never hits.
The second is tamper-evidence. A pipeline that can regenerate the exact file it shipped can prove, later, that an archived document was not altered: rebuild it, hash both, compare. If even one byte differs because of an embedded clock, the proof is gone and you are back to “trust me”.
The third is trustworthy CI. A golden-file test records a known-good output and fails when a change alters it. That signal is only meaningful if an unchanged engine reproduces an unchanged file. If every run differs on a timestamp, the golden file is noise, and the team learns to ignore a red build — the most expensive habit in testing.
The short version
Section titled “The short version”In NextPDF’s deterministic profile, the two engine-controlled fields that would
otherwise drift between identical builds are the dates and the /ID. This
assumes the rest of the pipeline is already stable — the same inputs, and a
serialization that does not vary on its own (more on that below):
- Embedded dates. The document information dictionary carries
CreationDateandModDate(Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3), and the XMP metadata mirrors them. Capture “now” at build time and every rebuild differs. - The file identifier. The
/IDarray is a pair of byte strings identifying the file (Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4), stored in the trailer dictionary (Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5). Libraries usually derive it from the current time plus random bytes, so it is different on every run by design.
Pin both — a fixed timestamp and a fixed seed for the /ID — and the output
becomes a deterministic function of its content. Leave content alone and the
file is byte-for-byte identical. This is the same discipline the
Reproducible Builds project established for compiled software, applied to the
document layer.
How NextPDF approaches it
Section titled “How NextPDF approaches it”Determinism in NextPDF is a configuration object, not a test hack. The engine
exposes a DeterministicSettings value object in the NextPDF\Core namespace.
It is final readonly, immutable, and it pins exactly the two clock- and
random-derived sources of drift named above: the dates and the /ID. Pinning
them removes the two most common drift sources, but it does not on its own
guarantee byte-identical output. The engine’s other serialization behavior —
object ordering, font subsetting, and compression settings — must also be
deterministic for the output to reproduce, and NextPDF holds those stable by
design.
Its constructor takes two arguments:
public function __construct( public DateTimeImmutable $timestamp, public string $fileIdSeed,) { // ...}$timestamp is the single fixed instant written to every date field —
CreationDate, ModDate, and their XMP mirror. Pass one
DateTimeImmutable and the document stops asking the wall clock what time it
is. $fileIdSeed is the input that pins the trailer /ID: a 32-character
hexadecimal string. Give the same seed and the engine derives the same file
identifier instead of sampling the clock and a random source.
The object validates its own input. The seed must be exactly 32 hexadecimal
characters; anything else is rejected at construction with an
InvalidConfigException rather than silently producing a different-looking
/ID. This is the same refuse-to-guess stance the rest of the engine takes —
an ambiguous input fails loudly instead of quietly changing the bytes.
With both pinned, the recipe is the one the Reproducible Builds project made familiar: rebuild it, diff it, and the diff is empty.
- Fix the inputsThe same content, fonts, and settings that produced the original document.
- Pin the timestampOne DateTimeImmutable feeds CreationDate, ModDate, and the XMP dates — no wall clock.
- Pin the /ID seedA 32-character hex seed derives the trailer /ID instead of a clock-plus-random value.
- BuildThe output is now a pure function of content; the two moving parts are held still.
- Rebuild and diffRegenerate from the same inputs and compare bytes — an empty diff is the proof.
Practical example
Section titled “Practical example”A small, complete shape. The settings are constructed once and reused, so two runs of the same program emit the same file.
<?php
declare(strict_types=1);
use NextPDF\Core\DeterministicSettings;use NextPDF\Exception\InvalidConfigException;
// One fixed instant for every date field — never the wall clock.$timestamp = new DateTimeImmutable('2026-01-01T00:00:00+00:00');
// A 32-character hex seed pins the trailer /ID. Same seed, same /ID.$fileIdSeed = '0123456789abcdef0123456789abcdef';
try { $deterministic = new DeterministicSettings( timestamp: $timestamp, fileIdSeed: $fileIdSeed, );} catch (InvalidConfigException $e) { // A malformed seed (not exactly 32 hex chars) is refused here, // before any document is built — not silently coerced. error_log($e->getMessage());
throw $e;}
// Hand $deterministic to the document configuration. With both moving// parts pinned, building the same content twice yields identical bytes://// sha256(build_one) === sha256(build_two)The seed is a build input you control, not a secret. Store it next to the rest of your build configuration. The point is that it is fixed, so the file identifier it produces is fixed too.
Common misconception
Section titled “Common misconception”The first trap is “I removed the timestamp, so my build is reproducible now.”
It usually is not, because the /ID array is the quieter of the two sources.
Dates are visible in a metadata panel and easy to remember; the trailer /ID
is invisible to most readers and is regenerated from the clock and a random
source on every run. A build that pins only the dates still produces a
different file each time. You have to hold both still.
The second trap is treating determinism as a security feature on its own. A
pinned /ID makes a file reproducible; it does not make it signed, and it
does not by itself prove that two builds match. A byte-for-byte comparison or a
hash proves the builds match; pinning the /ID only removes one source of
spurious difference. And neither of those proves that a third party vouches for
the file. Reproducibility and signing are complementary layers, not
substitutes.
Limits and boundaries
Section titled “Limits and boundaries”Determinism pins the engine’s own moving parts. It does not pin your inputs.
If your content embeds a live timestamp, pulls a font that changed on disk, or
renders a value that depends on the current date, the output changes because
the input changed — and that is correct. DeterministicSettings removes the
engine’s non-determinism, not yours. A reproducible build still requires
reproducible inputs.
| Edition | Availability |
|---|---|
| Core | Full support. |
| Pro | Not in this edition |
| Enterprise | Not in this edition |
Related docs
Section titled “Related docs”- Golden-file testing — the CI technique that depends on byte-identical output, and why a deterministic engine is its precondition.
- Incremental updates — how a PDF grows
by appending, where the
/IDarray matters again for relating a file to its earlier versions. - Metadata and the XMP packet — where the embedded dates live, and how the XMP packet mirrors the document information dictionary.
- The anatomy of a PDF file — the
trailer, the cross-reference table, and where the
/IDarray sits in the file structure.
Glossary
Section titled “Glossary”- Byte-identical — two files that match exactly, byte for byte. The strongest form of “the same”, and the one a hash or a diff can verify.
/ID(file identifier) — the array of two byte strings that identifies a PDF and its versions (ISO 32000-2 §14.4), stored in the trailer dictionary (§7.5.5). Usually derived from the clock plus random bytes, which is why it changes on every unpinned build.- Document information dictionary — the structure that carries
CreationDateandModDate(ISO 32000-2 §14.3.3). One of the two sources of non-determinism a deterministic build must pin. - Golden file — a recorded known-good output that a test compares against; meaningful only when an unchanged engine reproduces an unchanged file.
- Reproducible build — a build whose output is a deterministic function of its inputs, so rebuilding from the same inputs yields the same bytes. The term comes from the Reproducible Builds project for compiled software.