Tutorial index · Previous: first document · Next: paths and colors
This lesson builds three A4 pages that share resources. It shows how to position text, choose leading and compose a footer. The complete script is pages-and-text.php.
Run the example
Use PHP 8.3 or later and the dependencies installed by composer install, as in the first tutorial. From the root of a core checkout:
php examples/pages-and-text.php pages-and-text.pdf
The first argument names the output file. Without an argument, the script writes examples/pages-and-text.pdf. Its parent directory must already exist.
What you get
Three A4 pages, each with a title, subtitle, several lines of text and a page number. The body uses a size of 13 units and leading of 22 units. Displayed strings are deliberately ASCII; the document title in the metadata contains accented characters.

1. Share resources and page dimensions
The two Type1Font instances represent Helvetica and Helvetica-Bold. The resources' Font dictionary associates these objects with names /F1 and /F2. SetTextFontAndSize targets this local name: /F1 is not the font's commercial name.
PageTreeNode receives the /MediaBox and /Resources entries. The three PageObject instances supply only their /Contents streams. When a page has no local value for an inheritable entry, the PDF reader follows the /Pages tree upwards: here it finds the same rectangle and resources.
$page = new PageObject(contents: new StreamObject($contents));
$pages->addKid($page);
The tree model produces /Kids, /Parent and /Count. Each page belongs to one parent. References to fonts and resources target the same instances; the serializer assigns each a single object number.
Reference: ISO 32000-1, 7.7.3 for the page tree and inheritable attributes, 7.8.3 for resource dictionaries, 9.6.2.2 for the fourteen standard fonts.
2. Understand coordinates
The /MediaBox is [0 0 595.28 841.89]. In this document, a user-space unit is 1/72 inch. The rectangle therefore represents approximately an A4 sheet of 210 × 297 mm.
The origin is at the lower left. Position (54, 778) places the title near the top edge. Increasing y moves text upwards on this page, which has no rotation or additional initial transformation.
BeginTextObject (BT) initializes the text matrices to the identity. The first MoveTextPosition (Td) translates the text line matrix by (54, 692). In this initial state, the position therefore directly matches page coordinates. Another Td in the same object would be a relative movement in the line's coordinate system, not a new absolute position on the paper.
Reference: 8.3.2.3 for user space, 9.4.1–9.4.2 for text objects and positioning.
3. Move down one line
The body is a single text object: BT, font selection, leading selection, initial positioning, several text-showing instructions, then ET.
| Object | Operator | Effect in this example |
|---|---|---|
SetTextFontAndSize |
Tf |
Select /F1 at size 13. |
SetTextLeading |
TL |
Set leading to 22 units. |
MoveToNextTextLine |
T* |
Return to the start of the line and move down 22 units. |
ShowText |
Tj |
Draw the string's glyphs. |
Each line is added with ContentStreams::with(). The sequence is immutable: assign the result to $contents. The blank line is an empty string preceded by T*, so it occupies one line of leading.
A \n inside LiteralString does not compose a paragraph or trigger T*. Line widths and breaks are chosen in the script. The footer opens another text object and displays a string built in PHP, such as Page 1 / 3. The tree's child count and this string are separate pieces of information.
Reference: 9.3.5 for leading, 9.4.2–9.4.3 for positioning and showing text.
4. Distinguish glyphs from metadata
LiteralString carries bytes. With a simple font, these bytes act as character codes interpreted according to the font's encoding. Supplying a UTF-8 string with accented characters does not automatically select the corresponding glyphs. This example therefore keeps visible text in ASCII.
TextString is used here for the metadata title, following PDF text-string rules. It encodes Unicode characters when necessary. It does not convert text intended for ShowText into font glyphs.
For French text restricted to WinAnsi, a variation can explicitly declare /Encoding /WinAnsiEncoding in Type1Font and convert strings to Windows-1252 before creating LiteralString. This encoding cannot represent every Unicode character; check the conversion result. General Unicode text needs a suitable font and mapping tables, with embedding and text extraction handled explicitly.
Reference: 7.9.2.2 for metadata text strings and 9.6.6 for simple-font encodings.
Try it
- Change leading from 22 to 28 to observe the movement caused by each
T*. - Give one page an explicit
/MediaBoxto see how it overrides the inherited value. - Add a fourth chapter and observe that
/Countand the footers follow their respective constructions.
The script does not measure glyphs, break lines or create a new page when content overflows. These calculations must precede PDF operators; the table recipe demonstrates a composition calculated in PHP.
Tutorial index · Previous: first document · Next: paths and colors
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: 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;


