Building the PDF/UA structure element tree by hand, rather than letting the HTML renderer generate it.
- Rendered PDF: E063_manual_tag_tree.pdf
- PHP source: E063_manual_tag_tree.phps
- All examples: /examples/
- Category: Standards and Conformance
- Guide: Standards and Conformance
Source Code
<?php
declare(strict_types=1);
/**
* E063_manual_tag_tree.php
*
* @since 2026-05-01
* @category Library
* @package Pdf
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf
*
* This file is part of tc-lib-pdf software library.
*/
// NOTE: local file reads (images, fonts, attachments) are restricted to an allowlist of
// trusted paths that covers this package tree, so run the examples in place. To read assets
// from other locations, list them in the 'allowedPaths' entry of the fileOptions constructor
// parameter (see E047_remote_resources_security.php).
// NOTE: run make fonts in the project root to generate the dependencies and example fonts.
// autoloader when using Composer
require __DIR__ . '/../vendor/autoload.php';
// define fonts directory
\define('K_PATH_FONTS', \realpath(__DIR__ . '/../vendor/tecnickcom/tc-lib-pdf-font/target/fonts'));
/**
* Demonstrate manual PDF/UA structure-element tagging via
* beginStructElem() / endStructElem().
*
* Background
* ──────────
* Accessible (tagged) PDFs carry a logical structure tree that screen readers
* use to navigate and read content in reading order. When tc-lib-pdf operates
* in a pdfua or pdfua1/pdfua2 mode it auto-tags content produced by
* addHTMLCell() using the HTML semantic elements (h1→H1, p→P, etc.).
*
* For content produced with the lower-level text APIs - or for cases
* where you need precise control over structure roles - you can bracket each
* logical block with:
*
* beginStructElem(string $role, int $pid)
* Opens a structure-element bracket. All tagged content produced
* until the matching endStructElem() is associated with this element.
* $role is a PDF structure role (see PDF 32000 §14.8):
* Headings : H, H1 … H6
* Body : P, Blockquote, Note, Caption
* List : L, LI, Lbl, LBody
* Table : Table, TR, TH, TD
* Inline : Span, Link, Reference, BibEntry
* Other : Document, Part, Sect, Art, Figure, TOC, TOCI
* $pid is the page ID from addPage()['pid'].
*
* endStructElem()
* Closes the current bracket. The completed element (with all its
* tagged content MCIDs) is appended to the structure log.
* Empty elements (no tagged content inside the bracket) are silently
* discarded.
*
* When pdfuaMode is '' (plain PDF mode) both methods are no-ops, so the
* same code runs safely in plain and PDF/UA modes without changes.
*
* NOTE: beginStructElem()/endStructElem() only associate MCIDs with content
* emitted through the tagged text path. For manual tagging, use addTextCell()
* (second argument is the page ID) rather than writing raw getTextCell()
* output directly with page->addContent().
*
* This example uses 'pdfua1' mode and demonstrates the full manual-tagging
* surface across four pages:
*
* Page 1 - Heading roles (H1, H2, H3), P, Figure, Caption
* Page 2 - Note, Blockquote, multi-paragraph body
* Page 3 - Sect/Art grouping containers; list structure (L > LI > Lbl + LBody)
* Page 4 - Table nesting (Table > TR > TH / TD); Figure with /Alt alternate text
*
* Non-semantic decorative content can be emitted as Artifact marked-content
* with addArtifactContent() or, at the low level, by writing the strings
* returned by beginArtifact() / endArtifact() with page->addContent().
* This example uses addArtifactContent() to add decorative separator rules as
* /Artifact with /Type /Layout.
*
* All content is tagged with addTextCell() inside explicit struct-elem brackets.
* Compare with E015–E017 which enable pdfua mode via addHTMLCell (auto-tagged).
*/
$pdf = new \Com\Tecnick\Pdf\Tcpdf(
unit: \Com\Tecnick\Pdf\Page\Unit::Millimeter,
isunicode: true,
subsetfont: false,
compress: true,
mode: \Com\Tecnick\Pdf\PdfConformance::Pdfua1,
objEncrypt: null,
);
$pdf->setCreator('tc-lib-pdf');
$pdf->setAuthor('Nicola Asuni');
$pdf->setSubject('tc-lib-pdf example: 063');
$pdf->setTitle('Manual Tag Tree (PDF/UA Structure Elements)');
$pdf->setKeywords('TCPDF tc-lib-pdf example pdfua tagged structure accessibility wcag screen reader');
$pdf->setPDFFilename('E063_manual_tag_tree.pdf');
// A tagged document must declare the natural language of its content.
$pdf->setLanguage('en-US');
$pdf->setViewerPreferences(['DisplayDocTitle' => true]);
$leftMargin = 15.0;
$rightMargin = 15.0;
$pdf->setDefaultCellMargin(top: 0.0, right: $rightMargin, bottom: 0.0, left: 0.0);
$setFont = static function (\Com\Tecnick\Pdf\Tcpdf $pdf, string $family, string $style, int $size): array {
if ($family === 'helvetica') {
$family = 'dejavusans';
}
$font = $pdf->font->insert($pdf->pon, $family, $style, $size);
$pdf->page->addContent($font['out']);
return $font;
};
$addDecorativeSeparator = static function (\Com\Tecnick\Pdf\Tcpdf $pdf, int $pid, float $posY) use ($leftMargin): void {
$lineStyle = [
'all' => [
'lineWidth' => 0.25,
'lineCap' => 'butt',
'lineJoin' => 'miter',
'dashArray' => [],
'dashPhase' => 0,
'lineColor' => '#bdbdbd',
'fillColor' => '',
],
];
$pdf->addArtifactContent($pdf->graph->getLine($leftMargin, $posY, 195.0, $posY, $lineStyle), $pid, 'Layout');
};
// -----------------------------------------------------------------------
// Font setup - insert before addPage()
// -----------------------------------------------------------------------
$pdf->font->insert($pdf->pon, 'dejavusans', '', 10);
// -----------------------------------------------------------------------
// Page 1 - H1, H2, H3, P, Caption, Figure structure elements
// -----------------------------------------------------------------------
$page1 = $pdf->addPage();
$pid1 = $page1['pid'];
$addDecorativeSeparator($pdf, $pid1, 16.0);
// --- H1 heading ---
$setFont($pdf, 'helvetica', 'B', 16);
$pdf->beginStructElem(role: 'H1', pid: $pid1);
$pdf->addTextCell(
'Manual Tag Tree — PDF/UA Structure Elements',
$pid1,
$leftMargin,
20.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
// --- Introductory paragraph (P) ---
$setFont($pdf, 'helvetica', '', 10);
$intro =
'This document uses beginStructElem() and endStructElem() to build '
. 'the accessibility structure tree manually. Every piece of text is '
. 'wrapped in an explicit structure-element bracket that maps content '
. 'to a PDF logical role (H1, H2, H3, P, Caption). A screen reader can '
. 'navigate the heading hierarchy and read body paragraphs in the correct '
. 'logical order.';
$pdf->beginStructElem(role: 'P', pid: $pid1);
$pdf->addTextCell(
$intro,
$pid1,
$leftMargin,
32.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
// --- Section 1: H2 + P ---
$setFont($pdf, 'helvetica', 'B', 14);
$pdf->beginStructElem(role: 'H2', pid: $pid1);
$pdf->addTextCell(
'Section 1 — Heading Roles',
$pid1,
$leftMargin,
55.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', '', 10);
$sec1body =
'Structure roles H1 through H6 define the heading hierarchy. '
. 'H1 is the document title; H2 is a top-level section heading; H3 is a '
. 'sub-section; and so on. Screen readers present this outline as a '
. 'navigable table of contents. Nesting is implied by the sequence of '
. 'structure elements logged by endStructElem().';
$pdf->beginStructElem(role: 'P', pid: $pid1);
$pdf->addTextCell(
$sec1body,
$pid1,
$leftMargin,
64.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
// --- Sub-section: H3 ---
$setFont($pdf, 'helvetica', 'BI', 11);
$pdf->beginStructElem(role: 'H3', pid: $pid1);
$pdf->addTextCell(
'1.1 — Sub-section with H3 role',
$pid1,
$leftMargin,
85.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'P', pid: $pid1);
$pdf->addTextCell(
'An H3 element is used here to show a third level in the outline. '
. 'The structure tree will contain: H1 > H2 > H3 > P, reflecting the '
. 'logical reading order of this page.',
$pid1,
$leftMargin,
93.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
// --- Section 2: H2 + P + Figure + Caption ---
$setFont($pdf, 'helvetica', 'B', 14);
$pdf->beginStructElem(role: 'H2', pid: $pid1);
$pdf->addTextCell(
'Section 2 — Figure and Caption Roles',
$pid1,
$leftMargin,
113.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'P', pid: $pid1);
$pdf->addTextCell(
'The Figure role wraps graphical content. '
. 'A Caption role immediately following a Figure provides an '
. 'accessible description of the visual element for screen-reader users.',
$pid1,
$leftMargin,
122.0,
150,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
// Draw and tag a simple coloured rectangle as Figure content.
$figStyle = [
'all' => [
'lineWidth' => 0.5,
'lineCap' => 'butt',
'lineJoin' => 'miter',
'dashArray' => [],
'dashPhase' => 0,
'lineColor' => '#336699',
'fillColor' => '#cce0ff',
],
];
// The bounding box is required by PDF/UA for figures contained on a single page.
// It is expressed in points, from the bottom-left page corner.
$pdf->addTaggedFigureContent(
$pdf->graph->getStartTransform() . $pdf->graph->getRect($leftMargin, 143.0, 180.0, 25.0, 'DF', $figStyle)
. $pdf->graph->getStopTransform(),
$pid1,
'Blue placeholder rectangle representing a figure area',
[
$pdf->toPoints($leftMargin),
$pdf->toPoints($page1['height'] - 143.0 - 25.0),
$pdf->toPoints($leftMargin + 180.0),
$pdf->toPoints($page1['height'] - 143.0),
],
);
// Caption for the figure.
$pdf->page->addContent($pdf->color->getPdfColor('black'), $pid1);
$setFont($pdf, 'helvetica', 'I', 8);
$pdf->beginStructElem(role: 'Caption', pid: $pid1);
$pdf->addTextCell(
'Figure 1 — Placeholder rectangle representing graphical content (Caption role)',
$pid1,
$leftMargin,
170.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Center,
);
$pdf->endStructElem();
// -----------------------------------------------------------------------
// Page 2 - Note, Blockquote, and multi-paragraph body
// -----------------------------------------------------------------------
$page2 = $pdf->addPage();
$pid2 = $page2['pid'];
$addDecorativeSeparator($pdf, $pid2, 16.0);
$setFont($pdf, 'helvetica', 'B', 14);
$pdf->beginStructElem(role: 'H2', pid: $pid2);
$pdf->addTextCell(
'Page 2 — Note, Blockquote, and Multi-element Brackets',
$pid2,
$leftMargin,
20.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', '', 10);
// Note element (with explicit ID for validator compatibility)
$pdf->beginStructElem(role: 'Note', pid: $pid2, alt: null, attr: ['ID' => 'note-p2-01']);
$pdf->addTextCell(
'NOTE: beginStructElem() / endStructElem() are no-ops in plain PDF mode '
. '(pdfuaMode = ""). The same code runs safely in both plain and PDF/UA '
. 'modes without conditional branching.',
$pid2,
$leftMargin,
32.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
// Blockquote element
$setFont($pdf, 'helvetica', 'BI', 11);
$pdf->beginStructElem(role: 'BlockQuote', pid: $pid2);
$pdf->addTextCell(
'"Tagged PDF is the foundation of accessible PDF. '
. 'Without a well-formed structure tree, automated tools and assistive '
. 'technologies cannot reliably interpret document content."',
$pid2,
$leftMargin,
52.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', 'I', 8);
$pdf->beginStructElem(role: 'Caption', pid: $pid2);
$pdf->addTextCell(
'— ISO 14289-1:2012 (PDF/UA-1) rationale',
$pid2,
$leftMargin,
72.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Right,
);
$pdf->endStructElem();
// Multi-paragraph body section
$setFont($pdf, 'helvetica', '', 10);
$paras = [
'Each call to endStructElem() closes the innermost open bracket. '
. 'Completed elements keep their nested parent-child relationships, so '
. 'the emitted structure tree follows the bracket hierarchy and preserves '
. 'the reading order of nested content blocks.',
'A structure bracket that receives no tagged content (i.e. no addTextCell '
. 'call between beginStructElem and endStructElem) is silently discarded. '
. 'This prevents empty structure nodes from appearing in the tree.',
'Pages 3 and 4 of this document demonstrate the remaining role categories: '
. 'grouping containers (Sect, Art), list structure (L, LI, Lbl, LBody), '
. 'and table structure (Table, TR, TH, TD) — each requiring multi-level '
. 'nested brackets that the library resolves at getOutPDFString() time.',
];
$yPos = 85.0;
foreach ($paras as $para) {
$pdf->beginStructElem(role: 'P', pid: $pid2);
$pdf->addTextCell(
$para,
$pid2,
$leftMargin,
$yPos,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
$yPos += 25.0;
}
// -----------------------------------------------------------------------
// Page 3 - Sect/Art grouping containers + List structure
// -----------------------------------------------------------------------
$page3 = $pdf->addPage();
$pid3 = $page3['pid'];
$addDecorativeSeparator($pdf, $pid3, 16.0);
$setFont($pdf, 'helvetica', 'B', 16);
$pdf->beginStructElem(role: 'H2', pid: $pid3);
$pdf->addTextCell(
'Page 3 — Grouping Containers and List Structure',
$pid3,
$leftMargin,
20.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
// ── Sect wrapping a sub-section (H3 + P inside a Sect container) ──────
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'Sect', pid: $pid3);
$setFont($pdf, 'helvetica', 'B', 13);
$pdf->beginStructElem(role: 'H3', pid: $pid3);
$pdf->addTextCell(
'3.1 — Sect Container',
$pid3,
$leftMargin,
34.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'P', pid: $pid3);
$pdf->addTextCell(
'The Sect (Section) role is a grouping container: it contributes no '
. 'content of its own but wraps related blocks — here an H3 and this P '
. 'paragraph — into a single logical section node in the structure tree. '
. 'Screen readers and PDF validators see the H3 + P as children of the Sect.',
$pid3,
$leftMargin,
43.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
$pdf->endStructElem(); // Sect
// ── Art container ─────────────────────────────────────────────────────
$pdf->beginStructElem(role: 'Art', pid: $pid3);
$setFont($pdf, 'helvetica', 'B', 13);
$pdf->beginStructElem(role: 'H3', pid: $pid3);
$pdf->addTextCell(
'3.2 — Art Container',
$pid3,
$leftMargin,
65.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'P', pid: $pid3);
$pdf->addTextCell(
'The Art (Article) role marks a self-contained composition: its content '
. 'could stand alone as a discrete piece. It is distinguished from Sect '
. 'in that Sect groups thematically related material within a larger work, '
. 'while Art denotes an independently distributable unit.',
$pid3,
$leftMargin,
74.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
$pdf->endStructElem(); // Art
// ── List: L > LI > {Lbl + LBody} ─────────────────────────────────────
$setFont($pdf, 'helvetica', 'B', 13);
$pdf->beginStructElem(role: 'H3', pid: $pid3);
$pdf->addTextCell(
'3.3 — List Structure (L > LI > Lbl + LBody)',
$pid3,
$leftMargin,
97.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'P', pid: $pid3);
$pdf->addTextCell(
'Each list item (LI) contains a label (Lbl) and a body (LBody). '
. 'The outer L bracket wraps all items into one logical list node.',
$pid3,
$leftMargin,
107.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
$listItems = [
['Headings (H1–H6)', 'Define the document outline navigated by screen readers.'],
['Body roles (P, Note, Blockquote)', 'Carry prose content at the block level.'],
['Grouping roles (Sect, Art, Part)', 'Wrap related blocks without adding visible content.'],
['List roles (L, LI, Lbl, LBody)', 'Express enumerated or bulleted list structure.'],
['Table roles (Table, TR, TH, TD)', 'Capture row/column relationships in data tables.'],
];
$pdf->beginStructElem(role: 'L', pid: $pid3);
$itemY = 122.0;
foreach ($listItems as [$label, $body]) {
$pdf->beginStructElem(role: 'LI', pid: $pid3);
$setFont($pdf, 'helvetica', 'B', 10);
$pdf->beginStructElem(role: 'Lbl', pid: $pid3);
$pdf->addTextCell(
'• ' . $label,
$pid3,
$leftMargin,
$itemY,
65.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem(); // Lbl
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'LBody', pid: $pid3);
$pdf->addTextCell(
$body,
$pid3,
$leftMargin + 65.0,
$itemY,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem(); // LBody
$pdf->endStructElem(); // LI
$itemY += 8.0;
}
$pdf->endStructElem(); // L
// -----------------------------------------------------------------------
// Page 4 - Table nesting + Figure with /Alt alternate description
// -----------------------------------------------------------------------
$page4 = $pdf->addPage();
$pid4 = $page4['pid'];
$addDecorativeSeparator($pdf, $pid4, 16.0);
$setFont($pdf, 'helvetica', 'B', 16);
$pdf->beginStructElem(role: 'H2', pid: $pid4);
$pdf->addTextCell(
'Page 4 — Table Structure and Figure with Alt-Text',
$pid4,
$leftMargin,
20.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
// ── Table ─────────────────────────────────────────────────────────────
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'P', pid: $pid4);
$pdf->addTextCell(
'The Table role wraps a complete data table. Each row is a TR; header '
. 'cells are TH; data cells are TD. All brackets must be closed '
. 'inside-out before moving to the next row.',
$pid4,
$leftMargin,
32.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
// Column geometry (three columns)
$colX = [$leftMargin, $leftMargin + 55.0, $leftMargin + 120.0];
$colW = [55.0, 65.0, 60.0];
$rowH = 8.0;
$tableY = 52.0;
$tableHeaders = ['Structure Role', 'Category', 'Nesting context'];
$tableHeaderIds = ['th-role', 'th-category', 'th-context'];
$tableRows = [
['H1–H6', 'Heading', 'Document / Sect / Art'],
['P', 'Body', 'Document / Sect / Art'],
['Note, Blockquote', 'Body', 'Document / Sect / Art'],
['L', 'List', 'Document / Sect'],
['LI', 'List item', 'Inside L'],
['Lbl / LBody', 'List parts', 'Inside LI'],
['Table', 'Table', 'Document / Sect'],
['TR', 'Table row', 'Inside Table'],
['TH / TD', 'Table cell', 'Inside TR'],
['Sect / Art / Part', 'Container', 'Document-level'],
['Figure', 'Graphic', 'Document / Sect'],
['Caption', 'Caption', 'After Figure'],
];
$pdf->beginStructElem(role: 'Table', pid: $pid4);
// Header row
$pdf->beginStructElem(role: 'TR', pid: $pid4);
foreach ($tableHeaders as $idx => $hdr) {
$setFont($pdf, 'helvetica', 'B', 9);
$pdf->beginStructElem(role: 'TH', pid: $pid4, alt: null, attr: [
'O' => 'Table',
'ID' => $tableHeaderIds[$idx],
'Scope' => 'Column',
]);
$pdf->addTextCell(
$hdr,
$pid4,
$colX[$idx],
$tableY,
$colW[$idx],
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem(); // TH
}
$pdf->endStructElem(); // TR (header)
// Data rows
foreach ($tableRows as $row) {
$tableY += $rowH;
$pdf->beginStructElem(role: 'TR', pid: $pid4);
foreach ($row as $idx => $cell) {
$setFont($pdf, 'helvetica', '', 9);
$pdf->beginStructElem(role: 'TD', pid: $pid4, alt: null, attr: [
'O' => 'Table',
'Headers' => $tableHeaderIds[$idx],
]);
$pdf->addTextCell(
$cell,
$pid4,
$colX[$idx],
$tableY,
$colW[$idx],
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem(); // TD
}
$pdf->endStructElem(); // TR
}
$pdf->endStructElem(); // Table
// ── Figure with /Alt alternate description ────────────────────────────
$figY = $tableY + 18.0;
$setFont($pdf, 'helvetica', 'B', 13);
$pdf->beginStructElem(role: 'H3', pid: $pid4);
$pdf->addTextCell(
'4.1 — Figure with /Alt Entry',
$pid4,
$leftMargin,
$figY,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Left,
);
$pdf->endStructElem();
$figY += 10.0;
$setFont($pdf, 'helvetica', '', 10);
$pdf->beginStructElem(role: 'P', pid: $pid4);
$pdf->addTextCell(
'For non-text graphics, use addTaggedFigureContent() and pass an /Alt '
. 'description for accessibility. The string is written into the Figure '
. 'structure-element dictionary and read aloud by screen readers when '
. 'the graphical content cannot be represented as plain text.',
$pid4,
$leftMargin,
$figY,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Justify,
);
$pdf->endStructElem();
$figY += 22.0;
// Draw a simple placeholder rectangle for the figure.
$figStyle = [
'all' => [
'lineWidth' => 0.5,
'lineCap' => 'butt',
'lineJoin' => 'miter',
'dashArray' => [],
'dashPhase' => 0,
'lineColor' => '#4a7c59',
'fillColor' => '#d6ead9',
],
];
$pdf->addTaggedFigureContent(
$pdf->graph->getStartTransform() . $pdf->graph->getRect($leftMargin, $figY, 180.0, 22.0, 'DF', $figStyle)
. $pdf->graph->getStopTransform(),
$pid4,
'Horizontal bar chart illustrating the relative nesting depth of each PDF structure role category',
[
$pdf->toPoints($leftMargin),
$pdf->toPoints($page4['height'] - $figY - 22.0),
$pdf->toPoints($leftMargin + 180.0),
$pdf->toPoints($page4['height'] - $figY),
],
);
$pdf->page->addContent($pdf->color->getPdfColor('black'), $pid4);
$setFont($pdf, 'helvetica', 'I', 8);
$pdf->beginStructElem(role: 'Caption', pid: $pid4);
$pdf->addTextCell(
'Figure 2 — Placeholder for a bar chart; /Alt text is written to the Figure struct-element dictionary',
$pid4,
$leftMargin,
$figY + 24.0,
0.0,
0.0,
drawcell: false,
valign: \Com\Tecnick\Pdf\TextVAlign::Top,
halign: \Com\Tecnick\Pdf\TextHAlign::Center,
);
$pdf->endStructElem();
$rawpdf = $pdf->getOutPDFString();
$pdf->renderPDF(rawpdf: $rawpdf);
