HotPDF, le composant PDF VCL natif pour Delphi et C++Builder, évalue les trois types de fonctions PDF construites à partir de formules plutôt que de grilles d'échantillons : l'interpolation exponentielle de Type 2, l'assemblage (stitching) de Type 3, et les fonctions calculateur PostScript de Type 4, correspondant à ISO 32000-1 §7.10.3, §7.10.4, et §7.10.5. Le Type 2 mélange deux vecteurs de sortie le long d'une courbe, le Type 3 enchaîne plusieurs sous-fonctions sur un même domaine d'entrée, et le Type 4 exécute un programme PostScript restreint capable de brancher, comparer et calculer presque tout ce qu'un flux de contenu peut demander à partir de ses entrées. Se tromper légèrement sur l'un des trois ne s'annonce jamais comme un bogue — cela se traduit par un dégradé avec une bande plate morte, une couleur d'accompagnement qui se rend en noir pur, ou une fonction calculateur décalée d'exactement un aux entrées qu'une suite de tests n'a justement pas essayées
Ces trois types se placent aux côtés d'un quatrième, le Type 0, qui stocke une grille échantillonnée plutôt qu'une formule et qui est couvert séparément dans l'article compagnon sur les tables de correspondance de couleurs Type 0. Les deux familles résolvent le même problème, faire correspondre une entrée à une sortie, mais le Type 0 est une donnée calculée une fois et intégrée au fichier, tandis que les Types 2, 3 et 4 sont du code que le lecteur évalue à chaque appel. Les quatre partagent un unique point de répartition dans le moteur de rendu de HotPDF, indexé par l'entrée /FunctionType du dictionnaire de fonction, si bien qu'un ombrage, une transformation de teinte, ou une fonction de point de trame n'a jamais besoin de savoir lequel des quatre il a reçu avant de pouvoir demander une couleur
Comment fonctionne une fonction exponentielle PDF Type 2 ?
Une fonction PDF Type 2 calcule une seule formule — y = C0 + x^N × (C1 − C0), appliquée composante par composante — où x est l'entrée unique de la fonction, normalisée selon son /Domain avant que la formule ne s'exécute (ISO 32000-1 §7.10.3). /C0 et /C1 sont les vecteurs de sortie aux deux extrémités de cette plage, un nombre par composante de sortie, et /N est l'exposant qui façonne la courbe entre elles : N = 1 donne la rampe linéaire droite derrière la plupart des arrêts de dégradé et des conversions en duplex, N supérieur à 1 tire la courbe vers C0, et N entre 0 et 1 la pousse vers C1. RegisterExponentialFunction construit ce dictionnaire à partir de cinq arguments et renvoie un objet de fonction prêt à être branché dans un ombrage, une fonction de point de trame, ou tout autre endroit où la spécification accepte une clé /Function
La relation de nombre de composantes entre C0 et C1 compte à deux reprises : une fois lorsque vous créez une fonction Type 2, et de nouveau chaque fois que HotPDF doit rendre une fonction qu'il n'a pas créée lui-même. Côté création, RegisterExponentialFunction vérifie C0 et C1 l'un par rapport à l'autre et lève une exception s'ils sont incohérents, si bien qu'un appel qui atteint BeginDoc correspond déjà à un objet de fonction cohérent avec lui-même. Côté rendu, en revanche, l'évaluateur doit faire confiance à quels que soient les tableaux /C0 et /C1 qu'un fichier source déclare réellement — un fichier d'imprimerie ouvert pour aperçu, disons, ou un document signé affiché à un utilisateur — et les versions antérieures à 2.376.0 lisaient ces tableaux dans un tampon dimensionné pour quatre composantes, le cas CMJN. Une teinte exponentielle DeviceGray ou DeviceRGB, avec un /C0 et /C1 à un ou trois éléments, échouait silencieusement à cette lecture et laissait les deux tableaux à zéro, si bien que la teinte se peignait en noir uni au lieu de sa couleur voulue. La version 2.376.0 a redimensionné le lecteur selon le nombre de sorties réellement déclaré par la fonction plutôt qu'un tampon fixe — exactement le genre de bogue que seul un cas de test non CMJN expose, puisque la suite existante tournait uniquement en CMJN, où quatre-dans-quatre correspondait toujours
var
EaseIn: THPDFDictionaryObject;
begin
// Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
EaseIn := Pdf.RegisterExponentialFunction(
[0,1], // Domain: single input, clamped to [0,1]
[0, 0, 0], // C0: output at x = 0
[0.8, 0, 0], // C1: output at x = 1
3, // N: exponent, 1 = linear, > 1 eases toward C0
[]); // Range omitted: defaults to a [0,1] clamp per output
end;
Assemblage Type 3 : enchaîner des sous-fonctions via un tableau Bounds
Une fonction PDF Type 3 assemble k sous-fonctions en un mappage par morceaux unique sur le /Domain d'une entrée unique, et les deux tableaux qui rendent cela possible sont /Bounds et /Encode (ISO 32000-1 §7.10.4). /Bounds contient k − 1 points de découpage intérieurs qui divisent /Domain en k intervalles consécutifs ; l'évaluateur choisit le premier intervalle dont la borne supérieure dépasse l'entrée, ou le dernier intervalle une fois que l'entrée atteint la borne finale, et transfère le contrôle à la sous-fonction de cet intervalle. /Encode remappe ensuite l'entrée depuis sa position à l'intérieur de cet intervalle vers la plage d'entrée que la sous-fonction choisie attend elle-même — typiquement [0, 1] si la sous-fonction est un segment exponentiel de plus — avant que l'évaluation ne se poursuive, un appel plus profond, dans le propre /Domain et /Range de cette sous-fonction
L'évaluateur d'assemblage de HotPDF ne gérait auparavant que exactement deux sous-fonctions, et son lecteur de /Bounds exigeait un tableau complet à huit éléments, si bien que le point de découpage unique dont un dégradé à deux segments a réellement besoin — un seul nombre dans /Bounds — échouait toujours à l'analyse et la fonction ne renvoyait rien. /Encode n'était pas appliqué du tout. La version 2.376.0 a réécrit la sélection comme la recherche générale à k sous-fonctions que la spécification décrit, et a commencé à lire /Bounds selon sa longueur réellement déclarée, si bien qu'un dégradé à trois, quatre, ou cinq arrêts assemblé à partir d'autant de segments exponentiels se résout désormais comme un dégradé à deux segments l'avait toujours prétendu faire. L'exemple ci-dessous construit une rampe à deux segments noir-vers-rouge-vers-blanc, la forme que un ombrage axial ou radial utilise chaque fois qu'une seule courbe exponentielle ne peut porter tous les arrêts de couleur qu'une conception exige
var
ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
// Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
ToRed := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0], [0.8, 0, 0], 1, []);
ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1], 1, []);
Ramp := Pdf.RegisterStitchingFunction(
[0,1], // Domain: the stitched function's own input range
[ToRed, ToWhite], // Functions: k = 2 sub-functions
[0.5], // Bounds: k - 1 = 1 split point
[0,1, 0,1], // Encode: 2 numbers per sub-function
[]); // Range omitted: inherited from each sub-function
end;
Que peut faire une fonction calculateur PostScript Type 4 que les Types 2 et 3 ne peuvent pas ?
Une fonction PDF Type 4 exécute un véritable programme, quoique délibérément restreint : un calculateur PostScript qui empile ses entrées sur une pile d'opérandes, exécute des opérateurs arithmétiques, de comparaison, de manipulation de pile et booléens, ainsi que des conditionnelles if/ifelse, et laisse ses sorties sur la pile lorsqu'il se termine (ISO 32000-1 §7.10.5, Tableau 42). Il n'y a aucune construction de boucle ni de stockage de variable nommée, seulement la pile, ce qui garde un programme conforme facile à raisonner — mais dans cet ensemble d'opérateurs restreint, le Type 4 peut exprimer des choses que les Types 2 et 3 ne peuvent pas, comme une véritable formule de mélange multi-encre pour une séparation DeviceN ou une fonction de point de trame de demi-teinte avec un seuil conditionnel. L'évaluateur de HotPDF, HPDFEvalPostScriptCalculator, tokenise le programme une fois — nombres, opérateurs, et blocs de procédure { } — puis parcourt une pile d'opérandes à 100 entrées, la profondeur exigée par ISO 32000-1 §7.10.5, derrière un plafond strict de 50 000 opérateurs évalués comme garde-fou défensif contre des programmes pathologiques ou écrits à la main
L'opérateur roll : la direction est facile à inverser par erreur
roll est l'opérateur le plus susceptible de sortir inversé lors d'une première tentative, car l'ordre de ses arguments et sa direction de rotation vont tous deux à l'encontre de la façon dont le français ou l'anglais les décrirait intuitivement. n j roll dépile un compte n et une quantité de rotation j, puis décale cycliquement les n entrées supérieures de la pile de j positions, en repliant sur l'autre extrémité les éléments qui sortent d'un côté ; l'exemple canonique, tiré directement de la spécification, est a b c 3 1 roll produisant c a b — l'élément du sommet se déplace vers le bas du groupe, et non l'inverse, et chaque autre élément se décale d'une position vers le haut pour lui faire de la place. L'évaluateur de HotPDF calcule la nouvelle position de l'entrée de pile i comme (i + j) mod n, ce qui correspond exactement à cet exemple, mais c'est une boucle de deux lignes tout aussi facile à écrire avec la rotation inversée, et un roll inversé produit tout de même une couleur d'apparence plausible — ce n'est simplement pas la couleur que l'auteur du fichier a demandée
const
Prog = '{ 3 1 roll }'; // (a b c) -> (c a b): the third input moves to the front
var
Reorder: THPDFStreamObject;
begin
// Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
Reorder := Pdf.RegisterPostScriptFunction(
[0,1, 0,1, 0,1], // Domain: 2 numbers per input
[0,1, 0,1, 0,1], // Range: 2 numbers per output (required for Type 4)
Prog);
end;
round n'est pas le Round de Delphi : arrondi au supérieur contre arrondi bancaire
L'opérateur round de PostScript résout systématiquement une égalité à ,5 vers l'entier supérieur, et la fonction Round intégrée de Delphi ne le fait pas : elle arrondit au pair le plus proche, la convention d'arrondi bancaire qui alterne le sens dans lequel bascule une égalité à ,5 afin que des arrondis répétés n'accumulent pas de biais. Les deux s'accordent presque partout et ne divergent précisément que sur la limite qui compte ici — le Round(0.5) de Delphi renvoie 0 et Round(2.5) renvoie 2, tandis que le round de la spécification PDF veut 1 et 3 pour ces mêmes entrées — si bien que la discordance se cache lors de tests superficiels puis se reproduit comme un décalage constant d'une unité partout où le calcul intermédiaire d'un programme calculateur tombe exactement sur un demi-entier. ISO 32000-1 §7.10.5 Tableau 42 est explicite : round pousse une fraction de ,5 vers l'entier le plus grand, si bien que HotPDF implémente l'opérateur comme Floor(x + 0.5) plutôt que d'appeler le Round de Delphi, et tout code qui réimplémente ou vérifie ponctuellement à la main l'arithmétique d'un programme Type 4 a besoin de la même substitution
function PostScriptRound(const X: Double): Double;
begin
// ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
// integer. Delphi's Round() is banker's rounding and disagrees here:
// Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
Result := Floor(X + 0.5);
end;
La validation à l'enregistrement détecte tôt un mauvais programme calculateur
Un programme Type 4 malformé est bon marché à détecter au moment de la création et coûteux à détecter partout ailleurs, si bien que RegisterPostScriptFunction ne se contente pas de stocker le texte source : elle évalue le programme à titre d'essai une fois, au point médian du /Domain déclaré, avant que l'objet de fonction ne soit jamais écrit dans le document. Des blocs { } non équilibrés, un opérateur non reconnu, un dépassement de pile par le bas, ou un nombre de sorties qui ne correspond pas à /Range échouent tous à cet essai et lèvent immédiatement une exception, avec la pile d'appels pointant vers l'appel RegisterPostScriptFunction plutôt que vers un artefact de rendu découvert pendant le contrôle qualité sur un fichier déjà livré. L'essai au point médian ne prouve pas que le programme est correct sur l'ensemble de son /Domain — une branche conditionnelle qui ne se comporte mal que près d'une extrémité de la plage d'entrée peut encore passer inaperçue à un unique point d'échantillonnage — mais il élimine toute la classe des programmes structurellement cassés plutôt que simplement erronés dans un coin
Où les dégradés et les couleurs d'accompagnement mettent ces fonctions au travail
Les Types 2, 3, et 4 apparaissent rarement isolément dans un PDF réel ; ils se manifestent partout où la spécification accepte une clé /Function, et les deux consommateurs les plus courants sont les ombrages et les transformations de teinte de couleurs d'accompagnement. L'opérateur sh d'un dégradé axial ou radial (ISO 32000-1 §8.7.4.5) évalue sa /Function une fois par position le long de l'axe du dégradé, ce qui est exactement le cas à arrêts multiples pour lequel l'assemblage Type 3 existe. La transformation de teinte d'un espace de couleur Separation ou DeviceN est l'autre foyer fréquent de ces trois types, et c'est là que le Type 4 justifie sa place : une seule encre d'accompagnement se réduit habituellement à une courbe Type 2 ou Type 0, mais un mélange DeviceN de plusieurs encres avec un véritable comportement de trapping et de surimpression a souvent besoin de la logique conditionnelle que seul un calculateur PostScript peut exprimer, le cas couvert dans l'article sur le rendu des couleurs d'accompagnement Separation et DeviceN. RegisterSeparationFunc est l'appel apparié côté création : il prend un nom de colorant, un espace de couleur alternatif, et n'importe quel objet renvoyé par la famille Register*Function, et branche cette transformation de teinte dans une ressource d'espace de couleur Separation que le reste de la page peut sélectionner avec scn/SCN
Ensemble, les grilles échantillonnées du Type 0 et ces trois types pilotés par formule couvrent chaque /Function qu'un PDF peut déclarer, et choisir le bon revient surtout à une question de ce que vous avez déjà : une table de correspondance calculée ailleurs devient un Type 0, un mélange à deux points d'extrémité devient un Type 2, plusieurs mélanges enchaînés sur un domaine deviennent un Type 3, et tout ce qui comporte une véritable logique conditionnelle devient un Type 4. RegisterExponentialFunction, RegisterStitchingFunction, et RegisterPostScriptFunction font partie du composant HotPDF standard pour Delphi et C++Builder, aux côtés du reste de son API de fonctions et d'ombrages ISO 32000-1