PDFlibPas, la losLab PDF Developer Library, conserve le texte décimal exact qu'il a analysé pour chaque nombre réel d'un document et réécrit ce texte tel quel dès que la valeur n'a jamais été modifiée. Depuis la v3.539.19, le réglage SetPrecision ne gouverne plus que les nombres que la bibliothèque crée ou modifie, donc un banal chargement-enregistrement n'arrondit plus un /Gamma CalRGB de 2.22221 à 2.2222 et ne décale plus les couleurs d'une page que personne n'a touchée. Le changement est petit en code et lourd en ce qu'il dit des analyseurs : la valeur que vous décodez et le littéral que vous émettez sont deux choses différentes, et un aller-retour par Double n'est pas une transformation identité
Pourquoi une sauvegarde sans modification décalait-elle les couleurs de la page ?
Parce que ce sont les paramètres de l'espace de couleur qui étaient reformatés, pas l'image. Le fichier qui a révélé cela est un document de bureau de 35 pages du corpus de régression local, avec une image d'en-tête réutilisée sur chaque page. Le charger puis l'enregistrer tel quel produisait des flux d'image identiques octet pour octet à l'entrée, et une comparaison de hachages de flux déclarait le document inchangé. Une comparaison de rendu n'était pas d'accord : chacune des 35 pages montrait des différences de pixels dans l'en-tête, et nulle part ailleurs
L'image d'en-tête se dessine via un espace de couleur CalRGB, qu'ISO 32000-1 §8.6.5.3 définit par un /WhitePoint, un tableau /Gamma facultatif de trois éléments et une /Matrix facultative de neuf éléments. Ces tableaux sont de simples objets numériques dans le dictionnaire d'espace de couleur. TPDFNumeric stockait chacun comme un Double et rien d'autre, et TPDFNumeric.Output formatait ce Double via PDFPrecNum, qui vaut quatre décimales par défaut. Donc /Gamma passait de 2.22221 à 2.2222, une entrée de matrice passait de 0.71519 à 0.7152, et le moteur de rendu produisait fidèlement des couleurs légèrement différentes à partir d'un calibrage légèrement différent. Les octets de l'image étaient innocents ; les nombres autour ne l'étaient pas. Le plus inconfortable est à quel point cela était invisible. Comparer les octets des flux décodés ne peut pas le voir, parce que les nombres vivent dans un dictionnaire, pas dans un flux. Comparer les charges des pièces jointes ne peut pas le voir non plus. Même le diff de révision décrit dans l'article sur les niveaux de modification calcule l'empreinte d'un corps d'objet normalisé, donc les deux révisions donnent le même hachage et le diff les déclare identiques. Seul le rendu l'a attrapé, et c'est pourquoi la référence du corpus rend chaque page au lieu de se fier aux seuls contrôles structurels
La valeur que vous avez analysée n'est pas le littéral que vous devez écrire
Un nombre réel PDF est une chaîne décimale, et ISO 32000-1 §7.3.3 est explicite : ce n'est qu'une chaîne décimale, pas de notation de base, pas de forme exponentielle. L'annexe C liste ensuite la précision qu'une implémentation est censée honorer, environ cinq chiffres décimaux significatifs dans la partie fractionnaire. Une précision de sortie par défaut de quatre est déjà en dessous, et cela empire près de zéro : PLDoubleToStr met la valeur à l'échelle, arrondit à un entier et émet 0 quand le résultat est nul, donc une entrée de matrice valant -0.000012345 ne perd pas un chiffre, elle disparaît complètement
Relever la valeur par défaut ne ferait que déplacer la falaise. Le correctif consiste à cesser de faire comme si un Double était le nombre. Quand le tokenizer de TPDFStructure.Decode reconnaît un réel standard, c'est-à-dire un token contenant un point décimal et aucun marqueur d'exposant, il stocke le texte source dans le nouveau champ FOriginalText à côté de la valeur convertie. Output préfère alors ce texte et ne retombe sur le formatage que quand il n'y a rien à préférer
// Lib/PDFlibStruct.pas — tout le correctif côté sortie
Function TPDFNumeric.Output: AnsiString;
Begin
If FOriginalText<> '' Then
Result:= FOriginalText
Else
Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;
Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
FOriginalText:= ''; // un nombre modifié est un nouveau nombre
FValue:= Value;
FChanged:= True;
End;
Deux frontières sont délibérées. Les entiers ne sont pas préservés, parce que le formatage des entiers est déjà sans perte. Les formes exponentielles comme 6.02E23 sont tolérées à l'entrée pour ménager les producteurs défaillants mais ne sont pas préservées en sortie, puisque les réécrire perpétuerait une syntaxe que §7.3.3 interdit ; elles passent par le formateur comme n'importe quel nombre généré par la bibliothèque. Le tokenizer applique aussi sa réparation minimale habituelle avant de stocker le texte, donc un littéral à point initial comme .5 est conservé sous 0.5 et un littéral à point final comme 5. sous 5.0. Les deux sont le même nombre pour tout lecteur et sont bien plus largement acceptés
Que garantit SetPrecision après la v3.539.19 ?
TPDFlib.SetPrecision contrôle désormais les décimales des nombres que la bibliothèque produit elle-même : les valeurs dessinées via le painter, les nombres créés à partir d'un Double comme via NewNumeric, et toute valeur analysée qui a été modifiée depuis avec SetTo. Notez que le texte décodé via l'API objet, par exemple un littéral passé à SetObjectFromString, traverse le même tokenizer et est préservé de la même façon. Un décimal analysé qui n'a jamais été modifié garde sa précision d'entrée quel que soit le réglage, et changer le réglage après le chargement ne le touche pas rétroactivement. L'entrée de référence de SetPrecision a été mise à jour dans la même version pour dire exactement cela, car l'ancienne formulation laissait entendre que le réglage s'appliquait à tous les nombres du fichier
L'effacement se produit dans SetTo plutôt que d'être dérivé du drapeau Changed, et cette distinction compte. Le pipeline de sauvegarde remet Changed à zéro sur les objets une fois qu'ils ont été écrits, donc un contrôle du type « émettre le texte d'origine sauf si modifié » se mettrait à émettre du texte périmé pour une valeur modifiée, enregistrée puis modifiée à nouveau dans la même session. Lier le texte d'origine à l'affectation elle-même rend impossible que les deux se contredisent. Le test de régression verrouille chacun de ces comportements avec les valeurs du fichier d'origine
uses
PDFlibStruct;
var
Structure: TPDFStructure;
Values: TPDFArray;
Number: TPDFNumeric;
begin
Structure := TPDFStructure.Create;
try
Structure.PDFPrecNum := 4;
Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
// L'entrée non modifiée survit telle quelle, y compris la valeur que
// le formatage à quatre décimales aurait réduite à 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Une modification jette le texte d'origine et suit PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Abaisser la précision ensuite n'atteint pas l'entrée non modifiée
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Pourquoi le modèle de contenu normalise-t-il encore les nombres ?
Parce que TPDFContentProgram promet des opérandes numériques canoniques, et que cette promesse vaut plus que le texte tel quel à l'intérieur d'un flux de contenu. Le modèle de contenu éditable, celui-là même sur lequel est construit le suivi d'état graphique, existe pour que NormalizeContentStreams, l'optimiseur et Emit produisent une sortie stable et comparable à partir d'une entrée quelconque. Si un opérande analysé transportait son texte d'origine dans le modèle, une séquence d'opérateurs comme 0.50000 0 0 RG s'émettrait différemment de 0.5 0 0 RG, et toutes les comparaisons en aval dériveraient avec les habitudes de formatage du producteur
Le modèle retire donc le texte d'origine à ses deux points d'entrée. NormalizeContentNumbers s'exécute sur chaque opérande quand l'analyseur l'empile, et de nouveau dans SetOperand quand la source fournie par l'appelant est décodée, et il descend récursivement dans les tableaux et les dictionnaires pour que les motifs de tirets, les tableaux TJ et les dictionnaires de propriétés du contenu balisé soient couverts. Appeler SetTo(AsDouble) sur chaque numérique suffit, puisque c'est précisément l'opération qui efface le texte. Les données d'image inline brutes sont laissées tranquilles, comme elles l'ont toujours été
// Lib/PDFlibContentModel.pas — le modèle de contenu garde son contrat
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
K: Integer;
Begin
If Obj is TPDFNumeric Then
TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
Else If Obj is TPDFArray Then
For K:= 0 To TPDFArray(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFArray(Obj).Item[K])
Else If Obj is TPDFDictionary Then
For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;
La règle pratique pour les appelants est donc simple. Un LoadFromFile suivi d'un SaveToFile laisse les flux de contenu non touchés et les nombres de dictionnaire non touchés tels qu'ils étaient. Une page qui passe par NormalizeContentStreams, ou toute modification faite via le modèle de contenu, ressort canonique par conception, et le reste du document est toujours préservé. Ce sont deux demandes différentes, et elles font désormais deux choses différentes
Ce que cela coûte, et où la garantie s'arrête
Chaque TPDFNumeric porte maintenant une référence AnsiString de plus, et chaque décimal analysé garde son texte source vivant aussi longtemps que l'objet. Sur un document à millions de nombres réels, c'est de la mémoire réelle, et cela a sa place dans toute mesure sur gros documents plutôt que d'être balayé d'un revers de main. La garantie est aussi limitée au document du nombre lui-même : copier des objets entre documents ou reconstruire des valeurs via l'API objet produit de nouveaux nombres, qui suivent la précision de sortie comme n'importe quel autre nouveau nombre. Il vaut la peine d'être précis sur ce que la version affirme et sur ce qu'elle n'affirme pas. Un chargement-enregistrement d'un document non touché préserve désormais les nombres de calibrage que le moteur de rendu consomme réellement, ce qui est la propriété que vérifie la référence du corpus. Elle ne prétend pas produire une sortie identique octet pour octet, ce qui dépend aussi de la numérotation des objets, de la compression des flux et de l'identifiant de trailer traité dans l'article sur l'ID PDF déterministe. Et elle ne fait pas voir au diff d'empreintes les différences d'arrondi des fichiers produits par d'autres logiciels, puisque ceux-ci hachent toujours le corps normalisé. La leçon se généralise bien au-delà de CalRGB : quand un analyseur ne garde que la valeur convertie, chaque sauvegarde est une modification, et le seul moyen de s'en apercevoir est de regarder le résultat rendu. Le traitement des nombres et la sémantique de SetPrecision sont documentés sur la page produit losLab PDF Developer Library