losLab PDF Library peut produire une sortie PDF identique octet pour octet pour une entrée identique, une fois que vous appelez SetDeterministicDocumentID(1). Par défaut, le tableau /ID du trailer est un condensé MD5 de l horloge murale, donc deux exécutions du même générateur diffèrent au moins sur ces octets. Le mode déterministe dérive /ID d une graine stable à la place, ce qui restaure les builds reproductibles
Le symptôme apparaît généralement en CI avant que quiconque ne parte à sa recherche. Le modèle n a pas changé, l enregistrement d entrée n a pas changé, les polices n ont pas changé, et le PDF généré produit toujours un hachage différent à chaque exécution du pipeline. Les caches de build ne trouvent jamais de correspondance. Le stockage adressable par contenu accumule un nouveau blob à chaque build nocturne. Des diffs de régression au niveau octet s allument sur des fichiers que personne n a touchés. Traquez le diff jusqu aux octets réels et ce sont presque toujours les mêmes quelques chiffres hexadécimaux assis dans le trailer du fichier
À quoi sert le tableau ID du trailer
Le /ID du trailer est un marqueur d identité de fichier, pas une somme de contrôle du contenu. La norme ISO 32000-1 §14.4 le définit comme un tableau de deux chaînes d octets : le premier élément est l identifiant permanent attribué à la création du document et destiné à survivre à chaque modification ultérieure, et le second élément est l identifiant changeant qu un rédacteur rafraîchit à chaque modification du fichier. Ensemble, ils permettent à un système de décider si deux fichiers sont des révisions d un même document ou deux documents sans rapport. Le §7.5.5 rend l entrée effectivement obligatoire en pratique, puisque le trailer doit porter /ID dès qu il porte aussi /Encrypt
Rien dans la spécification ne dit comment calculer la valeur. La recommandation est un condensé d éléments tels que l heure actuelle, le chemin du fichier, la taille du fichier et le dictionnaire d informations du document, et l horloge murale est l ingrédient qui rend le résultat unique. C est exactement la propriété que l on souhaite pour l identité et exactement la propriété qui détruit la reproductibilité, raison pour laquelle cela doit être un interrupteur explicite plutôt qu un changement de comportement silencieux
Pourquoi le même build produit-il un PDF différent à chaque fois ?
Parce que l identifiant par défaut est dérivé du moment de la génération. Historiquement, losLab PDF Library construisait les chaînes /ID à partir d un MD5 de l horodatage actuel, donc un document créé deux fois à une seconde d intervalle porte deux identifiants permanents différents même quand tous les autres octets du fichier sont identiques. Le coût en aval est réel : un système de build qui indexe les artefacts par hachage ne peut jamais réutiliser une étape PDF, un magasin d objets à déduplication conserve une copie par build au lieu d une copie par document, et un relecteur examinant un diff binaire doit prouver que le seul changement est du bruit avant de faire confiance au reste du diff. La génération déterministe de /ID existe pour éliminer ce bruit, dans le même esprit que le travail de stabilité de mise en page décrit dans les notes sur les flux d objets et les flux de table de références croisées
Passer à un identifiant reproductible
Le mode déterministe est optionnel, par document, et désactivé par défaut afin que la sortie existante reste inchangée tant que vous ne le demandez pas. SetDeterministicDocumentID accepte 0 ou 1 et renvoie 1 lorsque la valeur a été acceptée, 0 pour toute valeur hors plage ; GetDeterministicDocumentID rapporte l état actuel. SetDocumentIDSeed fournit une chaîne de graine explicite qui l emporte sur tout le reste, et passer une graine vide revient à la graine dérivée. GetDocumentFileID relit /ID[0] après l enregistrement afin que vous puissiez le journaliser ou l affirmer par test
var
Lib: TPDFlib;
FileID: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed('invoice-4471-rev3');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Invoice 4471');
Lib.SaveToFile('invoice.pdf');
FileID := Lib.GetDocumentFileID; // identical on every run
finally
Lib.Free;
end;
end;
Le rafraîchissement a lieu au moment de l enregistrement, pas quand vous activez l indicateur, donc activer le mode déterministe tard dans la construction d un document prend quand même effet. Cela signifie aussi qu une graine modifiée atteint le fichier lors du prochain enregistrement complet : définissez la graine A, enregistrez, définissez la graine B, enregistrez, et les deux fichiers portent des identifiants différents, tandis que restaurer la graine A restaure la valeur d origine. Une graine explicite est le bon choix chaque fois que votre document possède une clé stable naturelle telle qu un numéro de facture, une révision d enregistrement ou un identifiant de commit git, car cela découple l identifiant des métadonnées accessoires
D où vient la graine quand vous n en fournissez pas ?
Sans graine explicite, losLab PDF Library en dérive une à partir de l état du document censé être invariant entre des régénérations identiques : l en-tête de version PDF, le nombre de pages, et chaque entrée du dictionnaire d informations du document. Les valeurs chaîne et nom sont prises telles quelles, les autres types d objet contribuent leur forme sérialisée, et le tout est haché dans les chaînes /ID. La conséquence importante est que CreationDate et ModDate font partie du dictionnaire d informations et donc de la graine par conception. Deux exécutions n obtiennent le même identifiant que lorsqu elles produisent réellement les mêmes métadonnées de document
Lib.SetDeterministicDocumentID(1);
// No SetDocumentIDSeed: the seed is derived from document state,
// so the timestamps in the Info dictionary have to be pinned.
Lib.SetInformation(2, 'Quarterly Report'); // Title
Lib.SetInformation(5, 'reporting-service 4.2'); // Creator
Lib.SetInformation(7, 'D:20260101000000Z'); // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z'); // ModDate
Lib.SaveToFile('report.pdf');
Épingler ModDate avec la clé 8 fait double emploi, et c est la partie qui piège les gens. Un /ID déterministe seul ne rend pas le fichier identique octet pour octet, car le chemin d enregistrement estampille ModDate avec l heure actuelle à moins que l appelant ne l ait définie explicitement. Définir la clé 8 marque la valeur comme fournie par l appelant et supprime cet estampillage. Si vous voulez un fichier reproductible plutôt que simplement un identifiant reproductible, traitez les horodatages de métadonnées comme des entrées de build : dérivez-les de l enregistrement source ou d une époque fixe, jamais de Now
Pourquoi réécrire l ID casse-t-il un PDF chiffré ?
Parce que /ID[0] n est pas une simple métadonnée dans un document chiffré, c est du matériel de clé. La norme ISO 32000-1 §7.6.3.3 Algorithme 2 fait entrer le premier élément de l identifiant du fichier dans le calcul de la clé de chiffrement pour le gestionnaire de sécurité standard aux révisions 2 à 4, aux côtés du mot de passe complété, de la valeur /O et des bits de permission. La clé dérivée produit alors la chaîne de validation /U qu un lecteur vérifie à l ouverture, et la clé du fichier est dérivée et mise en cache lorsque vous appelez Encrypt ou lorsqu un document chiffré est chargé, ce qui se produit toutes deux avant l enregistrement. Réécrire l identifiant pendant l enregistrement produirait donc un fichier structurellement valide dont la vérification /U échouerait à la réouverture : pas une corruption subtile mais un document que personne ne peut ouvrir, y compris vous. C est pourquoi le rafraîchissement déterministe est restreint aux documents qui ne portent pas d état de chiffrement, et pourquoi un document chiffré conserve quel que soit le /ID qu il avait déjà, mode déterministe ou non, et le paramètre n a simplement aucun effet sur ce chemin. La gestion des révisions et la sémantique des permissions associées sont couvertes dans la présentation de l audit de chiffrement et de permissions PDF. Notez également que le chemin de restauration du chiffrement ne rafraîchit que /ID[1], l identifiant de changement, exactement comme le prévoit le §14.4
Pourquoi les enregistrements incrémentiels conservent-ils l identifiant d origine
La seconde limite est le mode d ajout. Une mise à jour incrémentielle laisse chaque octet antérieur du fichier intact et écrit une nouvelle révision après lui, et la permanence de /ID[0] à travers le §14.4 est ce qui indique à un consommateur que la nouvelle révision appartient au même document que l ancienne. La réécrire romprait ce lien, contredirait les révisions déjà présentes dans le fichier, et interférerait avec la sémantique de signature, puisqu une signature couvre une plage d octets d une révision spécifique d un document spécifique. losLab PDF Library rafraîchit donc l identifiant déterministe uniquement lors des enregistrements complets et jamais en mode d ajout, ce qui préserve la garantie décrite dans l article sur les mises à jour incrémentielles PDF et l ajout au flux
Un point de passage unique pour la génération d identifiant
Toute la génération de /ID dans losLab PDF Library transite désormais par une seule routine interne, NewFileIDString, ce qui rend l interrupteur déterministe digne de confiance plutôt qu un correctif sur un seul chemin de code. La création de document vierge, la création paresseuse d un tableau /ID manquant à la demande, et le chemin de restauration de l empreinte de chiffrement l appellent tous, donc il n y a qu un seul endroit où l horloge murale pourrait se réinfiltrer. Cela signifie aussi que de futures variantes, comme un identifiant dérivé du contenu, sont un changement d une seule fonction plutôt qu un audit de tout le sérialiseur
function BuildQuote(const Seed: WideString): AnsiString;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed(Seed);
Lib.SetInformation(7, 'D:20260101000000Z');
Lib.SetInformation(8, 'D:20260101000000Z');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Quote 8812');
Result := Lib.SaveToString;
finally
Lib.Free;
end;
end;
// Regression guard: two independent builds, one byte sequence.
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
WriteLn('reproducible')
else
WriteLn('nondeterminism leaked into the output');
Câblez cette comparaison dans votre suite de tests avant de vous fier à une sortie reproductible ailleurs, car elle échoue bruyamment dès qu une nouvelle fonctionnalité réintroduit un horodatage. La reproductibilité est une propriété qui se dégrade silencieusement sinon, et une seule assertion sur deux enregistrements en mémoire ne coûte presque rien à exécuter à chaque build
L API d identifiant déterministe présentée ici est livrée avec losLab PDF Library pour Delphi et C++Builder, aux côtés de la référence complète sur les informations de document, le chiffrement et l enregistrement incrémentiel