From 49b8c1f7b65e15f4852d904bd8f3c7ab74b3b990 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 28 Oct 2026 15:07:01 +0100 Subject: [PATCH 1/3] fix: keep PDF content with large document images inside tables --- inc/simplepdf.class.php | 27 ++++++++++++++++++++++----- tests/Units/SimplePdfTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/inc/simplepdf.class.php b/inc/simplepdf.class.php index b30456f..9eac465 100644 --- a/inc/simplepdf.class.php +++ b/inc/simplepdf.class.php @@ -393,19 +393,25 @@ public function displayText($name, $content = '', $minline = 3, $maxline = 100) // The sanitizer encodes some characters in attributes (e.g. "=" => "=", "+" => "+"), which TCPDF // does not decode: document URLs are no longer recognized and base64 data URIs get corrupted. // Images are therefore resolved here from their decoded "src" and passed to TCPDF as validated data streams. - $content = preg_replace_callback( + // Resolved images are replaced by placeholders until the end of the processing: their (large) base64 data + // would otherwise make the table regex hit "pcre.backtrack_limit" and the whole content would be lost. + $images = []; + $placeholder_token = 'pdfimg' . bin2hex(random_bytes(8)) . '_'; + $content = preg_replace_callback( '/(]*>\s*)?(]*>)(\s*<\/a>)?/i', - function (array $match): string { + function (array $match) use (&$images, $placeholder_token): string { $img_tag = $this->buildPdfImageTag($match[2]); if ($img_tag === null) { return $match[0]; } + $placeholder = $placeholder_token . count($images); + $images[$placeholder] = $img_tag; if ($match[1] !== '' && ($match[3] ?? '') !== '') { // Remove the link wrapping the image - return $img_tag; + return $placeholder; } - return $match[1] . $img_tag . ($match[3] ?? ''); + return $match[1] . $placeholder . ($match[3] ?? ''); }, $content, ); @@ -429,7 +435,18 @@ function (array $match): string { } } - $this->displayInternal(240, 0.5, self::LEFT, $minline * 5, [$formatted_content]); + // Put back the resolved images + $formatted_content = strtr($formatted_content, $images); + + // TCPDF also matches tables with a lazy regex and silently drops the whole content when it fails: + // raise "pcre.backtrack_limit" to fit the content size (large embedded images) while rendering it. + $backtrack_limit = ini_get('pcre.backtrack_limit'); + ini_set('pcre.backtrack_limit', (string) max((int) $backtrack_limit, 2 * strlen($formatted_content))); + try { + $this->displayInternal(240, 0.5, self::LEFT, $minline * 5, [$formatted_content]); + } finally { + ini_set('pcre.backtrack_limit', $backtrack_limit); + } /* Restore */ [$this->cols, $this->colsx, $this->colsw, $this->align, ] = $save; diff --git a/tests/Units/SimplePdfTest.php b/tests/Units/SimplePdfTest.php index ba4d21d..36c36cf 100644 --- a/tests/Units/SimplePdfTest.php +++ b/tests/Units/SimplePdfTest.php @@ -181,6 +181,37 @@ public function testDisplayTextDropsCorruptedBase64Image(): void $this->assertStringNotContainsString('/Subtype /Image', $output); } + /** + * Test a large document image inside a table + * Its base64 data made the table regex hit `pcre.backtrack_limit`, and the whole content vanished from the PDF + */ + public function testDisplayTextEmbedsLargeDocumentImageInTable(): void + { + // ~1 MB PNG: random pixels are not compressible + $image = imagecreatetruecolor(600, 600); + for ($x = 0; $x < 600; $x++) { + for ($y = 0; $y < 600; $y++) { + imagesetpixel($image, $x, $y, random_int(0, 0xFFFFFF)); + } + } + ob_start(); + imagepng($image, null, 0); + $content = ob_get_clean(); + $this->assertGreaterThan(1_000_000, strlen($content)); + + $docid = $this->createDocumentFile('PNG', 'png', $content); + $url = '/front/document.send.php?docid=' . $docid; + + $pdf = new PluginPdfSimplePDF(); + $pdf->newPage(); + $pdf->displayText( + 'Description', + '
', + ); + + $this->assertStringContainsString('/Subtype /Image', $pdf->output()); + } + /** * Create a document whose file is stored in GLPI_DOC_DIR */ From 3fb7a0d153df9784ebd35e277b08249c9cad09ee Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 28 Oct 2026 15:23:27 +0100 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4225c1..3082546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fix warning on the Domains tab of PDF exports - Fix errors when generating a pdf with software license/installation tabs - Incomplete columns when exporting Users or Groups +- Fix content missing from PDF export when a table contains a large image ## [4.1.6] - 2026-10-06 From 826f41119ba86ae02224bd230d1e070def7c6ebb Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 3 Nov 2026 09:25:29 +0100 Subject: [PATCH 3/3] fix: avoid pcre backtrack limit on large pasted base64 images --- inc/simplepdf.class.php | 4 +-- tests/Units/SimplePdfTest.php | 54 ++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/inc/simplepdf.class.php b/inc/simplepdf.class.php index 9eac465..e4d7f95 100644 --- a/inc/simplepdf.class.php +++ b/inc/simplepdf.class.php @@ -464,10 +464,10 @@ function (array $match) use (&$images, $placeholder_token): string { */ private function buildPdfImageTag(string $img_tag): ?string { - if (!preg_match('/\bsrc\s*=\s*(["\'])(.*?)\1/is', $img_tag, $src_match)) { + if (!preg_match('/\bsrc\s*=\s*(?|"([^"]*)"|\'([^\']*)\')/i', $img_tag, $src_match)) { return null; } - $src = html_entity_decode($src_match[2], ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $src = html_entity_decode($src_match[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); $data = null; if (preg_match('/^data:image\/[a-z0-9.+-]+;base64,(.*)$/is', $src, $data_match)) { diff --git a/tests/Units/SimplePdfTest.php b/tests/Units/SimplePdfTest.php index 36c36cf..e93b486 100644 --- a/tests/Units/SimplePdfTest.php +++ b/tests/Units/SimplePdfTest.php @@ -36,6 +36,7 @@ use Document; use Glpi\Tests\DbTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use PluginPdfSimplePDF; use Psr\Log\LogLevel; use ReflectionMethod; @@ -187,16 +188,7 @@ public function testDisplayTextDropsCorruptedBase64Image(): void */ public function testDisplayTextEmbedsLargeDocumentImageInTable(): void { - // ~1 MB PNG: random pixels are not compressible - $image = imagecreatetruecolor(600, 600); - for ($x = 0; $x < 600; $x++) { - for ($y = 0; $y < 600; $y++) { - imagesetpixel($image, $x, $y, random_int(0, 0xFFFFFF)); - } - } - ob_start(); - imagepng($image, null, 0); - $content = ob_get_clean(); + $content = $this->getLargePngContent(); $this->assertGreaterThan(1_000_000, strlen($content)); $docid = $this->createDocumentFile('PNG', 'png', $content); @@ -212,6 +204,31 @@ public function testDisplayTextEmbedsLargeDocumentImageInTable(): void $this->assertStringContainsString('/Subtype /Image', $pdf->output()); } + public static function largeBase64ImageProvider(): iterable + { + yield 'outside a table' => ['

before %s after

']; + yield 'inside a table' => ['
before %s after
']; + } + + /** + * Test a large pasted base64 image, inside and outside a table + * Its data made the `src` and table regexes hit `pcre.backtrack_limit`, and the image or the whole content was lost + */ + #[DataProvider('largeBase64ImageProvider')] + public function testDisplayTextEmbedsLargeBase64Image(string $html): void + { + // ~1.5 MB base64 data + $base64 = base64_encode($this->getLargePngContent()); + $this->assertGreaterThan(1_500_000, strlen($base64)); + + $pdf = new PluginPdfSimplePDF(); + $pdf->newPage(); + $pdf->displayText('Description', sprintf($html, '')); + + // The whole content (including the image) was lost when a regex failed + $this->assertStringContainsString('/Subtype /Image', $pdf->output()); + } + /** * Create a document whose file is stored in GLPI_DOC_DIR */ @@ -262,6 +279,23 @@ private function getValidPngContent(): string return ob_get_clean(); } + /** + * Build a ~1.15 MB uncompressed PNG: random pixels are not compressible + */ + private function getLargePngContent(): string + { + $image = imagecreatetruecolor(620, 620); + for ($x = 0; $x < 620; $x++) { + for ($y = 0; $y < 620; $y++) { + imagesetpixel($image, $x, $y, random_int(0, 0xFFFFFF)); + } + } + ob_start(); + imagepng($image, null, 0); + + return ob_get_clean(); + } + /** * Build a PNG file with a valid RGBA header but undecodable image data */