Skip to content
getnextpdf.com

The same bytes every time: reproducible PDFs

Spec: ISO 32000-2, §14.4Spec: ISO 32000-2, §14.3.3

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.

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.

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 CreationDate and ModDate (Spec: ISO 32000-2, §14.3.3), and the XMP metadata mirrors them. Capture “now” at build time and every rebuild differs.
  • The file identifier. The /ID array is a pair of byte strings identifying the file (Spec: ISO 32000-2, §14.4), stored in the trailer dictionary (Spec: ISO 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.

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.

  1. Fix the inputsThe same content, fonts, and settings that produced the original document.
  2. Pin the timestampOne DateTimeImmutable feeds CreationDate, ModDate, and the XMP dates — no wall clock.
  3. Pin the /ID seedA 32-character hex seed derives the trailer /ID instead of a clock-plus-random value.
  4. BuildThe output is now a pure function of content; the two moving parts are held still.
  5. Rebuild and diffRegenerate from the same inputs and compare bytes — an empty diff is the proof.
Reproducible build: identical inputs plus a pinned timestamp and a pinned /ID seed produce the same bytes, which a rebuild-and-diff step confirms.

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.

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.

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.

Deterministic byte-identical output — edition availability
EditionAvailability
Core

Full support. DeterministicSettings ships in the open-source core: pin the timestamp and the /ID seed and the same content rebuilds to the same bytes — no edition gate.

ProNot in this edition
EnterpriseNot in this edition
  • 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 /ID array 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 /ID array sits in the file structure.
  • 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 CreationDate and ModDate (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.