Une table de lookup /Indexed stocke des composants de son espace de couleur de base, pas des triplets RGB. Une base DeviceGray empaquette un octet par entrée, DeviceCMYK quatre, Lab trois qui exigent une mise à l'échelle de plage, et ICCBased ce que le /N du profil déclare. HotPDF normalise toutes à l'analyse dans la palette RGB publique que THPDFColorSpace expose
Cette phrase, c'est toute la classe de bugs. Presque chaque rendu de palette commence sa vie en supposant un stride de trois octets, parce que l'écrasante majorité des espaces /Indexed du monde réel s'assoient sur /DeviceRGB et l'hypothèse tient pendant des années. Puis une facture scannée arrive avec [/Indexed /DeviceCMYK 15 <...>], le lecteur parcourt la chaîne de lookup trois octets à la fois, et chaque index au-delà de zéro atterrit au milieu de la couleur de quelqu'un d'autre. La sortie n'est pas vide et elle ne plante pas. C'est du charabia à l'air plausible, ce qui est bien pire, parce que personne ne dépose un bug contre des couleurs qui ont simplement l'air fausses
Pourquoi une palette Indexed sort-elle fausse ?
Le stride est faux parce que le stride n'est pas une constante. ISO 32000-1 §8.6.6.3 définit la table de lookup comme m × (hival + 1) octets, où m est le nombre de composants de l'espace de base, et il ne dit rien sur cette base étant RGB. La même section n'interdit d'emblée que deux bases : /Indexed lui-même et /Pattern. Tout le reste est permis, y compris les quatre familles basées CIE des §8.6.5.2 à §8.6.5.5
Donc le vrai travail n'est pas de lire des octets, c'est de résoudre la base d'abord et de lui demander quelle largeur fait une entrée. ResolveIndexedPalette dans HotPDF fait exactement cela : il résout la base en un THPDFColorSpace, prend ComponentCount depuis l'enregistrement résolu, et rejette le tableau d'emblée si la base revient en csfIndexed, csfPattern ou csfUnsupported. Ce n'est qu'alors qu'il dimensionne le budget d'octets comme PaletteCount * Count, ce qui pour le maximum légal de hival 255 et une base à quatre composants plafonne à 1024 octets. Deux détails dans ce dimensionnement sont là où un fichier hostile vous sonderá. HotPDF clampe hival dans 0..255 avant de l'utiliser, selon la limite du §8.6.6.3, donc un tableau de lookup revendiquant hival 100000 ne se transforme pas en allocation d'un gigaoctet ; et il tronque le budget à ce que les données de lookup contiennent réellement, puis arrête de remplir la palette à la première entrée dont les octets dépassent la fin. Une table courte donne une palette courte, pas une lecture au-delà du buffer
Normaliser quatre espaces de base différents en une table RGB
HotPDF convertit les palettes en RGB pendant l'analyse plutôt qu'au moment du dessin, et cette décision pilote le reste de la conception. Le champ public THPDFColorSpace.Palette est un TBytes de triplets RGB avec une longueur 3 * PaletteCount, et il a cette forme depuis bien avant que les bases CIE soient supportées. Réécrire le layout de l'enregistrement pour porter les composants natifs de la base aurait cassé chaque consommateur du champ. Convertir à l'analyse garde le layout intact et, en bonus, route les entrées Gray, CMYK, Lab, CalGray, CalRGB et ICCBased à travers le même chemin HPDFResolveColor que le rendu utilise déjà pour les opérateurs de couleur directs
L'étape de mise à l'échelle est là où les espaces de base cessent d'être interchangeables. Un octet de lookup est toujours 0..255, mais ce que cet octet veut dire dépend de la plage de composants que la base déclare. Les composants Device et calibrés se projettent sur 0..1. Lab non : le composant L* court de 0 à 100, pendant que a* et b* courent sur le /Range que l'espace déclare. HotPDF tire ces bornes des champs AMin, AMax, BMin et BMax de THPDFCIEParams, que le parseur Lab remplit depuis l'entrée /Range du dictionnaire et définit par défaut à −108..101 quand l'entrée est absente, donc ne codez pas en dur une plage symétrique de mémoire. Les bases ICCBased ajoutent une troisième source de vérité : le nombre de composants vient du /N du flux de profil, remonté comme ICCN, et un flux ICCBased peut lui-même porter un tableau /Range que HotPDF lit via HPDFReadNumericArrayUpTo dans la même table de bornes par composant. Une palette ICC à trois composants et une palette Lab à trois composants marchent donc la chaîne de lookup avec des strides identiques et une arithmétique complètement différente, ce qui est exactement le résultat qu'un stride RGB fixe ne peut pas produire
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObj est l'entrée /ColorSpace d'un XObject image ou d'un
// dictionnaire de ressources ; ce peut être un nom, un tableau ou une réf indirecte
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// La palette est déjà en RGB quel que soit l'espace de base
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // table de lookup courte, arrêt propre
R := CS.Palette[Entry * 3];
G := CS.Palette[Entry * 3 + 1];
B := CS.Palette[Entry * 3 + 2];
UseColor(Entry, R, G, B);
end;
end;
Quand la table de lookup vit dans un flux
ISO 32000-1 §8.6.6.3 permet que la table de lookup soit une chaîne ou un flux, et la forme flux est celle qui mord. Un flux peut porter /FlateDecode, il peut être un objet indirect partagé avec des parties sans rapport du fichier, et son TStream peut être positionné n'importe où au moment où votre parseur l'atteint. HotPDF gère le cas compressé en déléguant au callback d'octets décodés enregistré via HPDFSetColorStreamReader, le même callback que le moteur de fonction échantillonnée Type 0 utilise
Quand aucun lecteur n'est enregistré, le repli est délibérément étroit plutôt que best-effort. HotPDF vérifie le dictionnaire du flux pour /Filter et refuse la palette entière si un est présent, au lieu de nourrir des octets compressés dans la lookup et de produire une table avec assurance fausse. Sans filtre, il lit les octets bruts, plafonnés au budget d'octets calculé depuis hival et le nombre de composants de la base. Toute la branche flux s'assoit dans un try..finally qui restaure le Stream.Position original sur chaque chemin de sortie, y compris les refus, parce que l'objet est partagé et que le prochain consommateur ne vous remerciera pas pour un curseur déplacé. LookupStreamUsesDecodedReaderAndRestoresPosition dans la suite de régression épingle les deux moitiés de ce comportement
// Enregistrez les deux callbacks avant de résoudre un espace de couleur, sinon
// les bases indirectes et les flux de lookup compressés seront simplement refusés
HPDFSetColorSpaceResolver(MyResolveIndirectObject);
HPDFSetColorStreamReader(MyDecodedStreamReader);
try
CS := HPDFResolveColorSpace(ColorSpaceObj);
if (CS.Family = csfIndexed) and (Length(CS.Palette) = 0) then
LogWarning('lookup table unreadable: filtered stream or no reader');
finally
HPDFSetColorStreamReader(nil);
HPDFSetColorSpaceResolver(nil);
end;
Qu'est-ce qui empêche un espace de couleur Indexed de récurser à l'infini ?
Rejeter /Indexed comme base directe est nécessaire et loin d'être suffisant. L'attaque intéressante passe par le côté. Prenez un [/Indexed /Separation ...] valide, et rappelez-vous que le §8.6.6.4 laisse une Separation nommer un espace alternatif et que le §8.6.6.5 laisse DeviceN faire pareil. Faites de cet alternatif une référence indirecte qui pointe en retour vers le tableau Indexed d'origine. Rien dans la chaîne ne viole la règle de base directe à aucun saut individuel : la base est une Separation, l'alternatif de la Separation est un tableau, et le tableau se trouve être celui d'où vous êtes parti. Résolvez naïvement et le parseur marche la boucle jusqu'à ce que la pile cède. La mécanique de teinte-transformation sur laquelle ces espaces s'appuient est un sujet à part, couvert dans l'article sur le rendu des couleurs d'accompagnement Separation et DeviceN ; ici ils ne comptent que comme un maillon du cycle
HotPDF ferme cela au point d'entrée unique par lequel passe déjà chaque chemin. HPDFResolveColorSpace enveloppe le résolveur interne dans un compteur de profondeur borné par HPDFColorSpaceMaxDepth, qui vaut 64. Franchissez cette borne et le garde pose un drapeau, et le drapeau est ce qui compte : au déroulement, tout niveau d'imbrication qui l'a vu posé jette son propre enregistrement partiellement résolu et renvoie csfUnsupported, donc tout le graphe est rejeté plutôt que la moitié retenue, et les deux compteurs se réinitialisent quand l'appel le plus externe revient. Ces compteurs sont déclarés threadvar, pas des globals d'unité, et ce n'est pas du rangement accessoire : GResolveObj et GReadStream vivent dans le stockage local de thread pour la même raison, parce que les pages rendent en parallèle. Un global partagé laisserait un thread enfoncé dans une chaîne DeviceN imbriquée déclencher la limite pour un autre thread qui parse un [/Indexed /DeviceRGB 255 <...>] parfaitement ordinaire, produisant un échec non déterministe dépendant de la charge — le moins débogable qui soit. Soixante-quatre est une marge confortable pour le contenu légitime, qui s'imbrique rarement au-delà de quatre ou cinq niveaux, donc traitez un csfUnsupported venant d'un tableau à l'air structurellement valide comme digne d'être journalisé : en pratique cela veut dire un cycle ou une profondeur qu'aucun producteur réel n'émet. IndirectAlternateCycleIsUnsupported, paramétré sur Separation et DeviceN, et ExcessivePatternDepthIsUnsupported couvrent les deux cas
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// csfUnsupported ici couvre une famille réellement inconnue, un cycle
// via un alternatif indirect, et une profondeur au-delà de 64 niveaux
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray, l'espace initial du PDF
end;
Une frontière d'API qui vaut la peine d'être documentée pour vos appelants
HotPDF applique la configuration de conversion ICC en vigueur au moment de l'analyse quand il construit une palette Indexed, et des appels ultérieurs à HPDFSetColorSpaceRenderingIntent ne régénèrent pas une palette déjà résolue. Cette fonction marche les transformations ICCBased et alternatif de l'enregistrement que vous lui passez et reconstruit ces pipelines ; les octets RGB aplatis dans Palette sont un artefact de l'analyse et restent comme ils étaient. C'est une conséquence de normaliser tôt, pas un oubli, et cela mérite d'être écrit dans vos propres docs d'API plutôt que laissé à un client pour le découvrir. Si votre application laisse un utilisateur changer l'intention de rendu en pleine session, résolvez les espaces de couleur à nouveau après le changement ; si elle règle l'intention une fois pendant la configuration du pipeline, ce qui est le cas écrasant, il n'y a rien à faire. Le même ordre s'applique aux profils d'épreuvage installés via HPDFSetICCColorPipeline : configurez-les avant de commencer à résoudre, pas après
Le bug en miroir côté écriture
Tout ce qui précède parle de lecture. Le défaut symétrique côté écriture est plus facile à livrer et beaucoup plus facile à manquer. Quand HotPDF construit un tableau /Indexed depuis un bitmap Windows 8 bits, il appelle GetPaletteEntries, et cette API ne remplit que autant d'entrées que la palette en tient réellement et renvoie ce compte. Le code original testait le résultat pour = 0 puis itérait un fixe de 256 entrées sur un tableau de pile. Pour un bitmap de 16 couleurs, cela écrivait 240 entrées de mémoire de pile non initialisée directement dans la chaîne de lookup d'un PDF que vous tendez ensuite à un client, avec hival codé en dur à 255 pour que chaque lecteur traite docilement le résidu comme de vraies données de couleur. C'est un bug de correction et un bug de divulgation d'information portant les mêmes habits. La correction est sans glamour et complète : mettez le tableau à zéro avant l'appel, puis honorez le compte renvoyé pour la borne de boucle et pour hival. Si vous générez des images /Indexed quelque part dans votre propre code, allez vérifier ce hival maintenant
Deux pièces adjacentes complètent le tableau : les palettes Indexed et les fonctions échantillonnées Type 0 partagent le lecteur de flux décodé et la même discipline de clampage, discutée dans la pièce sur les tables de lookup de couleur des fonctions Type 0, et les règles de filtre qui gouvernent les flux de lookup sont les mêmes que celles qui gouvernent les données d'image dans extraire des images chargées à travers les filtres de décodage. Le résolveur d'espace de couleur décrit ici est livré comme partie du HotPDF Delphi Component pour Delphi et C++Builder, avec la référence complète de THPDFColorSpace sur la page produit