Le script complet construit trois pages : un titre, un paragraphe continuant sur la seconde page, un tableau avec en-tête identifié et un lien associant son texte et son annotation.
php examples/tagged-structure.php tagged-structure.pdf
Les opérateurs dessinent le contenu visible. Une séquence BDC/EMC donne un
MCID à une portion de contenu. Cet entier est local à son conteneur ; une autre
page peut avoir le même entier. Plusieurs flux /Contents d'une page sont lus
dans leur ordre, y compris si une séquence commence dans l'un et finit dans l'autre.
StructureTreeRoot et StructureElement composent l'ordre de lecture logique,
indépendamment des positions. Les enfants /K peuvent être d'autres éléments,
un contenu de page, un MCR précisant une autre page ou un OBJR vers une
annotation. L'exemple utilise un MCR pour le paragraphe continu et un OBJR
pour la zone interactive du lien.
ParentTree établit les liens inverses depuis StructParents et StructParent.
IDTree identifie l'en-tête du tableau ; Headers de sa cellule utilise cet
identifiant. ClassMap partage les attributs en conservant les révisions propres
à chaque association.
Les pieds de page sont des artefacts. La langue fr-FR et MarkInfo.Marked
déclarent les conventions choisies. Les objets contrôlent rôles, associations et
identités ; la qualité de l'ordre de lecture et les alternatives restent une
responsabilité éditoriale. Cet exemple n'est pas une certification PDF/UA.
Modifier les MCID, les liens vers les pages ou les identifiants d'en-tête montre pourquoi ces valeurs dépendent de l'ensemble du graphe.
Référence : ISO 14.7–14.9, pages physiques 564–628.
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.
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é : tagged-structure.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\Document\Structure\AttributeAssociation;
use PDF\Document\Structure\AttributeClass;
use PDF\Document\Structure\AttributeClassAssociation;
use PDF\Document\Structure\AttributeClassAssociations;
use PDF\Document\Structure\ClassMap;
use PDF\Document\Structure\MarkInformation;
use PDF\Document\Structure\MarkedContentContainer;
use PDF\Document\Structure\MarkedContentIdentifier;
use PDF\Document\Structure\MarkedContentReference;
use PDF\Document\Structure\ObjectReference;
use PDF\Document\Structure\PageMarkedContent;
use PDF\Document\Structure\PageContentSequence;
use PDF\Document\Structure\RoleMap;
use PDF\Document\Structure\StandardStructureType;
use PDF\Document\Structure\StructureAttributeObject;
use PDF\Document\Structure\StructureAttributes;
use PDF\Document\Structure\StructureElement;
use PDF\Document\Structure\StructureTreeRoot;
use PDF\FileStructure;
use PDF\Specification\Dictionary\LinkAnnotationTable173;
use PDF\Specification\Dictionary\URIActionTable206;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operator;
use PDF\Syntax\ContentStreamsResources\Operators\BeginMarkedContent;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\EndMarkedContent;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
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\CatalogDictionary\Lang;
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\BooleanType;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// ISO 32000-1, 14.7-14.8: logical structure and page drawing are
// separate graphs. Their associations remain explicit objects.
$font = new Type1Font(StandardFont::Helvetica, new Entry(new NameType('Encoding'), new NameType('WinAnsiEncoding')));
$resources = new ResourceDictionary(new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))));
$box = new Rectangle(new IntegerType(0), new IntegerType(0), new IntegerType(612), new IntegerType(792));
// These local functions only draw text. Tagging is supplied separately
// by MCID objects: no layout engine decides meaning.
/** @return list<Operator> */
$line = static fn (string $text, int $y, int $size = 14): array => [
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new IntegerType($size)),
new MoveTextPosition(new IntegerType(64), new IntegerType($y)),
new ShowText(new LiteralString($text)),
new EndTextObject(),
];
/** @return list<Operator> */
$footer = static fn (string $text): array => [
new BeginMarkedContent(new NameType('Artifact')),
...$line($text, 40, 10),
new EndMarkedContent(),
];
$headingId = new MarkedContentIdentifier(new IntegerType(0));
$paragraphStartId = new MarkedContentIdentifier(new IntegerType(1));
$paragraphEndId = new MarkedContentIdentifier(new IntegerType(0));
$headerId = new MarkedContentIdentifier(new IntegerType(1));
$cellId = new MarkedContentIdentifier(new IntegerType(2));
$linkId = new MarkedContentIdentifier(new IntegerType(0));
// The MCID identifier is local to page content. MCID 0 can therefore
// be used on three pages through three different StructParents keys.
$page1Contents = new PageContentSequence(new ContentStreams(...[
$headingId->begin(StandardStructureType::H1),
...$line('PDFToolkit : structure logique', 716, 24),
new EndMarkedContent(),
$paragraphStartId->begin(StandardStructureType::P),
...$line('Ce paragraphe commence sur la premiere page.', 660),
]), new ContentStreams(...[
// Both Contents streams are considered one page stream:
// this second fragment ends the MCID 1 sequence opened in the first.
...$line('Son ordre de lecture sera conserve sur la page suivante.', 632),
new EndMarkedContent(),
...$footer('Page 1 - ce pied de page est un artefact'),
]));
$page1 = new PageObject($box, $resources, $page1Contents->contents(), new Entry(new NameType('StructParents'), new IntegerType(0)));
$page2 = new PageObject($box, $resources, new StreamObject(new ContentStreams(...[
$paragraphEndId->begin(StandardStructureType::P),
...$line('Le meme paragraphe se termine ici, sur la deuxieme page.', 716),
new EndMarkedContent(),
$headerId->begin(StandardStructureType::TH),
...$line('Composant', 622, 17),
new EndMarkedContent(),
$cellId->begin(StandardStructureType::TD),
...$line('StructureTreeRoot', 580),
new EndMarkedContent(),
...$footer('Page 2 - tableau avec association explicite de son en-tete'),
])), new Entry(new NameType('StructParents'), new IntegerType(1)));
// The visible link and its interactive area are separate objects. OBJR attaches
// both to the same Link element; StructParent is a fourth ParentTree key.
$annotation = new LinkAnnotationTable173(
new Entry(new NameType('Type'), new NameType('Annot')),
new Entry(new NameType('Subtype'), new NameType('Link')),
new Entry(new NameType('Rect'), new Rectangle(new IntegerType(64), new IntegerType(650), new IntegerType(460), new IntegerType(676))),
new Entry(new NameType('Border'), new ArrayType(new IntegerType(0), new IntegerType(0), new IntegerType(0))),
new Entry(new NameType('StructParent'), new IntegerType(3)),
new Entry(new NameType('A'), new URIActionTable206(
new Entry(new NameType('S'), new NameType('URI')),
new Entry(new NameType('URI'), new LiteralString('https://pdftoolkit.github.io/pdf/')),
)),
);
$page3 = new PageObject($box, $resources, new StreamObject(new ContentStreams(...[
$linkId->begin(StandardStructureType::Link),
...$line('Ouvrir la documentation PDFToolkit', 660, 18),
new EndMarkedContent(),
...$footer('Page 3 - texte et annotation font partie du meme lien logique'),
])), new Entry(new NameType('StructParents'), new IntegerType(2)), new Entry(new NameType('Annots'), new ArrayType(new IndirectReference($annotation))));
$pages = (new PageTreeNode())->addKid($page1)->addKid($page2)->addKid($page3);
$scope1 = new MarkedContentContainer($page1, new IntegerType(0));
$scope2 = new MarkedContentContainer($page2, new IntegerType(1));
$scope3 = new MarkedContentContainer($page3, new IntegerType(2));
// ClassMap shares an attribute value. Revisions remain specific to element
// associations; they are never placed in the shared attribute.
$normal = new AttributeClass(new NameType('Normal'), new StructureAttributeObject(new DictionaryType(
new Entry(new NameType('O'), new NameType('Layout')),
new Entry(new NameType('TextAlign'), new NameType('Start')),
)));
$root = new StructureTreeRoot($pages, new RoleMap(), new ClassMap($normal));
$document = new StructureElement(StandardStructureType::Document);
$heading = (new StructureElement(StandardStructureType::H1, $page1, new LiteralString('heading')))->addKid(new PageMarkedContent($scope1, $headingId));
$paragraph = new StructureElement(StandardStructureType::P, $page1, new LiteralString('paragraph'), classes: new AttributeClassAssociations(new AttributeClassAssociation(new NameType('Normal'))));
$paragraph->addKid(new PageMarkedContent($scope1, $paragraphStartId));
// The full MCR form identifies the second page without changing the paragraph's Pg.
$paragraph->addKid(new MarkedContentReference($scope2, $paragraphEndId));
$table = new StructureElement(StandardStructureType::Table, $page2, new LiteralString('components'), new StructureAttributes(
new AttributeAssociation(new StructureAttributeObject(new DictionaryType(new Entry(new NameType('O'), new NameType('Layout')), new Entry(new NameType('BBox'), new Rectangle(new IntegerType(64), new IntegerType(575), new IntegerType(450), new IntegerType(645)))))),
new AttributeAssociation(new StructureAttributeObject(new DictionaryType(new Entry(new NameType('O'), new NameType('Table')), new Entry(new NameType('Summary'), new TextString('Liste des composants PDF'))))),
));
$header = new StructureElement(StandardStructureType::TH, $page2, new LiteralString('component-header'), new StructureAttributes(new AttributeAssociation(new StructureAttributeObject(new DictionaryType(new Entry(new NameType('O'), new NameType('Table')), new Entry(new NameType('Scope'), new NameType('Column')))))));
$header->addKid(new PageMarkedContent($scope2, $headerId));
$cell = new StructureElement(StandardStructureType::TD, $page2, attributes: new StructureAttributes(new AttributeAssociation(new StructureAttributeObject(new DictionaryType(new Entry(new NameType('O'), new NameType('Table')), new Entry(new NameType('Headers'), new ArrayType(new LiteralString('component-header'))))))));
$cell->addKid(new PageMarkedContent($scope2, $cellId));
$table->addKid((new StructureElement(StandardStructureType::TR))->addKid($header));
$table->addKid((new StructureElement(StandardStructureType::TR))->addKid($cell));
$link = (new StructureElement(StandardStructureType::Link, $page3, new LiteralString('documentation-link')))->addKid(new PageMarkedContent($scope3, $linkId))->addKid(new ObjectReference($page3, $annotation, new IntegerType(3)));
$root->addKid($document->addKid($heading)->addKid($paragraph)->addKid($table)->addKid($link));
// This validation builds P/K, IDTree and ParentTree, checks their links,
// then verifies Tagged PDF structural conventions. Word choice, Unicode,
// layout and reading order remain explicit.
$catalog = new DocumentCatalog($pages, new Lang(new TextString('fr-FR')), ...$root->catalogEntries(new MarkInformation(new BooleanType(true))));
$file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/tagged-structure.pdf';
$file->writeTo($path);
echo 'PDF written to ' . $path . PHP_EOL;


