Article technique

Dégradés PDF axiaux et radiaux en Delphi avec PDFium

PDFium Component crée des dégradés PDF axiaux et radiaux en Delphi via TPdf.SaveAsShadingCreated. Vous décrivez chaque dégradé dans un enregistrement TPdfShadingCreate (type, espace de couleur, coordonnées, couleurs ou arrêts, page cible), et la méthode écrit une copie vérifiée dans laquelle chaque remplissage est un vrai dictionnaire de shading ISO 32000-1, enregistré dans le dictionnaire /Resources /Shading de la page et peint par lopérateur sh, si bien que PDFium le rend nativement

Le besoin apparaît dans les générateurs de rapports et de certificats. Un graphiste livre une maquette avec une bande den-tête bleu vers blanc toute douce ou une lueur radiale derrière un sceau, et lAPI dobjets de page qui couvre texte, tracés et images ny a rien pour ça. Vous pouvez singer un dégradé avec cent rectangles fins aux couleurs légèrement différentes, mais le résultat fait des bandes visiblement en zoom, gonfle le flux de contenu, et imprime mal. Le PDF a les shadings lisses depuis la 1.3 ; le travail est de les écrire pour quun moteur de rendu les dessine réellement

Pourquoi PDFium Component écrit-il les dégradés dans une sauvegarde séparée ?

TPdf.SaveAsShadingCreated est une opération de sauvegarde plutôt quun appel dobjets de page parce que lABI public de PDFium ne possède aucune fonction qui crée un dictionnaire de shading ou émet un opérateur sh. La couche dobjets de page employée quand on crée des PDF de zéro avec PDFium Component construit tracés, textes et images, et cest là quelle sarrête. Les shadings vivent un niveau plus bas, dans le graphe dobjets et la syntaxe de flux de contenu

La méthode travaille donc par étapes. PDFium sauvegarde dabord nativement le document ouvert, ce qui garde chaque édition que vous avez faite par lAPI normale. PDFium Component réécrit ensuite cette sortie mise en scène : il alloue un objet indirect par shading, ajoute lentrée de ressource à chaque page cible, ajoute un nouveau flux de contenu au /Contents de la page, et écrit une table de référence croisée neuve. Avant que quoi que ce soit atteigne votre flux ou fichier, le résultat est rechargé avec un contrôle strict de référence croisée, et la méthode renvoie False avec un message dans le rapport si ce rechargement échoue. La surcharge fichier écrit dabord dans un fichier temporaire, si bien quune sauvegarde en échec nécrase pas une cible existante

Construire un dégradé axial à deux couleurs

Un shading axial (ISO 32000-1 §8.7.4.5.3, Type 2) fond des couleurs le long dune ligne de x0 y0 à x1 y1, si bien que Coords prend quatre valeurs. Un shading radial (§8.7.4.5.4, Type 3) fond entre deux cercles et en prend six : x0 y0 r0 x1 y1 r1. Avec de simples StartColor et EndColor, PDFium Component émet une fonction exponentielle Type 2 (§7.10.3) avec /N 1, soit une interpolation linéaire franche, et règle /Extend [true true] pour que les couleurs dextrémité continuent au-delà des deux bouts de laxe

Géométrie des shadings axiaux et radiaux PDFium Component pour Delphi : un dégradé Type 2 fond StartColor vers EndColor le long de la ligne de x0 y0 à x1 y1 avec /Extend true true continuant au-delà des deux bouts, tandis quun dégradé Type 3 fond entre un cercle de départ de rayon r0 et un cercle darrivée de rayon r1
PDFium peint le shading sur ce que le clip permet : les Coords Type 2 placent laxe de fondu de x0 y0 à x1 y1, les Coords Type 3 placent les deux cercles, et /Extend [true true] transporte les deux couleurs dextrémité au-delà de la géométrie
uses
  System.SysUtils, PDFium, FPdfCompress;

procedure AddHeaderBand(const InFile, OutFile: string);
var
  Pdf: TPdf;
  Fills: TPdfShadingCreateShadings;
  Report: TPdfShadingCreateReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InFile;
    Pdf.Active := True;

    SetLength(Fills, 1);
    Fills[0] := Default(TPdfShadingCreate);
    Fills[0].ShadingType := pstAxial;
    Fills[0].ColorSpace := pscDeviceRGB;
    Fills[0].Coords := [0, 792, 0, 640];        // x0 y0 x1 y1, de haut en bas
    Fills[0].StartColor := [0.12, 0.35, 0.75];  // bleu foncé au bord supérieur
    Fills[0].EndColor := [1, 1, 1];             // blanc plus bas
    Fills[0].PageNumber := 1;                   // à base 1
    Fills[0].ResourceName := 'HeaderBand';      // optionnel, le défaut est Sh1, Sh2...

    if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
      raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
  finally
    Pdf.Free;
  end;
end;

Le nombre de composantes de chaque couleur doit correspondre à lespace de couleur : une pour pscDeviceGray et pscCalGray, trois pour pscDeviceRGB, pscCalRGB et pscLab, quatre pour pscDeviceCMYK. Pour pscICCBased le compte vient des octets de profil que vous passez dans IccProfile, qui doit exposer 1, 3 ou 4 composantes ; le profil est incorporé comme son propre flux ICC avec le /N correspondant. Les espaces calibrés acceptent un CalWhitePoint optionnel (trois valeurs, Y positif), un CalGamma (une valeur pour CalGray, trois pour CalRGB) et, pour Lab, un LabRange à six valeurs. Chaque coordonnée et composante est contrôlée pour NaN et linfini avant quun octet soit écrit

Notez que les coordonnées sont en espace utilisateur par défaut, avec lorigine en bas à gauche de la page. Rappelez-vous que laxe définit seulement où le fondu arrive : la zone peinte est ce que le clip permet, ce qui compte plus quil ny paraît, comme les sections suivantes le montrent

Les dégradés multi-arrêts se compilent en fonction stitching Type 3

PDFium Component transforme un tableau Stops en la fonction quun lecteur PDF attend : deux arrêts deviennent une fonction Type 2 unique, et 3 à 256 arrêts deviennent une fonction stitching Type 3 (ISO 32000-1 §7.10.4) faite dun segment Type 2 linéaire par paire adjacente. Chaque TPdfShadingCreateStop porte un Position et un Color. Quand Stops est non vide il remplace entièrement StartColor et EndColor

La fonction stitching a besoin de trois tableaux faciles à rater subtilement à la main. /Functions détient les segments, /Bounds ne détient que les positions darrêts intérieures (k segments exigent k-1 bornes), et /Encode remappe chaque sous-domaine vers [0 1] pour que chaque segment voie sa plage dentrée complète. Le composant dérive les trois depuis vos arrêts, voilà pourquoi il est strict sur lentrée : les positions doivent être finies, strictement croissantes, commencer à 0 et finir à 1 (à 1e-6 près). Une liste darrêts qui commencerait à 0.1 laisserait une partie du domaine sans fonction définie, et la méthode la rejette plutôt que de deviner

function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
  I: Integer;
begin
  Result.Position := Position;
  SetLength(Result.Color, Length(Rgb));
  for I := 0 to High(Rgb) do
    Result.Color[I] := Rgb[I];
end;

procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
  Fill := Default(TPdfShadingCreate);
  Fill.ShadingType := pstRadial;
  Fill.ColorSpace := pscDeviceRGB;
  // x0 y0 r0 x1 y1 r1 : un point au centre du sceau grandissant vers un cercle de 90 pt
  Fill.Coords := [450, 160, 0, 450, 160, 90];
  Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
                 Stop(0.4, [0.95, 0.75, 0.30]),
                 Stop(1.0, [1.00, 1.00, 1.00])];
  Fill.PageNumber := 1;
end;

Pour un dégradé à trois arrêts cela produit /Functions avec deux segments Type 2, /Bounds [0.4] et /Encode [0 1 0 1]. Les frontières de segments sont des jointures de couleurs dures, si bien quune liste darrêts ne lisse pas à travers elles ; si une jointure paraît trop franche, ajoutez des arrêts plutôt que dattendre du lecteur quil interpole la courbure

Compilation multi-arrêts de dégradé PDFium Component en Delphi : trois Stops aux positions 0.0, 0.4 et 1.0 deviennent une fonction stitching Type 3 avec un segment Type 2 par paire darrêts adjacents, /Bounds [0.4] pour la position intérieure et /Encode [0 1 0 1] remappant chaque sous-domaine vers la plage complète
Chaque paire darrêts adjacents devient un segment Type 2 dans une fonction Type 3 ; les tableaux /Bounds et /Encode dérivés sont ce que les fonctions stitching écrites à la main ratent dordinaire

Pourquoi PDFium ne dessine-t-il rien en silence ?

PDFium rend un shading nouvellement créé seulement quand deux détails sont bons : /ShadingType doit être un entier, et lappel sh doit se trouver à lintérieur dun chemin de clipping. Se tromper sur lun et le fichier souvre, se valide structurellement, et montre une page intacte. Aucune erreur, aucun avertissement, aucune ligne de journal

Piège un : /ShadingType est un nombre, pas un nom

ISO 32000-1 Table 78 définit /ShadingType comme un entier : 2 pour axial, 3 pour radial. La première version de ce rédacteur émettait /ShadingType /Axial parce que ça se lisait mieux dans un dump hexadécimal. PDFium lit la clé avec un getter entier, obtient 0 pour un nom, classe le shading comme invalide, échoue à le charger, et saute lopérateur sh sans plainte. La page se rendait vierge et rien dans PDFium ne disait pourquoi. La cause nest devenue visible quaprès avoir construit un PDF minimal écrit à la main et ouvert dans un second moteur de rendu indépendant, qui rapportait un type de shading inconnu de 0. La leçon se généralise à tout rédacteur PDF : quand la spécification type une énumération comme un entier, écrivez lentier, si tentante que paraisse une appellation auto-descriptive

Piège deux : un flux de contenu ne détenant que sh na pas de boîte englobante

Lopérateur sh (ISO 32000-1 §8.7.4.2) peint le shading sur la région de clipping courante, si bien que létendue propre de la page le borne sur papier. PDFium, cependant, construit un objet de page de shading dont le rectangle vient soit du chemin de clip actif, soit de la boîte englobante accumulée par les opérateurs de dessin antérieurs du même flux, et le moteur de rendu écarte les objets au rectangle vide. Un flux fraîchement ajouté qui ne contient rien dautre que /Sh1 sh na ni lun ni lautre, si bien que le dégradé est abandonné avant la rastérisation. TPdf.SaveAsShadingCreated lit donc la boîte de rognage de chaque page (avec repli sur la media box, puis sur la taille de page) et enveloppe chaque invocation dans un clip re W n explicite. Sur une page US Letter avec son origine à zéro, le flux ajouté ressemble à ceci :

q
0 0 612 792 re W n
/HeaderBand sh
Q

La paire q/Q isole le clip et toute transformation du contenu de page existant, si bien que rien de dessiné plus tard sur la page ne les hérite. Si vous écrivez vous-même des invocations de shading dans un autre outil, copiez cette forme : un clip explicite dans le même flux est bon marché et retire la dépendance à ce qui venait avant

Pourquoi un shading PDF dapparence valide ne dessine rien dans PDFium : un /ShadingType écrit comme le nom Axial est lu par un getter entier comme 0 et lopérateur sh est sauté en silence, et un flux de contenu ne détenant que sh na pas de boîte englobante jusquà ce quun clip re W n explicite lui donne un rectangle
Les deux échecs de shading invisible sont silencieux dans PDFium, si bien que SaveAsShadingCreated écrit lui-même le ShadingType entier et enveloppe chaque appel sh dans un clip re W n de boîte de rognage

Comment une matrice de dégradé personnalisée interagit-elle avec le clip ?

Une matrice de dégradé dans PDFium Component est appliquée comme un opérateur cm placé après le clip et avant sh, si bien que le clip reste pleine page tandis que seule la géométrie du dégradé est transformée. Réglez HasMatrix à True et remplissez MatrixA à MatrixF, les mêmes six nombres que toute matrice de transformation PDF. Les valeurs doivent être finies et le déterminant non nul, parce quune matrice singulière replierait le shading sur une ligne. Le composant enregistre aussi les six valeurs comme /Matrix dans le dictionnaire de shading, mais pour un appel sh direct cest le cm du flux de contenu qui fixe le système de coordonnées dans lequel PDFium peint

Lordre compte ici, et cest le troisième piège. Les opérandes de re sont interprétés dans la matrice de transformation courante. Mettez le cm dabord et le rectangle de clip à la taille de la page est mis à léchelle, tourné ou incliné avec le dégradé, si bien quune matrice qui aplatit une ellipse aplatit aussi la zone visible et des parties de la page perdent leur remplissage. Clipper dabord en espace utilisateur non transformé, puis concaténer la matrice, donne le résultat attendu : un dégradé transformé couvrant toute la page. Le guide des tampons Form XObject et leur matrice à six nombres passe en revue ce que fait chaque composante de matrice si vous devez en construire une

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // remplissage radial centré à 450,160
  Fill.Coords := [0, 0, 0, 0, 0, 90];  // cercles à lorigine
  Fill.HasMatrix := True;
  Fill.MatrixA := 1.6;       // étirement horizontal
  Fill.MatrixB := 0;
  Fill.MatrixC := 0;
  Fill.MatrixD := 0.8;       // compression verticale
  Fill.MatrixE := 450;       // puis déplacer vers le centre du sceau
  Fill.MatrixF := 160;
end;

Lire le rapport et connaître les limites

TPdfShadingCreateReport vous dit ce qui a été écrit : SourceByteCount et OutputByteCount, CreatedShadingObjectNumbers (un numéro dobjets par entrée, dans lordre), AttachedShadingCount, Succeeded et ErrorMessage. Les erreurs de validation nomment lentrée et le champ, par exemple une liste darrêts qui nest pas strictement croissante, si bien quelles valent dêtre journalisées verbatim

if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
  Writeln(Format('%d shading(s) attached, %d -> %d bytes',
    [Report.AttachedShadingCount, Report.SourceByteCount,
     Report.OutputByteCount]))
else
  Writeln('Rejected: ' + Report.ErrorMessage);

Le profil est délibérément borné. Il ne crée que des shadings axiaux et radiaux ; les shadings par fonction et par maillage (Types 1 et 4 à 7) et les espaces de couleur pattern sont rejetés. Un appel accepte 1 à 4096 remplissages, les numéros de pages doivent exister, et les noms de ressources sont limités à 127 caractères de lettres, chiffres, souligné, moins et point. Une entrée chiffrée est refusée à létape du trailer. Un document signé exige AllowSignedDocument := True, et comme la sortie est une réécriture complète plutôt quune mise à jour incrémentale, les signatures existantes ne se vérifieront plus, donc signez après avoir ajouté les dégradés. Chaque remplissage est peint à la fin du contenu de la page, au-dessus de ce qui y est déjà ; pour un effet de fond, créez le dégradé dabord et ajoutez textes et images ensuite, ou employez le contenu optionnel comme décrit dans larticle sur le contrôle des couches de contenu optionnel PDF en Delphi quand le remplissage doit être commutable

FAQ

SaveAsShadingCreated prend-il en charge les dégradés multi-arrêts ?

Oui. Passez 2 à 256 entrées dans Stops avec des positions strictement croissantes de 0 à 1. Deux arrêts produisent une fonction Type 2 et trois ou plus produisent une fonction stitching Type 3 avec les tableaux /Bounds et /Encode dérivés

Pourquoi mon dégradé est-il invisible dans PDFium alors que le fichier semble valide ?

Vérifiez que /ShadingType est lentier 2 ou 3 plutôt quun nom, et que lopérateur sh tourne à lintérieur dun clip re W n dans le même flux de contenu. PDFium saute en silence les types de shadings invalides et écarte les objets de shading au rectangle vide

Quels espaces de couleur un shading créé peut-il employer ?

TPdfShadingCreate accepte DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab et ICCBased avec un profil à 1, 3 ou 4 composantes. Les espaces de couleur Pattern et Indexed ne font pas partie du profil de création

La création de dégradés, le contenu optionnel et le reste des rédacteurs mis en scène sont livrés avec le PDFium Component for Delphi and C++Builder, qui enveloppe PDFium pour des projets Delphi, C++Builder et Lazarus