Tutorial path · Previous: navigation · Next: images
Goal and prerequisites
Create two clickable areas on a page: one opens the GitHub repository, the other displays a second page. This tutorial composes classes derived from specification tables to show how to use a feature without a specialized domain facade.
Read pages and text and destinations, then install dependencies with composer install. The script examples/links.php runs independently on PHP 8.3 or later with the extensions required by Composer.
Run and inspect
From the root of a core checkout:
php examples/links.php
php examples/links.php links-demo.pdf
The first call produces examples/links.pdf. The first page displays two blue text strings; the second leads to the page “Vous avez suivi le lien interne”. The GitHub link offers to open an external address, according to the reader's settings.

The image helps locate the text. Test links in the original PDF with an interactive reader: a page image does not carry annotations.
Drawing and interaction
ShowText produces visible glyphs. Neither their color nor their content creates a link. Interaction belongs to another object: an annotation with /Subtype /Link, referenced in the page's /Annots array.
Its /Rect is the active area, in page coordinates. The four numbers give two opposite corners, lower x, lower y, upper x, upper y, whereas the graphics operator re uses x, y, width, height. The script places text inside these areas and uses /Border [0 0 0] to suppress the border drawn by the reader.
Read the construction
- Create the target page. The script first builds the second
PageObject. The internal link targets this instance; final order is still determined by calls toaddKid(). - Describe the external action.
URIActionTable206containsSEntry(new NameType('URI'))andURIEntry(new LiteralString(...))./Sis the action type;/URIcontains its ASCII address. The annotation references this dictionary through its/Aentry. - Describe the internal destination. The second annotation receives
DestEntry(new Fit($targetPage)). There is no/Aaction here:/Destis an explicit destination. The specification forbids adding/Destwhen/Ais present on the same link; the script follows that composition rule. - Compose common entries.
TypeEntry,RectEntry,BorderEntryandContentsEntrycome from Table 164.SubtypeEntry,AEntryandDestEntrycome from Table 173.LinkAnnotationTable173composes the common Table 164 definition, including its required entries. - Attach annotations.
AnnotsEntrybelongs to Table 30, which describes the page. ItsArrayTypecontainsIndirectReferenceinstances to both annotations. Each annotation therefore retains its own PDF object identity. - Assemble the page tree. The first
PageObjectreceives/Annotsbefore attachment. The page containing the links is added first, followed by the target, to the samePageTreeNode.
ContentsEntry(new TextString(...)) supplies a text description of each annotation. This description helps interpret the object; by itself, it does not constitute a complete PDF accessibility structure.
Why these class names?
The names LinkAnnotationTable173 and URIActionTable206 retain the connection to ISO tables. Their entries and definitions are PHP objects, and known constraints are checked during construction or serialization. The number distinguishes tables with similar titles and makes their source easier to find.
Definitions also expose conditional rules in their descriptions. They do not automatically guarantee every possible relationship between entries. Here, the code deliberately chooses only one of the two targets /A and /Dest, supplies the correct names /Link and /URI, and uses indirect references in /Annots.
Mapping to ISO 32000-1:2008
| Object or entry | Reference |
|---|---|
Page /Annots |
§7.7.3.3, Table 30 |
| Common annotation entries | §12.5.2, Table 164 |
/Link annotation |
§12.5.6.5, Table 173 |
Explicit /Fit destination |
§12.3.2.2, Table 151 |
/URI action |
§12.6.4.7, Table 206 |
See the specification preserved in the core and the definitions guide.
Variations and limitations
To target a named destination, replace Fit with a LiteralString containing a key published in /Names /Dests, as in the navigation tutorial. To move a link, change both text position and annotation rectangle: their association is a layout decision in this script.
The active area is rectangular; it is not calculated from font metrics. The visible text's ASCII values avoid encoding issues with standard fonts here. There is no automatic line wrapping or custom annotation appearance. The reader may request confirmation before following an external link.
Back to the tutorial path · Continue: images
View the PDF result
This file was generated with the code in this documentation. All pages appear below; open the PDF to use its links and bookmarks.
Use the browser PDF viewer
This viewer depends on your browser. If the document does not appear, use “Open the PDF” or “Download”.
Complete source code
Code from roukmoute/pdf · View the source at the documented commit ↗
Read the commented script: 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;

