HotPDF effectue le rendu des tons directs Separation et DeviceN sur les pages PDF chargées en résolvant l'espace colorimétrique via HPDFResolveColorSpace, en évaluant la fonction de transformation de teinte avec HPDFEvalTintTransform et en convertissant le résultat via l'espace alternatif en RGB pour l'écran. Depuis la version 2.375.0, ce pipeline évalue les quatre types de fonctions PDF, y compris les calculateurs PostScript de Type 4, de sorte qu'un fichier prêt pour la presse contenant des encres Pantone affiche ses couleurs réelles au lieu d'un espace réservé. Cet article explique comment le pipeline fonctionne et, tout aussi utilement, comment il a échoué pendant sa construction
Le cas déclencheur est toujours le même. Un client reçoit un PDF d'une imprimerie : le titre est défini dans une encre de ton direct nommée, l'illustration de l'emballage utilise un mélange DeviceN à deux encres et le texte du corps est noir simple. Dans Acrobat, le rendu est parfait. Dans votre application Delphi, le titre s'affiche en noir, ou pire, ne s'affiche pas du tout, et le client signale un bug sur votre logiciel plutôt que sur le fichier. Le fichier est correct. C'est simplement le moteur de rendu qui ne comprend pas les espaces colorimétriques utilisés par le fichier
Pourquoi les tons directs s'affichent-ils en noir dans un lecteur PDF ?
Les tons directs s'affichent en noir, ou disparaissent, lorsque le moteur de rendu n'implémente que les opérateurs de couleur du périphérique (rg, g, k) et ignore les opérateurs génériques. La norme ISO 32000-1 §8.6 définit trois groupes d'espaces colorimétriques : les espaces du périphérique (DeviceGray, DeviceRGB, DeviceCMYK), les espaces basés sur le CIE (CalGray, CalRGB, Lab, ICCBased) et les espaces spéciaux (Indexed, Separation, DeviceN, Pattern). Tout ce qui est extérieur au groupe du périphérique est sélectionné avec les opérateurs génériques : cs et CS choisissent un espace par son nom dans le dictionnaire de ressources de la page, puis sc, SC, scn et SCN fournissent les valeurs des composants. Un moteur de rendu qui ignore ces opérateurs conserve la dernière couleur définie, ce qui, pour une page commençant par un titre en ton direct, correspond au noir DeviceGray initial
HotPDF a ajouté le jeu d'opérateurs génériques à son moteur de rendu de pages dans la version v2.333.0, avec un chemin de résolution unifié : chaque entrée de ressource /ColorSpace, qu'il s'agisse d'un simple nom, d'un tableau en ligne ou d'une référence indirecte, est analysée en un enregistrement THPDFColorSpace, et chaque demande de couleur de remplissage ou de contour est acheminée via un appel unique HPDFResolveColor. L'énumération des familles montre l'étendue de la prise en charge
type
THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
csfIndexed, csfCalGray, csfCalRGB, csfLab,
csfICCBased, csfSeparation, csfDeviceN,
csfUnsupported);
function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;
function HPDFResolveColor(const CS: THPDFColorSpace;
const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;
Une décision de conception s'avère payante à maintes reprises : csfUnsupported est une famille de premier ordre, pas une erreur. Un espace que le moteur de rendu ne peut pas interpréter se dégrade vers un repli défini au lieu d'interrompre l'affichage de la page, ce qui correspond au comportement des lecteurs grand public et évite qu'un seul remplissage exotique ne bloque le rendu d'un document
Comment une transformation de teinte convertit-elle une valeur d'encre en une couleur réelle ?
Un espace Separation contient trois informations : le nom de l'encre, un espace colorimétrique alternatif et une fonction de transformation de teinte. Le tableau [/Separation /PANTONE485 /DeviceCMYK f] indique : lorsque le flux de contenu écrit 0.8 scn, injectez la valeur de teinte 0.8 dans la fonction f et peignez le quadruple CMYK résultant. Le type DeviceN généralise cela à N encres avec une fonction à N entrées. Le nom de l'encre lui-même n'est qu'indicatif à l'écran ; la transformation de teinte constitue toute la sémantique de rendu. Ainsi, un moteur de rendu qui analyse l'espace mais ignore la fonction ne fait rien d'utile
HPDFEvalTintTransform est le moteur de fonction derrière cette étape. Introduit dans la version v2.334.0, il évalue les fonctions exponentielles de Type 2 (C0 + x^N * (C1 - C0) avec limitation du domaine et de la plage), les fonctions d'assemblage (stitching) de Type 3 (récursion de sous-fonction sélectionnée par bornes avec mappage de codage), et les fonctions échantillonnées de Type 0 avec des échantillons de 8, 16 et 32 bits. Le Type 0 correspond au mécanisme de table de correspondance que nous avons abordé côté création dans l'article sur la construction de tables de correspondance de couleurs de Type 0 ; le côté rendu parcourt la même structure dans le sens inverse, des octets d'échantillons décodés vers les valeurs des composants
Les fonctions de calculateur PostScript de Type 4 constituaient le dernier obstacle. Jusqu'à la version v2.375.0, elles se dégradaient vers un espace réservé neutre ; depuis la version v2.375.0, HPDFEvalPostScriptCalculator exécute l'ensemble d'opérateurs complet de la norme ISO 32000-1 §7.10.5 sur une pile d'opérandes limitée : opérations arithmétiques, de comparaison, booléennes et binaires, manipulations de pile y compris roll, et conditions if/ifelse. La sémantique des cas limites est plus stricte qu'il n'y paraît. L'opérateur PostScript round arrondit la moitié vers la valeur supérieure, de sorte que la fonction Round de Delphi (arrondi bancaire) ne peut pas être utilisée. Les opérateurs trigonométriques fonctionnent en degrés, atan renvoyant des valeurs dans [0, 360), et exp est une puissance à deux opérandes, et non l'exponentielle naturelle. Ce même évaluateur pilote également les remplissages en dégradé basés sur des fonctions, c'est pourquoi le rendu des dégradés axiaux et radiaux a bénéficié de rampes pilotées par calculateur dans la même version
// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
const Inputs: THPDFColorComps; InputCount: Integer;
out AltComps: THPDFColorComps): Boolean;
// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
const Inputs: THPDFColorComps; InputCount: Integer;
var Outputs: THPDFColorComps; OutCount: Integer): Boolean;
Honnêteté concernant la précision : un calculateur logiciel évalué en double précision ne correspondra pas à un RIP bit à bit, et la limitation aux limites de la plage (Range) peut différer d'un bit de poids faible par rapport à une autre implémentation. Pour l'affichage à l'écran et le rendu de régression, cela n'a pas d'importance ; si vous construisez un système d'épreuvage géré par la couleur, la transformation de teinte n'est que la première étape et vous aurez toujours besoin d'un véritable moteur CMM en aval
CalGray, CalRGB, Lab et ICCBased sans moteur ICC
Les familles basées sur le CIE prennent l'autre branche du même résolveur. HotPDF convertit les valeurs Lab via la chaîne standard Lab vers XYZ vers sRGB, en incluant le cube de point de rupture 6/29 dans la fonction de transfert inverse, et gère CalRGB avec son gamma par canal complété par une matrice linéaire 3x3 et CalGray avec son gamma unique. Le détail pertinent pour les performances concerne la gestion du point blanc : la matrice de conversion XYZ vers sRGB est adaptée selon la méthode de Bradford au point blanc déclaré dans l'espace colorimétrique et mise en cache dans l'enregistrement THPDFColorSpace résolu, de sorte que le travail par pixel reste une simple multiplication 3x3, quel que soit l'illuminant déclaré
Les espaces ICCBased bénéficient d'un traitement délibérément pragmatique. La spécification PDF exige que chaque flux ICCBased déclare un espace alternatif /Alternate ou un espace implicite via son nombre de composants /N, précisément pour que les moteurs sans gestion des couleurs puissent effectuer le rendu. HotPDF résout ICCBased via cette alternative, ou devine DeviceGray, DeviceRGB ou DeviceCMYK selon que /N vaut 1, 3 ou 4 lorsque l'entrée est absente, et n'analyse jamais les octets du profil. Cela élimine la dépendance à lcms et le coût de recherche de profil, au prix de l'exactitude colorimétrique : un espace ICCBased dont le profil s'écarte fortement de son alternative affichera le rendu de l'alternative. Pour l'affichage à l'écran et les vignettes, c'est le même compromis que font tous les lecteurs légers, et c'est la limite à énoncer clairement dans votre documentation
Le bug qui faisait échouer chaque recherche d'espace colorimétrique nommé
La plomberie d'opérateurs de la version v2.333.0 a été livrée avec un défaut resté invisible pendant quarante-deux versions : les gestionnaires cs et CS cherchaient leur opérande avec la barre oblique de tête (/CS0) dans des clés de dictionnaire de ressources stockées sans la barre oblique (CS0). Chaque recherche d'espace colorimétrique nommé échouait, cent pour cent du temps, et le code basculait silencieusement vers l'espace par défaut DeviceGray. Le symptôme visible était subtil de la pire des manières : un remplissage Separation de 1 scn devenait DeviceGray 1.0 (blanc), et de l'encre blanche sur une page blanche n'est pas une erreur de rendu que l'on capture d'écran. La correction, apportée dans la version v2.375.0, a été un utilitaire de normalisation de nom partagé appliqué à chaque recherche d'opérande vers ressource
Deux défauts associés ont été découverts lors de la même enquête. Premièrement, les références indirectes vers des objets de type tableau étaient renvoyées sans être résolues : l'accès aux documents du moteur de rendu comportait des résolveurs typés pour les flux et les dictionnaires uniquement. Ainsi, /CS0 5 0 R pointant vers un tableau autonome [/Separation ...] était renvoyé comme référence brute et l'espace était analysé comme non pris en charge. Deuxièmement, HPDFReadNumericArray imposait une sémantique de longueur stricte, exigeant que le tableau PDF soit au moins aussi long que le tampon fourni. La lecture de /C0 et /C1 d'une fonction de Type 2 dans un tampon de quatre éléments échouait donc pour les alternatives à un et trois composants, laissant les deux tableaux mis à zéro. Tout ton direct non-CMYK s'affichait donc en noir jusqu'à ce que la version v2.376.0 introduise le lecteur tolérant HPDFReadNumericArrayUpTo. La leçon à retenir : toute clé PDF documentée comme contenant un tableau numérique de longueur variable doit être lue avec un lecteur de remplissage de l'existant, car un tampon de taille fixe avec correspondance stricte transforme les fichiers valides en zéros silencieux
Comment tester le rendu des tons directs sans se tromper ?
La question délicate est de savoir pourquoi la suite de tests est restée verte malgré tout cela. Le test de régression original, SeparationRendersDistinguishable, affirmait uniquement que le bitmap rendu n'était pas entièrement noir. Un pipeline qui transformait chaque ton direct en DeviceGray produisait du gris et du blanc, ce qui n'est pas du noir, de sorte que l'assertion passait alors que la fonctionnalité était en panne. Des assertions faibles du type « non vide », « pas entièrement noir » ou « condensé non nul » ne peuvent pas distinguer un moteur de rendu fonctionnel d'un moteur en panne, car presque toutes les défaillances produisent tout de même quelques pixels
Le type d'assertion qui capture réellement ces défaillances verrouille la couleur attendue : effectuez le rendu d'un PDF minimal conçu à la main dont l'encre Separation correspond à une teinte connue, puis comptez les pixels dominants dans cette teinte. Le rendu vers un bitmap pour inspection utilise le point d'entrée RenderLoadedPageToBitmap décrit dans le guide de rendu de page vers bitmap
var
Pdf: THotPDF;
Bmp: TBitmap;
X, Y, RedHits: Integer;
Px: TColor;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
begin
Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
try
RedHits := 0;
for Y := 0 to Bmp.Height - 1 do
for X := 0 to Bmp.Width - 1 do
begin
Px := Bmp.Canvas.Pixels[X, Y];
if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
(GetBValue(Px) < 100) then
Inc(RedHits);
end;
// Lock the expected ink: demand a real area of red-dominant
// pixels, never settle for "not all black".
Assert(RedHits > 500);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
La ressource de test (fixture) importe autant que l'assertion. Un PDF conçu à la main de quelques centaines d'octets, avec un seul remplissage Separation et rien d'autre, ne laisse aucune ambiguïté sur la sortie attendue. Un fichier réel sollicite plus de code mais ne peut vous dire quelle étape a échoué. Nous traitons désormais le comptage des pixels de couleur attendue comme le seuil minimal pour tout test de fumée (smoke test) de rendu, car c'est le seul type d'assertion qui a forcé ces défauts à se révéler
Le rendu des tons directs et des espaces colorimétriques CIE fait partie du pipeline pour documents chargés dans le HotPDF Component pour Delphi et C++Builder, aux côtés du moteur de fonction, du rendu des dégradés et des chemins d'exportation de bitmaps présentés ci-dessus