Exemples pratiques · Parcours des tutoriels

But et prérequis

Dessiner un petit tableau de cinq articles avec un en-tête sombre, des lignes alternées, une grille et deux polices. La mise en page demeure dans examples/table.php : le noyau reçoit des objets d'opérateurs PDF.

Lire les tutoriels pages et texte et graphiques. Installer les dépendances avec composer install, puis utiliser PHP 8.3 ou supérieur et les extensions demandées par Composer.

Exécuter et regarder

Depuis la racine d'un checkout du core :

php examples/table.php
php examples/table.php table-demo.pdf

Le premier appel produit examples/table.pdf. Le résultat tient sur une page, avec quatre colonnes et une ligne de total sous la grille. Les données sont fictives ; les valeurs monétaires sont des chaînes de démonstration déjà préparées.

Aperçu de la recette de tableau

Ce que construit le script

La page est large de 595 unités et haute de 842. La table commence à x = 54, y = 716, mesure 487 unités de largeur et comporte six lignes de 34 unités, en comptant l'en-tête. Le bas est donc 716 - 6 × 34 = 512. Avec les réglages de page de cet exemple, une unité vaut 1/72 de pouce ; les valeurs ressemblent à des points typographiques.

Étape Objets utilisés Pourquoi
Données et géométrie Tableaux PHP, largeurs et hauteur de ligne Calculer le dessin propre à cet exemple
Ressources Deux Type1Font, une entrée /Font Choisir Helvetica et Helvetica-Bold avec /F1 et /F2
Fonds SetNonstrokingRGBColor, AppendRectangle, FillPathNonzero Remplir l'en-tête et les lignes alternées
Grille SetStrokingRGBColor, SetLineWidth, MoveTo, LineTo, StrokePath Construire tous les segments, puis les tracer
Cellules BeginTextObject, SetTextFontAndSize, MoveTextPosition, ShowText, EndTextObject Placer explicitement chaque ligne de texte
Fichier ContentStreams, StreamObject, PageObject, DocumentCatalog, FileStructure Transformer les instructions en un document PDF

Les listes PHP aident à calculer les répétitions. Les instructions résultantes restent des objets : chaque nombre est un NumericType, chaque nom de ressource un NameType, chaque chaîne un LiteralString.

Couleur, chemins et texte

rg règle la couleur de remplissage, utilisée aussi pour les glyphes. L'en-tête reçoit d'abord un fond sombre ; son texte reçoit ensuite un remplissage blanc. RG règle la couleur des contours, ici celle de la grille. Changer la couleur ne dessine encore rien.

AppendRectangle correspond à re : ses paramètres sont x, y, largeur, hauteur. Après chaque rectangle de fond, FillPathNonzero (f) remplit puis termine le chemin. Les segments de grille peuvent partager un même chemin : chaque MoveTo (m) démarre un sous-chemin, chaque LineTo (l) ajoute un segment, et le StrokePath (S) final les trace.

Pour les cellules, y diminue lorsqu'on descend dans la table. Le décalage horizontal de 10 est la marge intérieure choisie. Le décalage vertical de 22 place la ligne de base du texte ; ce n'est pas une mesure de hauteur de glyphe. Chaque cellule ouvre son propre BT/ET, donc son Td repart de la matrice texte identité au lieu de cumuler les déplacements des cellules précédentes.

Correspondance avec ISO 32000-1:2008

Concept Référence
Repères et unités §8.3.2
Largeur des traits §8.4.3.2
Construction et tracé de chemins §8.5.2 et §8.5.3
Opérateurs de couleur §8.6.8
Polices Type 1 et polices standard §9.6.2
États et positionnement du texte §9.3 et §9.4

Le mot « tableau » désigne ici le résultat visuel. Un tableau sémantique et accessible implique une structure logique PDF supplémentaire (§14.7), que cette recette ne construit pas. La norme conservée dans le core et le guide d'architecture donnent le cadre de ces objets.

Adapter la recette

Ajouter une ligne dans $rows prolonge la grille et déplace la note finale. Adapter $columnWidths déplace les séparateurs et les origines des cellules. $rowHeight change à la fois les fonds, la grille et les lignes de base ; revoir le décalage de texte si la hauteur devient trop petite. Modifier les triples RGB, dont les composantes ici sont comprises entre 0 et 1, change la palette.

La recette suppose que toutes les valeurs tiennent sur une ligne et une page. Elle ne mesure pas les glyphes, ne calcule pas les montants, n'aligne pas les décimales, n'applique pas de césure et ne déclenche pas de saut de page. Une bibliothèque de mise en page pourra calculer ces décisions avant de produire les mêmes objets du noyau.

Retour aux exemples pratiques · Revoir les opérateurs graphiques

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 table : 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é : table.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\FileStructure;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operator;
use PDF\Syntax\ContentStreamsResources\Operators\AppendRectangle;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\FillPathNonzero;
use PDF\Syntax\ContentStreamsResources\Operators\LineTo;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTo;
use PDF\Syntax\ContentStreamsResources\Operators\SetLineWidth;
use PDF\Syntax\ContentStreamsResources\Operators\SetNonstrokingRGBColor;
use PDF\Syntax\ContentStreamsResources\Operators\SetStrokingRGBColor;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\ContentStreamsResources\Operators\StrokePath;
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\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
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. PDF draws paths and text. This recipe calculates its cells
// in the script, then constructs each operator explicitly.
// All visible values are ASCII for the standard fonts used.
$rows = [
    ['Crayon', '12', '0.80', '9.60'],
    ['Cahier', '8', '2.50', '20.00'],
    ['Regle', '6', '1.20', '7.20'],
    ['Stylo', '10', '1.70', '17.00'],
    ['Gomme', '5', '0.90', '4.50'],
];
$headers = ['Article', 'Quantite', 'Prix EUR', 'Total EUR'];
$columnWidths = [210, 70, 100, 107];
$left = 54;
$top = 716;
$rowHeight = 34;
$tableWidth = array_sum($columnWidths);
$bottom = $top - (count($rows) + 1) * $rowHeight;

// 2. F1 and F2 are distinct resources. A Tf operator selects one
// or the other when drawing data or headers.
$regular = new Type1Font(StandardFont::Helvetica);
$bold = new Type1Font(StandardFont::HelveticaBold);
$resources = new ResourceDictionary(new Font(new DictionaryType(
    new Entry(new NameType('F1'), new IndirectReference($regular)),
    new Entry(new NameType('F2'), new IndirectReference($bold)),
)));

/** @var list<Operator> $operators */
$operators = [
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F2'), new NumericType(24)),
    new MoveTextPosition(new NumericType($left), new NumericType(780)),
    new ShowText(new LiteralString('Un tableau compose objet par objet')),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
    new MoveTextPosition(new NumericType(0), new NumericType(-30)),
    new ShowText(new LiteralString('Colonnes fixes, fonds alternes, grille et positions explicites.')),
    new EndTextObject(),
];

// 3. rg sets fill color. re constructs a rectangle; f fills it
// and ends the path. Finish this path before any text.
$operators[] = new SetNonstrokingRGBColor(new NumericType(0.12), new NumericType(0.23), new NumericType(0.38));
$operators[] = new AppendRectangle(new NumericType($left), new NumericType($top - $rowHeight), new NumericType($tableWidth), new NumericType($rowHeight));
$operators[] = new FillPathNonzero();
foreach ($rows as $index => $row) {
    if ($index % 2 === 0) {
        $operators[] = new SetNonstrokingRGBColor(new NumericType(0.92), new NumericType(0.95), new NumericType(0.98));
        $operators[] = new AppendRectangle(new NumericType($left), new NumericType($top - ($index + 2) * $rowHeight), new NumericType($tableWidth), new NumericType($rowHeight));
        $operators[] = new FillPathNonzero();
    }
}

// 4. RG and w set outlines. Each m/l pair adds a grid subpath;
// S strokes the whole path. y decreases while moving downwards.
$operators[] = new SetStrokingRGBColor(new NumericType(0.55), new NumericType(0.62), new NumericType(0.70));
$operators[] = new SetLineWidth(new NumericType(0.5));
$columnX = $left;
foreach ([0, ...$columnWidths] as $width) {
    $columnX += $width;
    $operators[] = new MoveTo(new NumericType($columnX), new NumericType($top));
    $operators[] = new LineTo(new NumericType($columnX), new NumericType($bottom));
}
for ($line = 0; $line <= count($rows) + 1; ++$line) {
    $lineY = $top - $line * $rowHeight;
    $operators[] = new MoveTo(new NumericType($left), new NumericType($lineY));
    $operators[] = new LineTo(new NumericType($left + $tableWidth), new NumericType($lineY));
}
$operators[] = new StrokePath();

// 5. Each cell opens its BT/ET text object: Td therefore starts from an
// identity matrix. The 10-point offset is our inner margin, and
// y identifies the letter baseline, not the top of the cell.
foreach ([$headers, ...$rows] as $rowIndex => $row) {
    $header = $rowIndex === 0;
    $component = $header ? 1 : 0.15;
    $operators[] = new SetNonstrokingRGBColor(new NumericType($component), new NumericType($component), new NumericType($component));
    $cellX = $left;
    foreach ($row as $columnIndex => $value) {
        $operators[] = new BeginTextObject();
        $operators[] = new SetTextFontAndSize(new NameType($header ? 'F2' : 'F1'), new NumericType(11));
        $operators[] = new MoveTextPosition(new NumericType($cellX + 10), new NumericType($top - $rowIndex * $rowHeight - 22));
        $operators[] = new ShowText(new LiteralString($value));
        $operators[] = new EndTextObject();
        $cellX += $columnWidths[$columnIndex];
    }
}
$operators[] = new BeginTextObject();
$operators[] = new SetTextFontAndSize(new NameType('F1'), new NumericType(11));
$operators[] = new MoveTextPosition(new NumericType($left), new NumericType($bottom - 30));
$operators[] = new ShowText(new LiteralString('Total : 58.30 EUR  |  Donnees fictives, une ligne par cellule.'));
$operators[] = new EndTextObject();

// 6. ContentStreams contains instructions; StreamObject contains bytes
// written in /Contents. The page and catalog assemble the graph.
$contents = new StreamObject(new ContentStreams(...$operators));
$page = new PageObject(
    new Rectangle(new NumericType(0), new NumericType(0), new NumericType(595), new NumericType(842)),
    $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__ . '/table.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;

Source de ce guide au commit documenté ↗