HotPDF Component peint les dégradés COLRv1 repeat et reflect en shadings PDF vectoriels au lieu de rasteriser le glyphe. La géométrie est étirée pour couvrir un nombre entier de cycles de ligne de couleur, chaque cycle reçoit son propre shading axial ou radial avec Extend coupé, les couleurs sont interpolées en CalRGB lumière linéaire, et les modes de composition in/out de Porter-Duff sexpriment avec des soft masks de luminosité
Quand nous avons couvert comment HotPDF dessine les polices COLR v1 et emoji couleur SVG en Delphi, les modes dextension repeat et reflect figuraient sur la liste de ce que le chemin natif némittait pas, et de tels glyphes partaient en repli raster. Les versions récentes dHotPDF les dessinent en vecteurs. Y arriver a pris plusieurs faux départs, dont la plupart causés par une hypothèse sur le PDF dapparence raisonnable qui sest révélée fausse dans au moins une visionneuse très répandue. Ce billet passe en revue ces hypothèses, parce que quiconque mappe des dégradés CSS, SVG ou OpenType sur le PDF rencontrera les mêmes
Pourquoi une visionneuse PDF ne peut-elle pas répéter le dégradé pour vous ?
Un shading PDF na ni mode repeat ni mode reflect, donc le producteur doit mettre en page chaque cycle lui-même. ISO 32000-1 §8.7.4.5.3 et §8.7.4.5.4 définissent /Extend pour les shadings axiaux et radiaux, et tout ce quil fait est continuer la couleur à t0 ou t1 au-delà de la fin de laxe. Cest COLRv1 EXTEND_PAD, et rien dautre. OpenType COLR v1 définit aussi EXTEND_REPEAT, où la ligne de couleur repart à zéro chaque fois que le paramètre franchit un entier, et EXTEND_REFLECT, où un cycle sur deux tourne à lenvers
La première tentative dHotPDF a essayé de faire faire le travail périodique à la fonction. Elle enregistrait une fonction échantillonnée Type 0 (§7.10.2) avec un domaine large, environ [-12.73, 12.73] pour une envergure de 25 cycles, la remplissait déchantillons repliés, et activait Extend. À lintérieur du premier cycle, un fichier de test minimal se rendait correctement ; en dehors, MuPDF peignait une seule couleur moyennée et Poppler ne valait pas mieux. Les deux visionneuses évaluent la fonction le long du paramètre de shading et prolongent au-delà de laxe, et aucune ne retourne interroger une fonction périodique dans la région étendue. Le domaine large était aussi une mauvaise lecture de la norme : le paramètre de shading t est passé tel quel à la fonction, et daprès §7.10.1 une entrée hors du domaine de la fonction est rognée, si bien quune fonction dont le domaine ne correspond pas au /Domain du shading (défaut [0 1]) ne voit quune partie de ses échantillons lus
La conception qui marche a trois parties, toutes vérifiées contre des rendus MuPDF et Poppler. La géométrie du shading couvre physiquement un nombre entier N de cycles, si bien quaucun pixel ne dépend de Extend. La fonction échantillonnée garde le domaine par défaut [0 1], correspondant à ce que la visionneuse passe réellement. Et la séquence déchantillons elle-même est repliée par cycle, avec 256 intervalles par cycle, si bien que la fonction contient les données de couleur repeat ou reflect au lieu de compter sur une quelconque fonctionnalité de visionneuse
Replier une ligne de couleur en échantillons Type 0
Replier signifie échantillonner la ligne de couleur une fois par cycle et refléter la séquence sur les cycles impairs pour le mode reflect. Le helper ci-dessous est du Delphi simple, pas une API HotPDF, et il fait deux choses que la version naïve rate. Il interpole en lumière linéaire au lieu dinterpoler sur les octets sRGB stockés, et il résout les arrêts durs (deux arrêts au même décalage) vers la couleur du côté lointain, ce qui est la sémantique OpenType et CSS
uses
System.SysUtils, System.Math;
type
TColorStop = record
Offset: Single; // 0..1 dans un cycle, trié croissant
R, G, B: Byte; // octets sRGB style CPAL
end;
function SRGBToLinear(V: Byte): Double;
var
C: Double;
begin
C := V / 255;
if C <= 0.04045 then
Result := C / 12.92
else
Result := Power((C + 0.055) / 1.055, 2.4);
end;
function LinearToSRGB(L: Double): Byte;
var
C: Double;
begin
L := EnsureRange(L, 0, 1);
if L <= 0.0031308 then
C := L * 12.92
else
C := 1.055 * Power(L, 1 / 2.4) - 0.055;
Result := Round(C * 255);
end;
// Un cycle dune ligne de couleur en triplets RGB 8 bits. Reverse = True
// reflète le cycle, ce que fait le mode reflect sur les cycles impairs
function SampleCycle(const Stops: array of TColorStop; Count: Integer;
Reverse: Boolean): TBytes;
var
I, S: Integer;
T, F, Span: Double;
begin
SetLength(Result, Count * 3);
for I := 0 to Count - 1 do
begin
T := I / (Count - 1);
if Reverse then
T := 1 - T;
S := 0;
// >= choisit le côté aval dun arrêt dur
while (S < High(Stops) - 1) and (T >= Stops[S + 1].Offset) do
Inc(S);
Span := Stops[S + 1].Offset - Stops[S].Offset;
if Span > 0 then
F := EnsureRange((T - Stops[S].Offset) / Span, 0, 1)
else
F := 1;
Result[I * 3] := LinearToSRGB(SRGBToLinear(Stops[S].R) +
(SRGBToLinear(Stops[S + 1].R) - SRGBToLinear(Stops[S].R)) * F);
Result[I * 3 + 1] := LinearToSRGB(SRGBToLinear(Stops[S].G) +
(SRGBToLinear(Stops[S + 1].G) - SRGBToLinear(Stops[S].G)) * F);
Result[I * 3 + 2] := LinearToSRGB(SRGBToLinear(Stops[S].B) +
(SRGBToLinear(Stops[S + 1].B) - SRGBToLinear(Stops[S].B)) * F);
end;
end;
Le moteur de rendu COLRv1 dans HotPDF va plus loin que ce croquis. Il écrit des échantillons linéaires 16 bits big-endian dans une fonction CalRGB et stocke lalpha comme un échantillon gris 16 bits séparé, tandis que les helpers publics montrés ensuite prennent des triplets DeviceRGB 8 bits, ce pourquoi le croquis reconvertit en octets sRGB. Pour la théorie de ce quun lecteur fait de ces échantillons, larticle sur les fonctions échantillonnées Type 0 comme LUT de couleur 3D couvre le modèle dinterpolation en détail
Pourquoi chaque cycle a-t-il besoin de son propre shading ?
Chaque cycle a besoin de son propre shading parce que les lecteurs névaluent pas exactement une fonction échantillonnée ; ils précalculent une table de couleurs, et la table interpole à travers la boucle de la dernière couleur dun cycle à la première couleur du suivant. MuPDF stocke la fonction unidimensionnelle dun shading comme une table de 256 entrées. Entassez 11 cycles dans une seule fonction de 2816 échantillons et chaque échantillon sauvegardé peut être exact tandis que MuPDF montre encore 12 à 14 niveaux derreur de canal, parce que les 256 entrées de table sont étalées sur toute lenvergure et la couture dure du repeat se fait lisser en rampe. Poppler et linterpolation analytique du même fichier étaient tous deux corrects, donc cest une limite de précision de lecteur, pas une erreur de fichier, et ajouter des échantillons naide pas
La correction est de garder chaque table de lecteur à lintérieur dun cycle. HotPDF émet les dégradés linéaires périodiques comme un shading axial indépendant par cycle dorigine, avec les deux drapeaux Extend à faux, si bien que le saut de couleur fin-début tombe sur la frontière entre deux shadings au lieu de tomber dans une table. Pour reflect, le test dimpairité emploie le numéro de cycle signé dorigine, cycles négatifs avant le début de la ligne de couleur compris, et la direction du shading est échangée plutôt que les échantillons recalculés. Tous les cycles sont dessinés dans une surface isolée en lumière linéaire et lalpha de peinture est appliqué une fois à la fin, si bien quun dégradé translucide ne double pas son opacité le long des coutures
Les méthodes publiques THotPDF.RegisterSampledAxialGradient et THotPDF.RegisterSampledRadialGradient exposent la brique de shading échantillonné pour votre propre dessin. Toutes deux prennent (..., DomainT0, DomainT1: Single; const Samples: TBytes; SampleCount, Cycles: Integer) après la géométrie, exigent PDF 1.3, lèvent une exception sauf si Samples contient exactement SampleCount * 3 octets, et renvoient un nom de pattern pour THPDFPage.SetFillPattern. Elles posent Extend aux deux bouts et enregistrent Cycles comme une clé de pattern personnalisée /Cycles que les visionneuses ignorent. Pour suivre la règle dun cycle par shading avec elles, enregistrez un pattern par cycle et laissez un rectangle de bande le clipper
// Dégradé repeat ou reflect horizontal fait dun shading par cycle.
// Les coordonnées du pattern sont en espace PDF par défaut ; un axe
// horizontal garde cet exemple indépendant de la convention Y haut-bas de la page
procedure PaintPeriodicBands(Pdf: THotPDF; const Stops: array of TColorStop;
X, Y, Width, Height, CycleLength: Single; Reflect: Boolean);
const
SamplesPerCycle = 257; // 256 intervalles par cycle
var
Cycle, CycleCount: Integer;
X0, X1: Single;
Samples: TBytes;
PatternName: AnsiString;
begin
CycleCount := Ceil(Width / CycleLength);
for Cycle := 0 to CycleCount - 1 do
begin
X0 := X + Cycle * CycleLength;
X1 := X0 + CycleLength;
Samples := SampleCycle(Stops, SamplesPerCycle, Reflect and Odd(Cycle));
// Domaine [0, 1] : le paramètre de shading atteint la fonction inchangé
PatternName := Pdf.RegisterSampledAxialGradient(X0, 0, X1, 0, 0, 1,
Samples, SamplesPerCycle, 1);
if PatternName = '' then
Exit; // version de document verrouillée sous PDF 1.3
Pdf.CurrentPage.SetFillPattern(PatternName);
// La bande clippe le shading à exactement un cycle, donc Extend napparaît jamais
Pdf.CurrentPage.Rectangle(X0, Y, Math.Min(X1, X + Width) - X0, Height);
Pdf.CurrentPage.Fill;
end;
end;
Les envergures très longues demandent une autre astuce, parce quun shading par cycle signifie une ressource par cycle. Pour les dégradés linéaires COLRv1 périodiques, HotPDF passe à un schéma de doublement : une forme feuille détient une primitive repeat à un cycle ou reflect à deux cycles, et chaque niveau de Form XObject dessine le niveau précédent deux fois, décalé de sa largeur. Le nombre de formes croît logarithmiquement avec lenvergure, tandis que la visionneuse peint toujours chaque cycle visible, si bien que la couverture après alignement de phase est plafonnée à 65536 unités de cycle. Une version antérieure qui plaçait la primitive dans un pattern de tuilage Type 1 a été abandonnée, parce que les lecteurs ajustent lespacement des tuiles à la grille du périphérique et que Poppler dérivait en phase sur un cycle qui nétait pas un nombre entier de pixels
Résoudre la plage de paramètres pour les familles de cercles radiaux
Un dégradé radial ne couvre le glyphe que si la plage de cycles est calculée pour la bonne famille de cercles, et les peintures radiales COLRv1 tombent en trois familles aux mathématiques différentes. Avec D = c1 - c0 et dr = r1 - r0, une famille strictement imbriquée a |dr| > |D|, un cône non imbriqué a |D|² > dr², et une famille tangente a |D| = |dr|. HotPDF mesure le paramètre depuis le point de rayon nul de la famille dorigine et garde le centre et le rayon de chaque cercle émis liés au même paramètre dorigine, si bien que les rayons négatifs remappés ne sont jamais bornés indépendamment
- Les familles imbriquées intersectent la boîte de remplissage de façon monotone, si bien que la plage de paramètres vient des quatre coins plus une bissection pour le premier cercle qui touche la boîte. Jusquà 64 cycles sont échantillonnés dans un shading à 256 échantillons par cycle, plafonné à 4096 échantillons ; les envergures plus profondes passent aux tranches par cycle
- Les cônes et les bandes à rayon égal emploient de courts shadings radiaux séparés aux frontières de cycles dorigine, chacun avec 257 échantillons et
Extendcoupé, composés dans lordre des paramètres dorigine à lintérieur dun groupe isolé non knockout, si bien que le plus grand paramètre valide lemporte là où les cercles se chevauchent - Les familles tangentes réduisent léquation du cercle à une équation linéaire. Relativement au point de tangence partagé, la racine est
q = |W|² / (2·(W·V)), et la région couverte est le demi-plan ouvert devant ce point
Le cas tangent a une vraie singularité. Quand la ligne tangente traverse la boîte de remplissage, le nombre de cycles nécessaires pour atteindre les pixels près de cette ligne est non borné, si bien quaucun nombre fini de shadings ne la couvre. HotPDF dépense un budget de 64 cycles depuis le point de tangence et laisse la mince bande restante transparente. Quand les quatre coins de la boîte se trouvent derrière le point de tangence, la peinture est transparente dans son ensemble et le reste du glyphe se dessine quand même. Le chemin de cône fini accepte jusquà 4096 cycles requis, avec une couverture plafonnée à 1024 tranches et tout ce qui dépasse le plafond laissé non peint ; un glyphe à 1024 tranches arrive à environ 1,8 Mo
Couleur en lumière linéaire et le piège des deux décimales
COLRv1 interpole et compose en lumière linéaire, si bien que HotPDF décode chaque octet CPAL avec la fonction de transfert inverse sRGB et peint aplats, shadings et groupes internes dans un espace CalRGB unique avec blanc D65, primaires sRGB et /Gamma [1 1 1] (§8.6.5.3). Seule la surface racine finie se convertit vers lespace de fusion de lappelant. Interpoler les octets sRGB stockés produit à la place des points milieux visiblement plus sombres, lerreur gamma classique que fait tout moteur de dégradés une fois
Le passage à la lumière linéaire a exposé un vieux bug de formateur. Une valeur de canal CPAL sombre de 12 devient environ 0.0036765 en lumière linéaire, et le formateur de nombres partagé derrière scn ne gardait que deux décimales, si bien que cette composante était écrite 0 avant que la moindre visionneuse touche le fichier. Le symptôme ressemblait exactement à une quantification de lecteur, et le premier réflexe fut daccuser les tampons de groupe 8 bits de MuPDF. Les composantes de remplissage et de trait passent désormais par un formateur décimal sans exposant qui garde les petites valeurs et ignore la locale du processus. La leçon se généralise : quand une couleur sombre disparaît, videz le flux de contenu et vérifiez lopérande écrit avant denquêter sur la visionneuse
La visionneuse compte toujours après cette correction. MuPDF 1.28.2 perd les canaux linéaires proches de zéro quand il compose un groupe de transparence isolé qui porte un espace de couleur de fusion explicite, et Poppler 26.07.0 Splash ignore carrément les espaces de fusion de groupe. La propriété opt-in THotPDF.COLRv1DeviceSpaceGroupBlend (défaut False) émet les groupes COLRv1 sans lespace de couleur de fusion, si bien quils se composent dans lespace périphérique hérité tandis que les couleurs de contenu, shadings et fonctions restent en CalRGB lumière linéaire. Le compromis est que les couches translucides qui se chevauchent fusionnent alors en espace périphérique dans les visionneuses qui honorent les espaces de couleur de groupe
Quels modes de composition COLRv1 le PDF peut-il exprimer exactement ?
HotPDF rend les modes de composition COLRv1 5 à 8 (SRC_IN, DEST_IN, SRC_OUT, DEST_OUT) nativement, et les modes 9 à 12 (SRC_ATOP, DEST_ATOP, XOR, PLUS) partent vers OnColorGlyphRasterize. Les modes in et out sont des produits dune surface par lalpha de lautre, ce qui est exactement ce que calcule un soft mask (§11.6.5). Lopérande survivante est capturée comme une forme ordinaire ; lopérande masquée est capturée lors dun passage alpha comme contenu plat à lintérieur dun groupe DeviceGray isolé et sert de masque /Luminosity avec /BC [0]. Les modes out écrivent 1 − α pour chaque alpha et placent un couvert blanc opaque sous le contenu du masque, si bien que la survivante reste visible là où lautre opérande na jamais peint
Les quatre autres exigent une addition prémultipliée de deux surfaces. Dans le modèle source-over du PDF, le terme de fond est toujours atténué par le (1 − α) de la source, et aucune primitive nadditionne deux surfaces ni ne met à léchelle le fond par la seule couverture de la source, si bien que atop, xor et plus ne peuvent pas sécrire en vecteurs. Une restriction de plus venait des PDF de test nus : les deux visionneuses calculent mal les groupes de soft mask dont le contenu imbrique un Do de forme, si bien que lopérande masquée doit être plate. Les couleurs unies, les dégradés pad et un wrapper PaintGlyph autour deux qualifient ; les couches, transformations et dégradés périodiques, qui produisent tous des formes internes, envoient le glyphe en repli
var
Pdf: THotPDF;
Fallback: TEmojiFallback; // classe gestionnaire avec la signature dévénement ci-dessous
begin
Pdf := THotPDF.Create(nil);
Fallback := TEmojiFallback.Create;
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'periodic-icons.pdf';
// Opt-in : composition des groupes en espace périphérique pour MuPDF et Poppler
Pdf.COLRv1DeviceSpaceGroupBlend := True;
// Reçoit les glyphes atop/xor/plus et tout le reste hors du sous-ensemble :
// (Sender; Format; const FontBytes, AssetData: TBytes; GlyphID: Word;
// PaletteIndex, PixelSize: Integer; out Width, Height: Integer;
// out RGBA: TBytes; out Handled: Boolean)
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('Fonts\GradientIcons.ttf');
if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $E001,
72, 144, 'Gradient Icons', 48) then
Writeln('U+E001 was neither drawn natively nor rasterized');
Pdf.EndDoc;
finally
Fallback.Free;
Pdf.Free;
end;
end;
Où le chemin vectoriel reste encore en deçà
La sortie vectorielle nest pas identique à des pixels identiques dans chaque visionneuse, et les enregistrements dacceptation HotPDF le disent franchement. Les échantillons à un ou deux pas dune frontière de cycle sont lissés par linterpolation du lecteur, si bien que les contrôles de pixels échantillonnent loin des coutures et que les flux de fonctions sont vérifiés en les décodant à la place. Interpoler des échantillons RGB droits et alpha stockés séparément est une approximation près des extrémités à faible alpha et des arrêts durs ; lerreur mesurée est du second ordre à 256 échantillons par cycle, mais ce nest pas exact. Poppler Splash échantillonne au bord gauche dun pixel plutôt quà son centre, ce qui se voit comme de petites différences sur les rampes les plus raides
Les budgets sont de vraies limites aussi. Les familles radiales imbriquées déclinent au-delà denvergures de 4096 cycles, les cônes et familles tangentes au-delà de 4096 cycles requis, et la bande tangente près de la ligne singulière reste vide. Un alpha variable des arrêts exige la transparence PDF 1.4, donc ce nest pas activé sous PDF/A-1, PDF/X-1a ou PDF/X-3. Si un glyphe tombe hors de ces limites, DrawRegisteredColorGlyph décline le graphe entier avant décrire un opérateur, ce qui est le même comportement tout-ou-rien quauparavant
FAQ
HotPDF rasterise-t-il les dégradés COLRv1 repeat et reflect ?
Non. Les versions récentes dHotPDF les émettent comme des shadings axiaux et radiaux vectoriels couvrant des cycles entiers. Seuls les graphes hors du sous-ensemble pris en charge, comme les compositions atop, xor et plus, partent vers OnColorGlyphRasterize
Pourquoi ne pas employer une seule fonction échantillonnée pour tous les cycles ?
Les lecteurs précalculent une table de couleurs depuis la fonction, et la table de MuPDF a 256 entrées. Étalée sur de nombreux cycles, elle interpole à travers chaque couture de repeat. Un shading par cycle garde chaque table à lintérieur dun cycle unique
Quand activer COLRv1DeviceSpaceGroupBlend ?
Activez-le quand des canaux quasi noirs des glyphes COLRv1 tombent à zéro dans MuPDF. Laissez-le coupé quand des couches translucides qui se chevauchent doivent fusionner en lumière linéaire dans des visionneuses qui honorent les espaces de couleur de groupe
Dégradés COLRv1 natifs, emoji couleur, shadings échantillonnés et groupes de transparence font tous partie de HotPDF Component for Delphi and C++Builder, qui génère des documents PDF directement depuis du code Delphi et C++Builder