Skip to content

Commit 5b4fb77

Browse files
committed
fix: keep large tables in writeHTML() by dropping table image align without a backtracking regex
1 parent 5245af0 commit 5b4fb77

2 files changed

Lines changed: 105 additions & 12 deletions

File tree

‎tcpdf.php‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
//============================================================+
44
// File name : tcpdf.php
5-
// Version : 7.0.15
5+
// Version : 7.0.16
66
// Author : Nicola Asuni - Tecnick.com LTD - www.tecnick.com - info@tecnick.com
77
// License : GNU-LGPL v3 (https://www.gnu.org/copyleft/lesser.html)
88
// Copyright (C): 2002-2026 Nicola Asuni - Tecnick.com LTD
@@ -15,7 +15,7 @@
1515
* See: https://tcpdf.org
1616
* @package com.tecnick.tcpdf
1717
* @author Nicola Asuni
18-
* @version 7.0.15
18+
* @version 7.0.16
1919
*/
2020

2121
// TCPDF configuration
@@ -7653,16 +7653,26 @@ protected function normalizeHtmlMarkup(string $html): string
76537653
// first line box of the table cell, which makes them overlap the
76547654
// row borders; the vertical-align attribute is dropped inside
76557655
// tables so the rows contain their images (legacy parity for the
7656-
// dominant image-only cell case).
7657-
$html = (string) preg_replace_callback(
7658-
'#<table\b.*?</table>#is',
7659-
static fn(array $match): string => (string) preg_replace(
7660-
'/(<img\b[^>]*?)\s+align="(top|middle|bottom|absmiddle|baseline)"/i',
7661-
'$1',
7662-
$match[0] ?? '',
7663-
),
7664-
$html,
7665-
);
7656+
// dominant image-only cell case). The markup is split on table tags
7657+
// (odd indexes) to track the nesting depth in a single linear pass.
7658+
$parts = preg_split('#(</?table\b[^>]*>)#i', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
7659+
if (is_array($parts)) {
7660+
$depth = 0;
7661+
foreach ($parts as $idx => $part) {
7662+
if (($idx % 2) === 1) {
7663+
$depth = ($part[1] ?? '') === '/' ? max(0, $depth - 1) : $depth + 1;
7664+
continue;
7665+
}
7666+
7667+
if ($depth > 0 && stripos($part, '<img') !== false) {
7668+
$parts[$idx] =
7669+
preg_replace('/(<img\b[^>]*?)\s+align="(top|middle|bottom|absmiddle|baseline)"/i', '$1', $part)
7670+
?? $part;
7671+
}
7672+
}
7673+
7674+
$html = implode('', $parts);
7675+
}
76667676

76677677
// Legacy HTML images may carry base64 data with an '@' prefix; the
76687678
// engine expects raw bytes after '@' but decodes data URIs.

‎test/TcpdfHtmlMarkupTest.php‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* HTML markup normalization tests for writeHTML().
7+
*
8+
* @package com.tecnick.tcpdf
9+
*/
10+
11+
require_once __DIR__ . '/TcpdfTestCase.php';
12+
13+
class TcpdfHtmlMarkupTest extends TcpdfTestCase
14+
{
15+
/**
16+
* Run the protected markup normalization on the given HTML.
17+
*/
18+
private function normalize(string $html): string
19+
{
20+
$pdf = new class('P', 'mm', 'A4', true, 'UTF-8', false) extends TCPDF {
21+
public function normalize(string $html): string
22+
{
23+
return $this->normalizeHtmlMarkup($html);
24+
}
25+
};
26+
27+
return $pdf->normalize($html);
28+
}
29+
30+
/**
31+
* The test runs in a separate process: the JIT does not honour the backtrack
32+
* limit, and turning it off only affects patterns compiled afterwards.
33+
* The limits are restored before returning, as PHPUnit itself calls
34+
* preg_replace() after the test method.
35+
*
36+
* @mago-expect lint:no-ini-set
37+
*/
38+
#[\PHPUnit\Framework\Attributes\RunInSeparateProcess]
39+
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
40+
public function testTableLargerThanBacktrackLimitIsKept(): void
41+
{
42+
$html = '<table border="1">';
43+
for ($row = 1; $row <= 200; ++$row) {
44+
$html .= '<tr><td>Row ' . $row . '</td><td><img src="data:," align="middle"></td></tr>';
45+
}
46+
47+
$html .= '</table>';
48+
49+
$jit = (string) ini_get('pcre.jit');
50+
$limit = (string) ini_get('pcre.backtrack_limit');
51+
ini_set('pcre.jit', '0');
52+
ini_set('pcre.backtrack_limit', '1000');
53+
try {
54+
$out = $this->normalize($html);
55+
} finally {
56+
ini_set('pcre.backtrack_limit', $limit);
57+
ini_set('pcre.jit', $jit);
58+
}
59+
60+
$this->assertSame(200, substr_count($out, '<tr>'), 'every table row must be kept');
61+
$this->assertStringContainsString('Row 200</td>', $out);
62+
$this->assertStringEndsWith('</table>', $out);
63+
$this->assertStringNotContainsString('align=', $out);
64+
}
65+
66+
public function testImageAlignIsDroppedOnlyInsideTables(): void
67+
{
68+
$out = $this->normalize(
69+
'<p><img src="data:," align="middle"></p>'
70+
. '<TABLE><tr><td>'
71+
. '<table><tr><td><img src="data:," align="top"></td></tr></table>'
72+
. '<img src="data:," align="bottom">'
73+
. '</td></tr></TABLE>'
74+
. '<img src="data:," align="baseline">'
75+
. '<tablex><img src="data:," align="top"></tablex>',
76+
);
77+
78+
$this->assertStringContainsString('<p><img src="data:," align="middle"></p>', $out);
79+
$this->assertStringContainsString('<td><img src="data:,"></td></tr></table><img src="data:,"></td>', $out);
80+
$this->assertStringContainsString('</TABLE><img src="data:," align="baseline">', $out);
81+
$this->assertStringContainsString('<tablex><img src="data:," align="top"></tablex>', $out);
82+
}
83+
}

0 commit comments

Comments
 (0)