Index des tutoriels · Précédent : premier document · Suivant : tracés et couleurs

Cette leçon construit trois pages A4 qui partagent leurs ressources. Elle montre comment placer du texte, choisir un interligne et composer un pied de page. Le script complet est pages-and-text.php.

Exécuter l'exemple

Utiliser PHP 8.3 ou supérieur et les dépendances installées par composer install, comme dans le premier tutoriel. Depuis la racine d'un checkout du core :

php examples/pages-and-text.php pages-and-text.pdf

Le premier argument désigne le fichier de sortie. Sans argument, le script écrit dans examples/pages-and-text.pdf. Son répertoire parent doit déjà exister.

Ce que l'on obtient

Trois pages A4, chacune avec un titre, un sous-titre, plusieurs lignes de texte et un numéro de page. Le corps utilise une taille de 13 unités et un interligne de 22 unités. Les chaînes affichées sont volontairement en ASCII ; le titre du document dans les métadonnées contient des accents.

Première page du tutoriel pages et texte

1. Partager les ressources et le papier

Les deux instances de Type1Font représentent Helvetica et Helvetica-Bold. Le dictionnaire Font des ressources associe ces objets aux noms /F1 et /F2. Un SetTextFontAndSize vise ce nom local : /F1 n'est pas le nom commercial de la police.

Le PageTreeNode reçoit les entrées /MediaBox et /Resources. Les trois PageObject fournissent uniquement leur flux /Contents. Lorsqu'une page n'a pas de valeur locale pour une entrée héritable, le lecteur PDF remonte l'arbre /Pages : il retrouve ici le même rectangle et les mêmes ressources.

$page = new PageObject(contents: new StreamObject($contents));
$pages->addKid($page);

Les noms /Kids, /Parent et /Count sont produits par le modèle d'arbre. Chaque page appartient à un seul parent. Les références vers les polices et les ressources visent les mêmes instances ; le sérialiseur attribue un seul numéro d'objet à chacune.

Référence : ISO 32000-1, 7.7.3 pour l'arbre des pages et les attributs héritables, 7.8.3 pour les dictionnaires de ressources, 9.6.2.2 pour les quatorze polices standard.

2. Comprendre les coordonnées

La /MediaBox est [0 0 595.28 841.89]. Dans ce document, une unité de l'espace utilisateur vaut 1/72 de pouce. Le rectangle représente donc approximativement un A4 de 210 × 297 mm.

L'origine est en bas à gauche. La position (54, 778) place le titre près du bord supérieur. Augmenter y fait monter le texte dans cette page, qui n'a ni rotation ni transformation initiale supplémentaire.

BeginTextObject (BT) initialise les matrices de texte à l'identité. Le premier MoveTextPosition (Td) déplace la matrice de ligne de texte de (54, 692). Dans cet état initial, la position est donc directement celle de la page. Un autre Td dans le même objet serait un déplacement relatif dans le repère de la ligne, et non une nouvelle coordonnée absolue sur le papier.

Référence : 8.3.2.3 pour l'espace utilisateur, 9.4.1–9.4.2 pour les objets et le placement du texte.

3. Descendre d'une ligne

Le corps est un objet de texte unique : BT, choix de police, choix d'interligne, placement initial, plusieurs instructions d'affichage, puis ET.

Objet Opérateur Effet dans cet exemple
SetTextFontAndSize Tf Choisit /F1 à la taille 13.
SetTextLeading TL Fixe un interligne de 22 unités.
MoveToNextTextLine T* Revient au début de ligne et descend de 22 unités.
ShowText Tj Dessine les glyphes de la chaîne.

Chaque ligne est ajoutée avec ContentStreams::with(). La séquence est immuable : il faut affecter le résultat à $contents. La ligne vide est une chaîne vide précédée d'un T* ; elle occupe donc un interligne.

Un \n inséré dans LiteralString ne compose pas un paragraphe et ne déclenche pas T*. La largeur des lignes et leurs coupures sont choisies dans le script. Le pied de page ouvre un autre objet de texte, puis affiche une chaîne construite en PHP : Page 1 / 3, par exemple. Le nombre d'enfants de l'arbre et cette chaîne sont deux informations distinctes.

Référence : 9.3.5 pour l'interligne, 9.4.2–9.4.3 pour le placement et l'affichage.

4. Distinguer glyphes et métadonnées

LiteralString porte des octets. Avec une police simple, ces octets servent de codes de caractères interprétés selon l'encodage de la police. Fournir une chaîne UTF-8 contenant des accents ne sélectionne pas automatiquement les glyphes correspondants. L'exemple garde donc ses textes visibles en ASCII.

TextString sert ici au titre des métadonnées, selon les règles des chaînes de texte PDF. Il encode les caractères Unicode quand cela est nécessaire. Son rôle ne consiste pas à convertir un texte destiné à ShowText en glyphes d'une police.

Pour un texte français limité à l'encodage WinAnsi, une variante peut déclarer explicitement /Encoding /WinAnsiEncoding dans Type1Font et convertir les chaînes en Windows-1252 avant de créer LiteralString. Tous les caractères Unicode ne sont pas représentables dans cet encodage ; vérifier le résultat de la conversion. Un texte Unicode général demande une police et des tables de correspondance adaptées, avec une incorporation et une extraction de texte à traiter explicitement.

Référence : 7.9.2.2 pour les chaînes de texte des métadonnées et 9.6.6 pour les encodages des polices simples.

À essayer

  • Passer l'interligne de 22 à 28 pour observer le déplacement de chaque T*.
  • Donner une /MediaBox explicite à une page pour observer comment elle remplace la valeur héritée.
  • Ajouter un quatrième chapitre et constater que /Count et les pieds de page suivent leurs constructions respectives.

Le script ne mesure pas les glyphes, ne coupe pas les lignes et ne crée pas de nouvelle page en cas de débordement. Ces calculs doivent précéder les opérateurs PDF ; la recette de tableau illustre une composition calculée en PHP.

Index des tutoriels · Précédent : premier document · Suivant : tracés et couleurs

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 pages-and-text : page 1 sur 3
Page 1 sur 3
Résultat PDF pages-and-text : page 2 sur 3
Page 2 sur 3
Résultat PDF pages-and-text : page 3 sur 3
Page 3 sur 3
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é : pages-and-text.php
<?php

declare(strict_types=1);

use PDF\Document\DocumentInformation;
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\Information\Title;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\FileStructure;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\MoveToNextTextLine;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextLeading;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DataStructure\TextString;
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. All three pages use the same font and resource instances.
// F1/F2 are local Resource dictionary names, not font names.
$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)),
    )),
);

// 2. MediaBox and Resources are inherited by children of the Pages node.
// A unit is 1/72 inch here: 595.28 × 841.89 corresponds to A4.
$pages = new PageTreeNode(
    new Entry(new NameType('MediaBox'), new Rectangle(
        new NumericType(0),
        new NumericType(0),
        new NumericType(595.28),
        new NumericType(841.89),
    )),
    new Entry(new NameType('Resources'), new IndirectReference($resources)),
);

// These PHP data describe demonstration content, not a layout API.
// ASCII bytes suit the encodings of these standard fonts.
$chapters = [
    [
        'title' => '01 / Une page, un repere',
        'subtitle' => 'Coordonnees explicites et objets de texte',
        'lines' => [
            'L\'origine (0, 0) est en bas a gauche.',
            'La premiere ligne commence en (54, 692).',
            'Le titre est place separement en (54, 778).',
            'Une unite vaut 1/72 de pouce dans ce document.',
            '',
            'BT ouvre un objet de texte ; ET le termine.',
            'Tf choisit une ressource de police et une taille.',
            'Td deplace la position de la ligne de texte.',
            'Tj dessine les glyphes des octets de la chaine.',
            '',
            'Aucun placement n\'est calcule automatiquement.',
        ],
    ],
    [
        'title' => '02 / Des lignes explicites',
        'subtitle' => 'Interligne et matrices de texte',
        'lines' => [
            'Le corps utilise Helvetica en taille 13.',
            'TL fixe un interligne de 22 unites.',
            'T* descend d\'un interligne et revient au debut.',
            'Chaque ligne est un nouvel operateur Tj.',
            '',
            'Cette ligne vide occupe aussi un interligne.',
            'Un caractere de nouvelle ligne dans une chaine',
            'ne remplace pas ces instructions PDF.',
            '',
            'Le pied de page est un autre objet de texte.',
            'Son numero est compose explicitement en PHP.',
        ],
    ],
    [
        'title' => '03 / Un arbre commun',
        'subtitle' => 'Pages autonomes, ressources partagees',
        'lines' => [
            'Chaque page possede son propre flux Contents.',
            'Les trois pages appartiennent au meme noeud Pages.',
            'Le noeud porte le format A4 et les ressources.',
            'Ses enfants heritent de MediaBox et Resources.',
            '',
            'F1 et F2 referencent les memes objets de police.',
            'Le serialiseur attribue leurs numeros une seule fois.',
            'Il produit aussi Kids, Parent et le nombre de pages.',
            '',
            'Le titre des metadonnees peut etre Unicode.',
            'Les chaines dessinees restent des octets de glyphes.',
        ],
    ],
];

// 3. Each BT/ET sequence starts again with identity text matrices.
foreach ($chapters as $index => $chapter) {
    $contents = new ContentStreams(
        new BeginTextObject(),
        new SetTextFontAndSize(new NameType('F2'), new NumericType(24)),
        new MoveTextPosition(new NumericType(54), new NumericType(778)),
        new ShowText(new LiteralString($chapter['title'])),
        new EndTextObject(),
        new BeginTextObject(),
        new SetTextFontAndSize(new NameType('F1'), new NumericType(11)),
        new MoveTextPosition(new NumericType(54), new NumericType(750)),
        new ShowText(new LiteralString($chapter['subtitle'])),
        new EndTextObject(),
        new BeginTextObject(),
        new SetTextFontAndSize(new NameType('F1'), new NumericType(13)),
        new SetTextLeading(new NumericType(22)),
        new MoveTextPosition(new NumericType(54), new NumericType(692)),
    );

    // with() returns a new sequence: retain that result.
    foreach ($chapter['lines'] as $lineIndex => $line) {
        if ($lineIndex > 0) {
            $contents = $contents->with(new MoveToNextTextLine());
        }
        $contents = $contents->with(new ShowText(new LiteralString($line)));
    }
    $contents = $contents->with(
        new EndTextObject(),
        new BeginTextObject(),
        new SetTextFontAndSize(new NameType('F1'), new NumericType(10)),
        new MoveTextPosition(new NumericType(54), new NumericType(44)),
        new ShowText(new LiteralString('Page ' . ($index + 1) . ' / ' . count($chapters))),
        new EndTextObject(),
    );

    // MediaBox and Resources are deliberately absent from each Page.
    $page = new PageObject(contents: new StreamObject($contents));
    $pages->addKid($page);
}

// 4. TextString encodes the Unicode metadata title, not text to draw.
$catalog = new DocumentCatalog($pages);
$information = new DocumentInformation(new Title(new TextString('Pages, texte et ressources partagées')));
$file = new FileStructure(
    new FileHeader('1.7'),
    new FileBody($catalog),
    new IndirectReference($catalog),
    new DictionaryType(new Entry(new NameType('Info'), new IndirectReference($information))),
);
$path = $argv[1] ?? __DIR__ . '/pages-and-text.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;

Source de ce guide au commit documenté ↗