Un flux marqué /Predictor 12 ne signifie pas que chaque ligne utilise le filtre PNG 2. HotPDF, le composant VCL natif pour Delphi et C++Builder, traite les valeurs de prédicteur 10 à 15 comme une seule famille : le tag de filtre réel, de 0 à 4, est le premier octet de chaque ligne encodée, et HPDFDecodePredictor lit et valide ce tag ligne par ligne. Cette distinction est à l'origine de presque tous les bogues dans ce recoin du PDF, car rien ne se déclenche quand on se trompe. La chaîne de filtres s'exécute, le raster a la taille attendue, et l'image ressort sous forme de parasite diagonal ou de dégradé qui dérive de plus en plus à chaque ligne de balayage. Les cinq nombres du /DecodeParms (ISO 32000-1 §7.4.4) changent surtout le sens des octets plutôt que leur longueur, si bien qu'un nombre erroné produit du bruit plausible plutôt qu'une erreur
Pourquoi /Predictor 12 ne signifie-t-il pas le filtre PNG 2 sur chaque ligne ?
Parce que le numéro de prédicteur indique seulement « la prédiction PNG est utilisée », pas quel filtre. Les encodeurs PNG choisissent un filtre par ligne de balayage et le filtre PDF en hérite, si bien que les valeurs de prédicteur 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) et 15 (Optimum) se décodent toutes de façon identique : c'est l'octet de tag en tête de chaque ligne que le décodeur doit respecter. La conséquence sur la disposition compte autant que la sémantique. Chaque ligne encodée fait 1 + RowBytes octets, l'entrée dépasse donc la sortie exactement du nombre de lignes, et un flux dont la longueur n'est pas un multiple entier de RowBytes + 1 est tronqué par définition. HotPDF vérifie cette limite avant de toucher un seul octet, rejette tout tag supérieur à 4 avec Invalid PNG predictor row tag, et lit la ligne précédente directement dans l'unique tampon de sortie plutôt que de matérialiser un tableau de lignes à deux dimensions. Les filtres 1 et 3 remontent de BytesPerPixel à l'intérieur de la ligne courante, le filtre 2 lit tout droit vers le haut, le filtre 4 applique le choix de Paeth entre gauche, haut et haut-gauche — et les quatre opèrent sur une sortie déjà reconstruite, ce qui explique pourquoi la ligne du dessus doit être la ligne décodée et jamais l'entrée filtrée
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
L'argument MaxOutputBytes n'est pas décoratif. Une étape de prédicteur est une étape de décompression déguisée, et une valeur /Columns hostile ou simplement corrompue transforme quelques kilo-octets d'entrée en une demande d'allocation de plusieurs gigaoctets. HotPDF calcule d'abord les bits par ligne, les octets par ligne et la taille totale du raster en Int64, refuse toute géométrie qui déborde, et respecte le plafond fourni par l'appelant. Passez une véritable borne dérivée du dictionnaire d'image, et le mode d'échec devient un message journalisé plutôt qu'une boîte de dialogue de manque de mémoire sur la machine d'un client
Pourquoi TIFF Predictor 2 corrompt-il les images en 4 bits ?
Parce que le Predictor 2 est une différenciation horizontale par échantillon, pas par octet, et qu'à 1, 2 ou 4 bits par composante, plusieurs échantillons partagent un octet. L'implémentation courante ajoute l'octet N-Colors à l'octet N, ce qui se trouve être correct à 8 bits par composante et silencieusement faux partout ailleurs. Un balayage RVB 8 bits se décode parfaitement, puis le même code détruit une image indexée 4 bits la première fois que l'une d'elles apparaît en production
L'arithmétique correcte opère à l'intérieur du champ de bits. HotPDF parcourt les échantillons de l'index Colors à Colors * Columns - 1, extrait l'échantillon et son voisin gauche de même composante avec un masque de (1 shl BitsPerComponent) - 1 au décalage approprié, les additionne modulo ce masque, et réécrit le résultat sans perturber les autres échantillons empaquetés dans le même octet. La fin de ligne compte également : une ligne est complétée jusqu'à une frontière d'octet, si bien que les bits de remplissage après le dernier échantillon doivent survivre intacts plutôt que d'être intégrés dans l'arithmétique. À 16 bits par composante, chaque échantillon est une paire d'octets en big-endian et l'addition boucle à $FFFF sur la paire plutôt que de propager une retenue entre octets indépendamment ; à 8 bits, la simple récurrence par octet est correcte, en avançant par pas de Colors afin que le rouge s'accumule contre le rouge et l'alpha contre l'alpha. Dans chaque variante, le premier pixel d'une ligne est un littéral, jamais une différence, et la récurrence redémarre à chaque frontière de ligne — la prédiction TIFF ne lit jamais la ligne du dessus, ce qui est toute la différence avec la famille PNG
Que contrôle réellement EarlyChange dans LZWDecode ?
Il contrôle le moment où le lecteur élargit sa taille de code d'un bit, et être décalé d'un seul code corrompt tout ce qui suit. HotPDF exprime la règle comme un invariant unique : après l'ajout d'une entrée au dictionnaire, la prochaine lecture s'élargit quand NextCode atteint (1 shl CodeSize) - Ord(EarlyChange). Avec /EarlyChange 1, la valeur par défaut d'ISO 32000-1 §7.4.4, le changement se produit un code trop tôt ; avec /EarlyChange 0, il se produit exactement à la limite. Les deux apparaissent dans des fichiers réels et rien dans le flux binaire n'indique lequel l'encodeur a utilisé. Le reste de la machine à états doit avancer en synchronisme : un code de remise à zéro (clear code) réinitialise ensemble la taille de code, le masque de bits, le prochain code libre et le stockage des phrases, et le code de fin d'information est lu à la largeur alors en vigueur à ce moment, pas aux 9 bits initiaux. HotPDF démarre à InitialCodeSize 9, plafonne la taille de code à 12 et le dictionnaire à 4096 entrées, et fixe par défaut FillOrder à foTop parce que le PDF empaquette les codes avec le bit de poids fort en premier — foBottom existe pour les flux de style TIFF qui ne le font pas
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
Les statistiques existent pour le triage, pas pour la vanité. Quand un fichier se décode à la bonne longueur mais avec les mauvais pixels, PeakCodeSize et DictionaryAdds indiquent immédiatement si le lecteur s'est élargi là où l'écrivain l'a fait. Basculez EarlyChange, décodez à nouveau, comparez les deux : si les nombres bougent, vous avez votre réponse en une seule exécution plutôt qu'en parcourant un lecteur de bits
La branche KwKwK, et quand un flux devrait simplement échouer
L'unique cas légal qui a l'air illégal est Code = NextCode, et HotPDF le gère en construisant l'entrée avant de l'émettre. Un encodeur peut émettre le code d'une phrase qu'il est en train de définir dans la même étape, ce qui se produit chaque fois que l'entrée contient un motif de la forme K w K w K ; le décodeur ne peut pas rechercher ce code car il n'existe pas encore, il doit donc construire Previous + First(Previous), l'ajouter comme nouvelle entrée, et émettre l'entrée qu'il vient de créer. HotPDF compte ces occurrences dans KwKwKExpansions et vérifie que le code qu'il a ajouté est bien le code demandé. Tout ce qui dépasse NextCode est de la corruption, et là un décodeur doit s'arrêter plutôt qu'improviser : HotPDF lève une exception sur un code futur, sur un préfixe de dictionnaire pointant hors de l'arène des phrases, sur un dictionnaire plein, et sur un premier code qui n'est pas un littéral. Deux commutateurs de rigueur sont délibérément désactivés par défaut, RequireInitialClear et RequireEndOfInformation, car de nombreux PDF de production omettent le code de remise à zéro initial ou manquent de données sans terminateur. Activez-les pour valider votre propre sortie, laissez-les désactivés pour consommer des fichiers venus de la nature
Où /DecodeParms est réellement lu côté document chargé
HotPDF résout /DecodeParms ou son abréviation /DP sur le dictionnaire du flux image, accepte soit un dictionnaire soit un tableau et prend le dernier élément lorsqu'il s'agit d'un tableau, puis transmet Predictor, Colors, BitsPerComponent, Columns et EarlyChange au chemin du raster. Le cas du tableau est celui qu'on oublie : un flux filtré par [/ASCII85Decode /FlateDecode] porte un tableau de paramètres parallèle, et les réglages du prédicteur appartiennent au dernier filtre, pas au premier
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Un défaut historique sur ce chemin mérite d'être cité, car cette classe de bogue se répète. L'ancienne routine Flate-avec-paramètres créait un flux de décompression puis copiait à partir de l'entrée compressée d'origine, si bien que l'étape de prédicteur recevait des octets compressés et les dé-prédisait consciencieusement : toujours faux, jamais signalé. Le code actuel ne lit qu'à partir du décodeur avant de transmettre le résultat au prédicteur partagé, et rejette un raster plus court que la taille calculée au lieu de se rabattre sur les octets encore compressés — un repli qui transformait autrefois un échec de décodage en bitmap corrompu. Cette même implémentation de prédicteur sert désormais aussi les flux de renvois, ce qui est une cohérence utile si vous travaillez aussi avec les flux d'objets et les mises à jour incrémentales, et la mécanique d'extraction environnante est couverte dans l'article compagnon sur l'extraction des images chargées et leurs filtres de décodage. Les images arrivant en DCTDecode ou en JPXDecode n'atteignent jamais le prédicteur ; elles portent leur propre modèle de pixels compressé
Débit : une arène de phrases contiguë contre des chaînes par entrée
Remplacer le dictionnaire de chaînes par entrée par une arène de phrases contiguë a mesuré environ 1,61 fois plus rapide sur une entrée pathologique : 1558 Mio/s contre 969 Mio/s sur un benchmark dont la plus longue phrase unique atteint 7 370 880 octets. La forme de cette entrée explique l'écart, car les implémentations classiques choisissent l'un des deux mauvais compromis. Un dictionnaire de valeurs AnsiString alloue et copie une nouvelle chaîne pour chacune des jusqu'à 4096 entrées, chaque nouvelle entrée copiant intégralement son parent ; une pile préfixe/suffixe évite entièrement cette mémoire mais reconstruit chaque phrase en remontant la chaîne à rebours octet par octet puis en l'inversant, ce qui convient pour du texte ordinaire et devient pénible quand une phrase atteint des mégaoctets. HotPDF ajoute chaque phrase de façon contiguë à une arène croissant géométriquement, indexe les entrées par décalage et longueur, et émet une phrase par un unique Move dans le tampon de sortie. Le coût honnête est la mémoire : une arène contenant chaque phrase en entier est bornée par la somme des longueurs de toutes les phrases plutôt que par le nombre d'entrées, ce qui explique précisément pourquoi MaxOutputBytes existe à la fois sur le décompresseur et sur le prédicteur. Dérivez cette limite de ce que le dictionnaire d'image prétend que le raster devrait être, et un flux mensonger échoue rapidement
Le décompresseur LZW, le prédicteur partagé et le chemin d'extraction des images chargées présentés ici sont livrés dans le composant HotPDF standard pour Delphi et C++Builder, avec la référence complète des filtres et de DecodeParms sur la page produit