Ir al contenido
getnextpdf.com

Los mismos bytes cada vez: PDF reproducibles

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

Construya un PDF a partir de las mismas entradas dos veces y esperaría el mismo archivo. La mayoría de las bibliotecas PDF no pueden prometerlo: reconstruya y compare, y los bytes se desvían. NextPDF puede fijar las dos cosas que se mueven, de modo que las mismas entradas produzcan los mismos bytes, cada vez.

Una salida idéntica byte a byte no es una métrica de vanidad. Es la base que sustenta tres cosas que los equipos realmente quieren.

La primera es el cacheado. Si una compilación es una función pura de sus entradas, el hash de su salida es una clave de caché. Mismas entradas, mismo hash, sáltese el trabajo y sirva el archivo almacenado. Cuando los bytes vagan, el hash vaga, y la caché nunca acierta.

La segunda es la evidencia de manipulación. Una canalización que puede regenerar el archivo exacto que entregó puede demostrar, más tarde, que un documento archivado no se alteró: reconstruirlo, calcular el hash de ambos, comparar. Si aunque sea un byte difiere por culpa de un reloj incrustado, la prueba se pierde y se vuelve al «confíe en mí».

La tercera es una CI fiable. Una prueba contra archivo de referencia registra una salida buena conocida y falla cuando un cambio la altera. Esa señal solo es significativa si un motor sin cambios reproduce un archivo sin cambios. Si cada ejecución difiere en una marca de tiempo, el archivo de referencia es ruido, y el equipo aprende a ignorar una compilación en rojo: el hábito más caro en las pruebas.

En el perfil determinista de NextPDF, los dos campos controlados por el motor que de otro modo se desviarían entre compilaciones idénticas son las fechas y el /ID. Esto presupone que el resto de la canalización ya es estable: las mismas entradas, y una serialización que no varía por sí sola (más sobre esto abajo):

  • Fechas incrustadas. El diccionario de información del documento lleva CreationDate y ModDate (Spec: ISO 32000-2, §14.3.3), y los metadatos XMP las reflejan. Capture el «ahora» en el momento de la compilación y cada reconstrucción difiere.
  • El identificador de archivo. El array /ID es un par de cadenas de bytes que identifican el archivo (Spec: ISO 32000-2, §14.4), almacenado en el diccionario del tráiler (Spec: ISO 32000-2, §7.5.5). Las bibliotecas suelen derivarlo de la hora actual más bytes aleatorios, así que es diferente en cada ejecución por diseño.

Fije ambos —una marca de tiempo fija y una semilla fija para el /ID— y la salida se convierte en una función determinista de su contenido. Deje el contenido en paz y el archivo es idéntico byte a byte. Es la misma disciplina que el proyecto Reproducible Builds estableció para el software compilado, aplicada a la capa del documento.

El determinismo en NextPDF es un objeto de configuración, no un truco de pruebas. El motor expone un objeto de valor DeterministicSettings en el espacio de nombres NextPDF\Core. Es final readonly, inmutable, y fija exactamente las dos fuentes de desviación derivadas del reloj y del azar nombradas arriba: las fechas y el /ID. Fijarlas elimina las dos fuentes de desviación más comunes, pero no garantiza por sí sola una salida idéntica byte a byte. El resto del comportamiento de serialización del motor —el orden de los objetos, la creación de subconjuntos de fuentes y los ajustes de compresión— también debe ser determinista para que la salida se reproduzca, y NextPDF mantiene eso estable por diseño.

Su constructor toma dos argumentos:

public function __construct(
public DateTimeImmutable $timestamp,
public string $fileIdSeed,
) {
// ...
}

$timestamp es el único instante fijo escrito en cada campo de fecha —CreationDate, ModDate y su reflejo XMP—. Pase un DateTimeImmutable y el documento deja de preguntarle al reloj de pared qué hora es. $fileIdSeed es la entrada que fija el /ID del tráiler: una cadena hexadecimal de 32 caracteres. Dé la misma semilla y el motor deriva el mismo identificador de archivo en lugar de muestrear el reloj y una fuente aleatoria.

El objeto valida su propia entrada. La semilla debe ser exactamente 32 caracteres hexadecimales; cualquier otra cosa se rechaza en la construcción con una InvalidConfigException en lugar de producir en silencio un /ID de aspecto distinto. Es la misma postura de negarse a adivinar que adopta el resto del motor: una entrada ambigua falla ruidosamente en lugar de cambiar los bytes en silencio.

Con ambos fijados, la receta es la que el proyecto Reproducible Builds hizo familiar: reconstruirlo, compararlo, y la comparación está vacía.

  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.

Una forma pequeña y completa. Los ajustes se construyen una vez y se reutilizan, de modo que dos ejecuciones del mismo programa emiten el mismo archivo.

<?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)

La semilla es una entrada de compilación que usted controla, no un secreto. Almacénela junto al resto de su configuración de compilación. El punto es que esté fija, para que el identificador de archivo que produce también sea fijo.

La primera trampa es «eliminé la marca de tiempo, así que ahora mi compilación es reproducible». Normalmente no lo es, porque el array /ID es la más silenciosa de las dos fuentes. Las fechas son visibles en un panel de metadatos y fáciles de recordar; el /ID del tráiler es invisible para la mayoría de los lectores y se regenera a partir del reloj y de una fuente aleatoria en cada ejecución. Una compilación que fija solo las fechas sigue produciendo un archivo distinto cada vez. Hay que mantener ambos quietos.

La segunda trampa es tratar el determinismo como una función de seguridad por sí sola. Un /ID fijo hace un archivo reproducible; no lo hace firmado, y no demuestra por sí mismo que dos compilaciones coincidan. Una comparación byte a byte o un hash demuestra que las compilaciones coinciden; fijar el /ID solo elimina una fuente de diferencia espuria. Y ninguna de esas cosas demuestra que un tercero responda por el archivo. La reproducibilidad y la firma son capas complementarias, no sustitutas.

El determinismo fija las piezas móviles del propio motor. No fija sus entradas. Si su contenido incrusta una marca de tiempo en vivo, toma una fuente que cambió en disco o renderiza un valor que depende de la fecha actual, la salida cambia porque la entrada cambió, y eso es correcto. DeterministicSettings elimina el no determinismo del motor, no el suyo. Una compilación reproducible sigue requiriendo entradas reproducibles.

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
  • Idéntico byte a byte: dos archivos que coinciden exactamente, byte a byte. La forma más fuerte de «lo mismo», y la que un hash o una comparación pueden verificar.
  • /ID (identificador de archivo): el array de dos cadenas de bytes que identifica un PDF y sus versiones (ISO 32000-2 §14.4), almacenado en el diccionario del tráiler (§7.5.5). Normalmente derivado del reloj más bytes aleatorios, que es por lo que cambia en cada compilación sin fijar.
  • Diccionario de información del documento: la estructura que lleva CreationDate y ModDate (ISO 32000-2 §14.3.3). Una de las dos fuentes de no determinismo que una compilación determinista debe fijar.
  • Archivo de referencia: una salida buena conocida y registrada con la que una prueba se compara; significativa solo cuando un motor sin cambios reproduce un archivo sin cambios.
  • Compilación reproducible: una compilación cuya salida es una función determinista de sus entradas, de modo que reconstruir a partir de las mismas entradas produce los mismos bytes. El término viene del proyecto Reproducible Builds para el software compilado.