*/ $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 */ $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;