@@ -604,6 +604,16 @@ abstract class HTML extends \Com\Tecnick\Pdf\JavaScript
604604 */
605605 protected string $ullidot = '!';
606606
607+ /**
608+ * Monospaced font applied to <pre>, <code> and <tt> elements when the author
609+ * has not set an explicit font-family. Defaults to the standard-14 Courier;
610+ * set an embedded font with setHTMLMonospaceFont() for PDF/UA output, which
611+ * requires every font to be embedded.
612+ *
613+ * @var string
614+ */
615+ protected string $monospaceFontName = self::FONT_MONO;
616+
607617 /**
608618 * Custom vertical spacing overrides for HTML block tags.
609619 * Structure: [ tagname => [ 0 => ['h' => float, 'n' => int], 1 => ['h' => float, 'n' => int] ] ]
@@ -6126,7 +6136,26 @@ protected function parseHTMLAttributesMonospace(array &$dom, int $key): void
61266136 $node = &$dom[$key];
61276137
61286138 if ($node['value'] === 'pre' || $node['value'] === 'tt' || $node['value'] === 'code') {
6129- $node['fontname'] = self::FONT_MONO;
6139+ // Respect an explicit author font-family; otherwise apply the configured
6140+ // monospace font (Courier by default, or an embedded font for PDF/UA).
6141+ if (!isset($node['style']['font-family']) || $node['style']['font-family'] === '') {
6142+ $node['fontname'] = $this->monospaceFontName;
6143+ }
6144+ }
6145+ }
6146+
6147+ /**
6148+ * Set the monospaced font used for <pre>, <code> and <tt> elements.
6149+ *
6150+ * PDF/UA requires every font to be embedded. The default Courier is a
6151+ * non-embedded standard-14 font, so set an embedded monospace font (after
6152+ * inserting it with the font stack) when producing tagged or accessible PDFs.
6153+ */
6154+ public function setHTMLMonospaceFont(string $fontname): void
6155+ {
6156+ $fontname = \trim($fontname);
6157+ if ($fontname !== '') {
6158+ $this->monospaceFontName = $fontname;
61306159 }
61316160 }
61326161
@@ -7206,7 +7235,19 @@ protected function replayHTMLTableHead(
72067235 }
72077236
72087237 $theadh = $this->measureHTMLCellRenderedHeight($thead, $tpx, $tpy, $tpw, $tph);
7209- $out = $this->getHTMLCell($thead, $tpx, $tpy, $tpw, $tph);
7238+
7239+ // The header is replayed visually at the top of each continuation page, but it
7240+ // is already represented once in the structure tree (on its first page). Suspend
7241+ // tagging during the replay so it does not open a second, nested Table subtree
7242+ // inside the still-open table, then emit the whole replayed header as an Artifact.
7243+ $savedPdfuaMode = $this->suspendPdfUaTagging();
7244+ try {
7245+ $rendered = $this->getHTMLCell($thead, $tpx, $tpy, $tpw, $tph);
7246+ } finally {
7247+ $this->resumePdfUaTagging($savedPdfuaMode);
7248+ }
7249+
7250+ $out = $this->tagPdfUaArtifactContent($rendered);
72107251 if ($theadh > 0.0) {
72117252 $tpy += $theadh;
72127253 $this->resetHTMLLineCursor($hrc, $tpx, $tpw);
@@ -7359,9 +7400,17 @@ protected function measureHTMLCellRenderedHeight(
73597400 $tph = $contenth;
73607401
73617402 $this->initHTMLCellContext($hrc, $contentx, $contenty, $contentw, $contenth);
7362- $this->renderHTMLCellFragments($hrc, $tpx, $tpy, $tpw, $tph, static function (string $fragment): void {
7363- unset($fragment);
7364- });
7403+ // Measurement renders into a discarded buffer, so suspend PDF/UA tagging:
7404+ // otherwise this pass appends phantom structure elements and advances the
7405+ // per-page MCID counter for content that is never written to a page stream.
7406+ $savedPdfuaMode = $this->suspendPdfUaTagging();
7407+ try {
7408+ $this->renderHTMLCellFragments($hrc, $tpx, $tpy, $tpw, $tph, static function (string $fragment): void {
7409+ unset($fragment);
7410+ });
7411+ } finally {
7412+ $this->resumePdfUaTagging($savedPdfuaMode);
7413+ }
73657414
73667415 $this->clearHTMLCellContext($hrc);
73677416 $this->restoreHTMLCallerFontState($callerfont);
@@ -11723,6 +11772,40 @@ protected function getHTMLStructRole(array $elm): string
1172311772 };
1172411773 }
1172511774
11775+ /**
11776+ * Return a trimmed string HTML attribute from a DOM node, or '' when absent.
11777+ *
11778+ * @param array<string, mixed> $elm DOM element.
11779+ */
11780+ protected function getHTMLNodeStringAttr(array $elm, string $name): string
11781+ {
11782+ if (
11783+ !isset($elm['attribute'])
11784+ || !\is_array($elm['attribute'])
11785+ || !isset($elm['attribute'][$name])
11786+ || !\is_string($elm['attribute'][$name])
11787+ ) {
11788+ return '';
11789+ }
11790+
11791+ return \trim($elm['attribute'][$name]);
11792+ }
11793+
11794+ /**
11795+ * Map an HTML th/td scope attribute to a PDF table header Scope value.
11796+ * row/rowgroup map to Row; col/colgroup and any other value (including the
11797+ * absent default) map to Column.
11798+ *
11799+ * @param array<string, mixed> $elm DOM element.
11800+ */
11801+ protected function getHTMLCellScope(array $elm): string
11802+ {
11803+ return match (\strtolower($this->getHTMLNodeStringAttr($elm, 'scope'))) {
11804+ 'row', 'rowgroup' => 'Row',
11805+ default => 'Column',
11806+ };
11807+ }
11808+
1172611809 /**
1172711810 * Return the PDF/UA ListNumbering value for a UL/OL element.
1172811811 *
@@ -17244,7 +17327,7 @@ protected function parseHTMLText(
1724417327 }
1724517328 $hrc['cellctx']['linewrapped'] = true;
1724617329
17247- return $background . $out . $inlineBorder;
17330+ return $this->tagPdfUaArtifactContent($ background) . $out . $this->tagPdfUaArtifactContent($ inlineBorder) ;
1724817331 }
1724917332
1725017333 $tpx = $this->computeHTMLTextAdvanceX(
@@ -17269,7 +17352,7 @@ protected function parseHTMLText(
1726917352 }
1727017353 $hrc['cellctx']['linewrapped'] = false;
1727117354
17272- return $background . $out . $inlineBorder;
17355+ return $this->tagPdfUaArtifactContent($ background) . $out . $this->tagPdfUaArtifactContent($ inlineBorder) ;
1727317356 }
1727417357
1727517358 // FUNCTIONS TO PROCESS HTML OPENING TAGS
@@ -17993,14 +18076,17 @@ protected function parseHTMLTagOPENhr(
1799318076 }
1799418077
1799518078 $lineY = $tpy + ($this->getHTMLLineAdvance($hrc, $key) / 2);
17996- $out .= $this->graph->getLine($tpx, $lineY, $tpx + $width, $lineY, [
18079+ // A horizontal rule carries no semantics, so in PDF/UA mode its stroke is wrapped
18080+ // as an Artifact (tagPdfUaArtifactContent is a no-op outside PDF/UA mode). Without
18081+ // this the rule would be untagged real content, violating PDF/UA-1 7.1.
18082+ $out .= $this->tagPdfUaArtifactContent($this->graph->getLine($tpx, $lineY, $tpx + $width, $lineY, [
1799718083 'lineWidth' => $strokeWidth,
1799818084 'lineCap' => 'butt',
1799918085 'lineJoin' => 'miter',
1800018086 'dashArray' => [],
1800118087 'dashPhase' => 0,
1800218088 'lineColor' => $elm['fgcolor'] === '' ? 'black' : $elm['fgcolor'],
18003- ]);
18089+ ])) ;
1800418090 $this->moveHTMLToNextLine($hrc, $key, $tpx, $tpy, $tpw);
1800518091
1800618092 return $out;
@@ -19448,16 +19534,6 @@ protected function parseHTMLTagOPENtd(
1944819534 return '';
1944919535 }
1945019536
19451- if ($this->pdfuaMode !== '') {
19452- $role = $elm['value'] === 'th' ? 'TH' : 'TD';
19453- $attr = [];
19454- if ($role === 'TH') {
19455- $attr = ['O' => 'Table', 'Scope' => 'Column'];
19456- }
19457-
19458- $this->beginStructElem($role, $this->page->getPageId(), null, $attr);
19459- }
19460-
1946119537 $table = $tableNode;
1946219538
1946319539 $colindex = $this->getHTMLTableNextFreeColumn($table);
@@ -19483,6 +19559,45 @@ protected function parseHTMLTagOPENtd(
1948319559 $colspan = \max(1, \min($remaining, $colspan));
1948419560 $rowspan = \max(1, $rowspan);
1948519561
19562+ if ($this->pdfuaMode !== '') {
19563+ $role = $elm['value'] === 'th' ? 'TH' : 'TD';
19564+ $attr = [];
19565+ if ($role === 'TH') {
19566+ // Honor an explicit scope; header cells default to column scope.
19567+ $attr = ['O' => 'Table', 'Scope' => $this->getHTMLCellScope($elm)];
19568+ }
19569+
19570+ // Record the cell spans on the structure element so assistive technology can
19571+ // reconstruct the table grid. The owner (/O /Table) is required for the span
19572+ // attributes; the serialiser writes ColSpan/RowSpan as integers.
19573+ if ($colspan > 1) {
19574+ $attr['O'] = 'Table';
19575+ $attr['ColSpan'] = (string) $colspan;
19576+ }
19577+
19578+ if ($rowspan > 1) {
19579+ $attr['O'] = 'Table';
19580+ $attr['RowSpan'] = (string) $rowspan;
19581+ }
19582+
19583+ // Pass through explicit header-association markup so complex tables can label
19584+ // header cells with an id and point data cells at them. The struct-tree
19585+ // serialiser promotes these to the StructElem /ID and /Headers entries.
19586+ $cellId = $this->getHTMLNodeStringAttr($elm, 'id');
19587+ if ($cellId !== '') {
19588+ $attr['ID'] = $cellId;
19589+ }
19590+
19591+ $cellHeaders = $this->getHTMLNodeStringAttr($elm, 'headers');
19592+ if ($cellHeaders !== '') {
19593+ $attr['Headers'] = $cellHeaders;
19594+ }
19595+
19596+ // Keep the cell in the structure tree even when empty so the row preserves
19597+ // its full column count and the table matrix stays regular.
19598+ $this->beginStructElem($role, $this->page->getPageId(), null, $attr, true);
19599+ }
19600+
1948619601 $cellx = $this->getHTMLTableColX($table, $colindex);
1948719602 $cellw = $this->getHTMLTableColSpanWidth($table, $colindex, $colspan);
1948819603 $rowtop = $table['rowtop'];
0 commit comments