Cet exemple crée un rectangle qui passe du bleu turquoise à l'orange. Le PDF contient une fonction type 2, un shading axial type 2 et un chemin de clipping. Le lecteur calcule les couleurs du dégradé à partir de ces objets.

Aperçu du dégradé axial turquoise et orange

Exécuter l'exemple

Depuis la racine d'un checkout du core, après composer install :

php examples/gradient.php gradient.pdf

Le chemin de sortie est facultatif ; sa valeur par défaut est examples/gradient.pdf. Voir le script complet examples/gradient.php.

1. Définir la couleur comme une fonction

ExponentialInterpolationFunction représente la fonction PDF type 2. Elle reçoit un nombre t et renvoie ici trois nombres, les composantes RGB d'une couleur :

C(t) = C0 + t^N × (C1 − C0)

Domain contient l'intervalle d'entrée [0, 1]. C0 = [0.10, 0.65, 0.85] donne le turquoise au début ; C1 = [1.00, 0.72, 0.20] donne l'orange à la fin. L'exposant N vaut 1, ce qui produit une interpolation linéaire. À t = 0.5, la couleur est la moyenne des deux couleurs.

Range contient trois intervalles [0, 1], un par composante de sortie. Le même objet immuable NumberInterval est réutilisé pour ces intervalles. NumberArray rassemble les composantes en conservant chacune comme objet numérique.

Le type de la fonction décrit le calcul. Le type du shading, également numéroté 2 ici, décrit comment répartir les paramètres de ce calcul dans l'espace de la page.

2. Construire le shading axial

Le dictionnaire Type2ShadingDictionaryTable80 reçoit les entrées communes de la table 78 et les entrées propres au shading axial de la table 80 :

Entrée Valeur de l'exemple Rôle
/ShadingType 2 Choisit le shading axial.
/ColorSpace /DeviceRGB Les trois sorties de la fonction sont des composantes RGB.
/Coords [64 500 548 500] Relie le point de départ (64, 500) au point d'arrivée (548, 500).
/Domain [0 1] Associe ces points à t=0 et t=1.
/Function Référence indirecte à $function Calcule la couleur pour chaque paramètre.
/Extend [false false] Demande de ne pas étendre les couleurs au-delà des extrémités de l'axe.

L'axe est horizontal. Le paramètre t varie le long de cet axe ; la couleur reste constante perpendiculairement à lui. /Coords ne définit donc pas à lui seul un rectangle.

Les classes ShadingTypeEntry et ColorSpaceEntry portent la provenance de la table 78. CoordsEntry, DomainEntry, FunctionEntry et ExtendEntry proviennent de la table 80. Cette composition exprime les deux parties du dictionnaire décrit par la norme.

3. Nommer la ressource

Le dictionnaire /Shading des ressources de la page associe /Sh1 à une référence indirecte vers le dictionnaire de shading. PaintShading(new NameType('Sh1')) produit /Sh1 sh et retrouve cette ressource locale.

Le graphe contient deux objets indirects distincts : le shading et la fonction qu'il référence. Le writer attribue leurs numéros en parcourant ce graphe ; le script n'écrit aucun numéro d'objet à la main.

4. Limiter la surface peinte

Le flux de contenu encadre le dégradé par ces instructions :

q
64 400 484 200 re
W
n
/Sh1 sh
Q
  1. q sauvegarde l'état graphique, y compris le clipping courant.
  2. re crée le chemin d'un rectangle à (64, 400), de largeur 484 et de hauteur 200 points.
  3. W demande que ce chemin serve de clipping selon la règle nonzero.
  4. n termine le chemin sans le peindre et applique le clipping demandé.
  5. sh peint le shading dans cette zone.
  6. Q restaure l'état graphique antérieur.

Le clipping donne au dégradé sa forme rectangulaire. La restauration permet d'écrire les explications en dessous, hors du rectangle : elles seraient masquées si le clipping restait actif.

À essayer

  • Passer N de 1 à 2 : l'axe et les couleurs finales restent les mêmes, mais l'orange arrive plus progressivement.
  • Placer les extrémités de /Coords verticalement pour changer la direction de variation.
  • Modifier le rectangle de clipping tout en conservant /Coords pour distinguer la géométrie du paramétrage des couleurs.
  • Réduire la longueur de l'axe et comparer /Extend [false false] à [true true] dans le même rectangle.

Points à connaître

Le script traite un shading axial en /DeviceRGB, avec une fonction à une entrée et trois sorties. Il n'aborde pas les shadings radiaux, les maillages, la transparence ni les profils ICC.

Les classes d'entrées vérifient les types décrits par les tables. La construction de l'exemple respecte aussi les liens sémantiques : quatre coordonnées, trois sorties de fonction pour RGB, domaine compatible et deux booléens d'extension. Les classes générées ne garantissent pas à elles seules toutes ces relations pour un assemblage arbitraire.

Repères dans ISO 32000-1:2008 : 7.10.3 et table 40 (fonction type 2), 8.4.2 (état graphique), 8.5.4 (clipping), 8.7.4.3 et table 78 (shading et sh), 8.7.4.5.3 et table 80 (shading axial).

← 6. Images · Tous les tutoriels · 8. Flux d'objets →

Voir le résultat PDF

Ce fichier a été généré avec le code de cette documentation. Toutes ses pages sont reproduites ci-dessous ; ouvrir le PDF pour utiliser ses liens et ses signets.

Résultat PDF gradient : page 1 sur 1
Page 1 sur 1
Utiliser le lecteur PDF du navigateur

Ce lecteur dépend du navigateur. Si le document ne s’affiche pas, utiliser « Ouvrir le PDF » ou « Télécharger ».

Code source complet

Code de roukmoute/pdf · Consulter la source au commit documenté ↗

Lire le script commenté : gradient.php
<?php

declare(strict_types=1);

use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\Document\Resources\Shading;
use PDF\FileStructure;
use PDF\Functions\Domain;
use PDF\Functions\ExponentialInterpolationFunction;
use PDF\Functions\NumberArray;
use PDF\Functions\NumberInterval;
use PDF\Functions\Range;
use PDF\Specification\Dictionary\ShadingDictionaryTable78\ColorSpaceEntry;
use PDF\Specification\Dictionary\ShadingDictionaryTable78\ShadingTypeEntry;
use PDF\Specification\Dictionary\Type2ShadingDictionaryTable80;
use PDF\Specification\Dictionary\Type2ShadingDictionaryTable80\CoordsEntry;
use PDF\Specification\Dictionary\Type2ShadingDictionaryTable80\DomainEntry;
use PDF\Specification\Dictionary\Type2ShadingDictionaryTable80\ExtendEntry;
use PDF\Specification\Dictionary\Type2ShadingDictionaryTable80\FunctionEntry;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\AppendRectangle;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\ClipPathNonzero;
use PDF\Syntax\ContentStreamsResources\Operators\EndPath;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\PaintShading;
use PDF\Syntax\ContentStreamsResources\Operators\RestoreGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SaveGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SetNonstrokingRGBColor;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Object\ArrayType;
use PDF\Syntax\Object\BooleanType;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\LiteralString;

require dirname(__DIR__) . '/vendor/autoload.php';

// 1. The type 2 PDF function receives a parameter t in [0, 1] and returns
//    three RGB components. With N=1, C(t)=C0+t*(C1-C0) is linear.
//    Range explicitly defines three output intervals, one per component.
$unitInterval = new NumberInterval(new NumericType(0), new NumericType(1));
$domain = new Domain($unitInterval);
$function = new ExponentialInterpolationFunction(
    $domain,
    new NumericType(1),
    new NumberArray(new NumericType(0.10), new NumericType(0.65), new NumericType(0.85)),
    new NumberArray(new NumericType(1.00), new NumericType(0.72), new NumericType(0.20)),
    new Range($unitInterval, $unitInterval, $unitInterval),
);

// 2. /ShadingType 2 is an axial gradient. /Coords describes its endpoints
//    in page coordinates: [64 500 548 500]. On the left t=0,
//    on the right t=1; color remains constant perpendicular to the axis.
//    Common entries come from Table 78, specific entries from Table 80.
//    The function remains an indirect object distinct from the shading dictionary.
$shading = new Type2ShadingDictionaryTable80(
    new ShadingTypeEntry(new IntegerType(2)),
    new ColorSpaceEntry(new NameType('DeviceRGB')),
    new CoordsEntry(new NumberArray(
        new NumericType(64),
        new NumericType(500),
        new NumericType(548),
        new NumericType(500),
    )),
    new DomainEntry($domain),
    new FunctionEntry(new IndirectReference($function)),
    new ExtendEntry(new ArrayType(new BooleanType(false), new BooleanType(false))),
);

// 3. /Shading associates local name /Sh1 with this dictionary. The sh operator
//    will invoke this resource; no bitmap or colored bands are created.
$helvetica = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(
    new Font(new DictionaryType(
        new Entry(new NameType('F1'), new IndirectReference($helvetica)),
    )),
    new Shading(new DictionaryType(
        new Entry(new NameType('Sh1'), new IndirectReference($shading)),
    )),
);

// 4. Shading fills its area within the current clipping path. re creates
//    a rectangle; W requests using it as the clipping area; n ends the
//    path without painting and applies clipping. q/Q limit its effect
//    to the gradient: subsequent text returns to the page clipping path.
$contents = new StreamObject(new ContentStreams(
    new SetNonstrokingRGBColor(new NumericType(0.08), new NumericType(0.18), new NumericType(0.28)),
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(26)),
    new MoveTextPosition(new NumericType(64), new NumericType(714)),
    new ShowText(new LiteralString('Un degrade calcule par le lecteur PDF')),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
    new MoveTextPosition(new NumericType(0), new NumericType(-30)),
    new ShowText(new LiteralString('Fonction type 2 + shading axial type 2 + clipping rectangulaire')),
    new EndTextObject(),
    new SaveGraphicsState(),
    new AppendRectangle(new NumericType(64), new NumericType(400), new NumericType(484), new NumericType(200)),
    new ClipPathNonzero(),
    new EndPath(),
    new PaintShading(new NameType('Sh1')),
    new RestoreGraphicsState(),
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(14)),
    new MoveTextPosition(new NumericType(64), new NumericType(374)),
    new ShowText(new LiteralString('t = 0  |  C0 = [0.10 0.65 0.85]')),
    new MoveTextPosition(new NumericType(250), new NumericType(0)),
    new ShowText(new LiteralString('t = 1  |  C1 = [1.00 0.72 0.20]')),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
    new MoveTextPosition(new NumericType(-250), new NumericType(-58)),
    new ShowText(new LiteralString('C(t) = C0 + t * (C1 - C0), car N = 1.')),
    new MoveTextPosition(new NumericType(0), new NumericType(-24)),
    new ShowText(new LiteralString('Coords fixe un axe ; le rectangle de clipping fixe la surface visible.')),
    new MoveTextPosition(new NumericType(0), new NumericType(-24)),
    new ShowText(new LiteralString('Ce texte reste visible hors du rectangle grace a la restauration Q.')),
    new EndTextObject(),
));

// 5. The same document graph as in other tutorials produces the file.
//    Indirect references are numbered during serialization.
$page = new PageObject(
    new Rectangle(new NumericType(0), new NumericType(0), new NumericType(612), new NumericType(792)),
    $resources,
    $contents,
);
$pages = (new PageTreeNode())->addKid($page);
$catalog = new DocumentCatalog($pages);
$file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/gradient.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;

Source de ce guide au commit documenté ↗