Parcours des tutoriels · Précédent : navigation · Suivant : images

But et prérequis

Créer deux zones cliquables sur une page : l'une ouvre le dépôt GitHub, l'autre affiche une seconde page. Ce tutoriel compose les classes issues des tableaux de la norme pour montrer comment utiliser une fonctionnalité qui n'a pas de façade spécialisée dans le domaine.

Lire pages et texte et destinations, puis installer les dépendances avec composer install. Le script examples/links.php fonctionne seul sous PHP 8.3 ou supérieur avec les extensions demandées par Composer.

Exécuter et regarder

Depuis la racine d'un checkout du core :

php examples/links.php
php examples/links.php links-demo.pdf

Le premier appel produit examples/links.pdf. La première page affiche deux textes bleus ; le second mène à la page « Vous avez suivi le lien interne ». Le lien GitHub propose d'ouvrir une adresse externe, selon les réglages du lecteur.

Aperçu de la page comportant les deux liens

L'image sert à repérer les textes. Tester les liens dans le PDF original avec un lecteur interactif : une image de page ne transporte pas les annotations.

Dessin et interaction

Un ShowText produit des glyphes visibles. Ni leur couleur ni leur contenu ne créent de lien. L'interaction est portée par un autre objet : une annotation /Subtype /Link, référencée dans le tableau /Annots de la page.

La zone active est son /Rect, en coordonnées de page. Les quatre nombres sont les deux coins opposés, x inférieur, y inférieur, x supérieur, y supérieur, alors que l'opérateur graphique re utilise x, y, largeur, hauteur. Le script place les textes à l'intérieur de ces zones et utilise /Border [0 0 0] pour supprimer la bordure dessinée par le lecteur.

Lire la construction

  1. Créer la page cible. Le script construit d'abord le second PageObject. Le lien interne vise cette instance ; l'ordre final reste déterminé par les appels à addKid().
  2. Décrire l'action externe. URIActionTable206 contient SEntry(new NameType('URI')) et URIEntry(new LiteralString(...)). /S est le type d'action ; /URI contient son adresse ASCII. L'annotation référence ce dictionnaire par son entrée /A.
  3. Décrire la destination interne. La deuxième annotation reçoit DestEntry(new Fit($targetPage)). Cette fois, il n'y a pas d'action /A : /Dest est une destination explicite. La norme interdit d'ajouter /Dest lorsque /A est présent sur le même lien ; le script respecte cette règle de composition.
  4. Composer les entrées communes. TypeEntry, RectEntry, BorderEntry et ContentsEntry proviennent du tableau 164. SubtypeEntry, AEntry et DestEntry proviennent du tableau 173. LinkAnnotationTable173 compose la définition du tableau commun 164, notamment ses entrées obligatoires.
  5. Rattacher les annotations. AnnotsEntry appartient au tableau 30, consacré à la page. Son ArrayType contient des IndirectReference vers les deux annotations. Une annotation garde ainsi sa propre identité d'objet PDF.
  6. Assembler l'arbre des pages. Le premier PageObject reçoit /Annots avant son rattachement. On ajoute ensuite la page porteuse des liens, puis la cible, au même PageTreeNode.

ContentsEntry(new TextString(...)) fournit une description textuelle de chaque annotation. Cette description contribue à l'interprétation de l'objet ; elle ne constitue pas, à elle seule, une structure complète d'accessibilité PDF.

Pourquoi ces noms de classes ?

Les noms LinkAnnotationTable173 et URIActionTable206 conservent le lien avec les tableaux ISO. Leurs entrées et leurs définitions sont des objets PHP, et leurs contraintes connues sont vérifiées lors de la construction ou de la sérialisation. Le numéro distingue les tableaux portant des titres proches et facilite la consultation de la source.

Les définitions exposent aussi les règles conditionnelles dans leurs descriptions. Elles ne garantissent pas automatiquement toutes les relations possibles entre entrées. Ici, le code choisit volontairement une seule des deux cibles /A et /Dest, fournit les bons noms /Link et /URI, et emploie des références indirectes dans /Annots.

Correspondance avec ISO 32000-1:2008

Objet ou entrée Référence
/Annots de la page §7.7.3.3, tableau 30
Entrées communes d'une annotation §12.5.2, tableau 164
Annotation /Link §12.5.6.5, tableau 173
Destination explicite /Fit §12.3.2.2, tableau 151
Action /URI §12.6.4.7, tableau 206

Voir la norme conservée dans le core et le guide des définitions.

Variantes et limites

Pour viser une destination nommée, remplacer le Fit par une LiteralString portant une clé publiée dans /Names /Dests, comme dans le tutoriel de navigation. Pour déplacer un lien, modifier à la fois la position du texte et le rectangle de l'annotation : leur association est une décision de mise en page dans ce script.

La zone active est rectangulaire ; elle n'est pas calculée à partir de métriques de police. Les valeurs ASCII du texte visible évitent ici les problèmes de codage avec les polices standard. Il n'y a pas de retour automatique à la ligne ni d'apparence personnalisée d'annotation. Le lecteur peut demander une confirmation avant de suivre un lien externe.

Retour au parcours · Continuer : images

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

declare(strict_types=1);

use PDF\Document\Destinations\Fit;
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\FileStructure;
use PDF\Specification\Dictionary\AnnotationDictionaryTable164\BorderEntry;
use PDF\Specification\Dictionary\AnnotationDictionaryTable164\ContentsEntry;
use PDF\Specification\Dictionary\AnnotationDictionaryTable164\RectEntry;
use PDF\Specification\Dictionary\AnnotationDictionaryTable164\TypeEntry;
use PDF\Specification\Dictionary\LinkAnnotationTable173;
use PDF\Specification\Dictionary\LinkAnnotationTable173\AEntry;
use PDF\Specification\Dictionary\LinkAnnotationTable173\DestEntry;
use PDF\Specification\Dictionary\LinkAnnotationTable173\SubtypeEntry;
use PDF\Specification\Dictionary\PageObjectTable30\AnnotsEntry;
use PDF\Specification\Dictionary\URIActionTable206;
use PDF\Specification\Dictionary\URIActionTable206\SEntry;
use PDF\Specification\Dictionary\URIActionTable206\URIEntry;
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\SetNonstrokingRGBColor;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
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\ArrayType;
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. Blue text is only drawing. Making it clickable requires
// a /Link annotation in /Annots with its rectangle and target.
$font = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(new Font(new DictionaryType(
    new Entry(new NameType('F1'), new IndirectReference($font)),
)));
$mediaBox = new Rectangle(new NumericType(0), new NumericType(0), new NumericType(595), new NumericType(842));

// 2. Building the target page first lets the internal link target this
// instance. Its position is determined later by addKid() order.
$targetPage = new PageObject($mediaBox, $resources, new StreamObject(new ContentStreams(
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(22)),
    new MoveTextPosition(new NumericType(54), new NumericType(770)),
    new ShowText(new LiteralString('Vous avez suivi le lien interne.')),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
    new MoveTextPosition(new NumericType(0), new NumericType(-40)),
    new ShowText(new LiteralString('La destination /Fit ajuste cette seconde page dans la fenetre.')),
    new EndTextObject(),
)));

// 3. A URI action describes external navigation: /S /URI selects its
// type; /URI contains the ASCII address. Table206 connects the class
// to its ISO definition. It is not a convenience API.
$externalAction = new URIActionTable206(
    new SEntry(new NameType('URI')),
    new URIEntry(new LiteralString('https://github.com/roukmoute/pdf')),
);
$externalLink = new LinkAnnotationTable173(
    new TypeEntry(new NameType('Annot')),
    new SubtypeEntry(new NameType('Link')),
    new RectEntry(new Rectangle(new NumericType(54), new NumericType(671), new NumericType(380), new NumericType(693))),
    new BorderEntry(new ArrayType(new NumericType(0), new NumericType(0), new NumericType(0))),
    new ContentsEntry(new TextString('Ouvrir le dépôt GitHub du projet')),
    new AEntry($externalAction),
);

// 4. The internal link has /Dest instead of /A. Do not supply both
// for one annotation. Fit produces [page-reference /Fit].
$internalLink = new LinkAnnotationTable173(
    new TypeEntry(new NameType('Annot')),
    new SubtypeEntry(new NameType('Link')),
    new RectEntry(new Rectangle(new NumericType(54), new NumericType(611), new NumericType(380), new NumericType(633))),
    new BorderEntry(new ArrayType(new NumericType(0), new NumericType(0), new NumericType(0))),
    new ContentsEntry(new TextString('Aller à la deuxième page')),
    new DestEntry(new Fit($targetPage)),
);

// 5. /Rect uses page coordinates with the origin at the lower left.
// Text is positioned at y=678 and y=618 inside these rectangles.
// /Border [0 0 0] suppresses the annotation's automatic border.
$contents = new StreamObject(new ContentStreams(
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(22)),
    new MoveTextPosition(new NumericType(54), new NumericType(770)),
    new ShowText(new LiteralString('Deux liens, deux mecanismes PDF')),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
    new MoveTextPosition(new NumericType(0), new NumericType(-40)),
    new ShowText(new LiteralString('Cliquez sur les textes bleus dans un lecteur PDF interactif.')),
    new SetNonstrokingRGBColor(new NumericType(0.1), new NumericType(0.3), new NumericType(0.75)),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(15)),
    new MoveTextPosition(new NumericType(0), new NumericType(-52)),
    new ShowText(new LiteralString('Ouvrir le depot GitHub du projet')),
    new MoveTextPosition(new NumericType(0), new NumericType(-60)),
    new ShowText(new LiteralString('Aller a la deuxieme page')),
    new EndTextObject(),
));

// 6. Each annotation is referenced indirectly by the /Annots array.
// The LinkAnnotation schema also composes common Table164 entries.
// The page must receive its entries before attachment to the tree.
$firstPage = new PageObject($mediaBox, $resources, $contents, new AnnotsEntry(new ArrayType(
    new IndirectReference($externalLink),
    new IndirectReference($internalLink),
)));
$pages = (new PageTreeNode())->addKid($firstPage)->addKid($targetPage);
$catalog = new DocumentCatalog($pages);
$file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/links.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;

Source de ce guide au commit documenté ↗