Aller au contenu
getnextpdf.com

Les mêmes octets à chaque fois : des PDF reproductibles

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

Construis un PDF deux fois à partir des mêmes entrées et tu t’attendrais au même fichier. La plupart des bibliothèques PDF ne peuvent pas le promettre — reconstruis et compare, et les octets dérivent. NextPDF peut fixer les deux choses qui bougent, pour que les mêmes entrées produisent les mêmes octets, à chaque fois.

Une sortie identique octet par octet n’est pas une métrique de vanité. C’est le socle sous trois choses que les équipes veulent vraiment.

La première est le cache. Si un build est une fonction pure de ses entrées, le hachage de sa sortie est une clé de cache. Mêmes entrées, même hachage, on saute le travail et on sert le fichier stocké. Quand les octets vagabondent, le hachage vagabonde, et le cache ne touche jamais.

La deuxième est la détection d’altération. Une chaîne qui peut régénérer le fichier exact qu’elle a expédié peut prouver, plus tard, qu’un document archivé n’a pas été modifié : reconstruis-le, hache les deux, compare. Si ne serait-ce qu’un seul octet diffère à cause d’une horloge incorporée, la preuve a disparu et tu es de retour au « crois-moi sur parole ».

La troisième est une CI digne de confiance. Un test sur fichier de référence enregistre une sortie connue comme bonne et échoue lorsqu’un changement la modifie. Ce signal n’a de sens que si un moteur inchangé reproduit un fichier inchangé. Si chaque exécution diffère sur un horodatage, le fichier de référence est du bruit, et l’équipe apprend à ignorer un build rouge — l’habitude la plus coûteuse en matière de test.

Dans le profil déterministe de NextPDF, les deux champs contrôlés par le moteur qui dériveraient autrement entre des builds identiques sont les dates et le /ID. Cela suppose que le reste de la chaîne est déjà stable — les mêmes entrées, et une sérialisation qui ne varie pas d’elle-même (voir ci-dessous) :

  • Dates incorporées. Le dictionnaire d’information du document porte CreationDate et ModDate (Spec: ISO 32000-2, §14.3.3), et les métadonnées XMP les reflètent. Capture « maintenant » au moment du build et chaque reconstruction diffère.
  • L’identifiant de fichier. Le tableau /ID est une paire de chaînes d’octets identifiant le fichier (Spec: ISO 32000-2, §14.4), stockée dans le dictionnaire du trailer (Spec: ISO 32000-2, §7.5.5). Les bibliothèques le dérivent généralement de l’heure actuelle plus des octets aléatoires, de sorte qu’il est différent à chaque exécution par conception.

Fixe les deux — un horodatage fixe et une graine fixe pour le /ID — et la sortie devient une fonction déterministe de son contenu. Laisse le contenu tranquille et le fichier est identique octet par octet. C’est la même discipline que le projet Reproducible Builds a établie pour les logiciels compilés, appliquée à la couche du document.

Le déterminisme dans NextPDF est un objet de configuration, pas une bidouille de test. Le moteur expose un objet-valeur DeterministicSettings dans l’espace de noms NextPDF\Core. Il est final readonly, immuable, et il fixe exactement les deux sources de dérive dérivées de l’horloge et de l’aléa nommées ci-dessus : les dates et le /ID. Les fixer supprime les deux sources de dérive les plus courantes, mais cela ne garantit pas à soi seul une sortie identique octet par octet. L’autre comportement de sérialisation du moteur — l’ordre des objets, le sous-ensemble de polices et les réglages de compression — doit aussi être déterministe pour que la sortie se reproduise, et NextPDF les maintient stables par conception.

Son constructeur prend deux arguments :

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

$timestamp est l’instant fixe unique écrit dans chaque champ de date — CreationDate, ModDate, et leur miroir XMP. Passe un seul DateTimeImmutable et le document cesse de demander l’heure à l’horloge murale. $fileIdSeed est l’entrée qui fixe le /ID du trailer : une chaîne hexadécimale de 32 caractères. Donne la même graine et le moteur dérive le même identifiant de fichier au lieu d’échantillonner l’horloge et une source aléatoire.

L’objet valide sa propre entrée. La graine doit faire exactement 32 caractères hexadécimaux ; toute autre chose est rejetée à la construction avec une InvalidConfigException plutôt que de produire silencieusement un /ID d’apparence différente. C’est la même posture de refus de deviner que le reste du moteur adopte — une entrée ambiguë échoue bruyamment au lieu de changer discrètement les octets.

Les deux étant fixés, la recette est celle que le projet Reproducible Builds a rendue familière : reconstruis-le, compare-le, et la comparaison est vide.

  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.
Build reproductible : des entrées identiques plus un horodatage fixé et une graine /ID fixée produisent les mêmes octets, ce qu'une étape de reconstruction-et-comparaison confirme.

Une petite forme complète. Les réglages sont construits une seule fois et réutilisés, de sorte que deux exécutions du même programme émettent le même fichier.

<?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 graine est une entrée de build que tu contrôles, pas un secret. Stocke-la à côté du reste de ta configuration de build. Le point est qu’elle est fixe, de sorte que l’identifiant de fichier qu’elle produit l’est aussi.

Le premier piège est « j’ai supprimé l’horodatage, donc mon build est reproductible maintenant ». Ce n’est généralement pas le cas, car le tableau /ID est la plus discrète des deux sources. Les dates sont visibles dans un panneau de métadonnées et faciles à retenir ; le /ID du trailer est invisible pour la plupart des lecteurs et est régénéré à partir de l’horloge et d’une source aléatoire à chaque exécution. Un build qui ne fixe que les dates produit toujours un fichier différent à chaque fois. Tu dois immobiliser les deux.

Le second piège est de traiter le déterminisme comme une fonctionnalité de sécurité en soi. Un /ID fixé rend un fichier reproductible ; il ne le rend pas signé, et il ne prouve pas à lui seul que deux builds correspondent. Une comparaison octet par octet ou un hachage prouve que les builds correspondent ; fixer le /ID ne supprime qu’une source de différence parasite. Et aucun de ces deux ne prouve qu’un tiers se porte garant du fichier. Reproductibilité et signature sont des couches complémentaires, pas des substituts.

Le déterminisme fixe les parties mobiles propres au moteur. Il ne fixe pas tes entrées. Si ton contenu incorpore un horodatage en direct, tire une police qui a changé sur le disque, ou rend une valeur qui dépend de la date actuelle, la sortie change parce que l’entrée a changé — et c’est correct. DeterministicSettings supprime le non-déterminisme du moteur, pas le tien. Un build reproductible exige toujours des entrées reproductibles.

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
  • Tests sur fichiers de référence — la technique de CI qui dépend d’une sortie identique octet par octet, et pourquoi un moteur déterministe en est la condition préalable.
  • Mises à jour incrémentales — comment un PDF croît par ajout, là où le tableau /ID compte à nouveau pour relier un fichier à ses versions antérieures.
  • Métadonnées et paquet XMP — où vivent les dates incorporées, et comment le paquet XMP reflète le dictionnaire d’information du document.
  • L’anatomie d’un fichier PDF — le trailer, la table de références croisées, et où le tableau /ID se situe dans la structure du fichier.
  • Identique octet par octet — deux fichiers qui correspondent exactement, octet par octet. La forme la plus forte de « le même », et celle qu’un hachage ou une comparaison peut vérifier.
  • /ID (identifiant de fichier) — le tableau de deux chaînes d’octets qui identifie un PDF et ses versions (ISO 32000-2 §14.4), stocké dans le dictionnaire du trailer (§7.5.5). Généralement dérivé de l’horloge plus des octets aléatoires, ce qui explique pourquoi il change à chaque build non fixé.
  • Dictionnaire d’information du document — la structure qui porte CreationDate et ModDate (ISO 32000-2 §14.3.3). L’une des deux sources de non-déterminisme qu’un build déterministe doit fixer.
  • Fichier de référence — une sortie connue comme bonne et enregistrée à laquelle un test se compare ; significatif seulement lorsqu’un moteur inchangé reproduit un fichier inchangé.
  • Build reproductible — un build dont la sortie est une fonction déterministe de ses entrées, de sorte que reconstruire à partir des mêmes entrées produit les mêmes octets. Le terme vient du projet Reproducible Builds pour les logiciels compilés.