diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d7584f..1f7b21b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Fix PDF export crash on tickets with tables containing nested-quote styles
+- Fix base64 images (e.g. in ticket tasks) corrupted or missing in PDF export
## [4.2.0] - 2026-10-06
diff --git a/inc/simplepdf.class.php b/inc/simplepdf.class.php
index e68d757..2c914a2 100644
--- a/inc/simplepdf.class.php
+++ b/inc/simplepdf.class.php
@@ -458,75 +458,26 @@ public function displayText($name, $content = '', $minline = 3, $maxline = 100)
$content = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
$content = RichText::getSafeHtml($content);
- preg_match_all(
- '/]*>\s*(]*src=["\']([^"\']*docid=(\d+)[^"\']*)["\'][^>]*>)\s*<\/a>/i',
+ // 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(
+ '/(]*>\s*)?(
]*>)(\s*<\/a>)?/i',
+ function (array $match): string {
+ $img_tag = $this->buildPdfImageTag($match[2]);
+ if ($img_tag === null) {
+ return $match[0];
+ }
+ if ($match[1] !== '' && ($match[3] ?? '') !== '') {
+ // Remove the link wrapping the image
+ return $img_tag;
+ }
+
+ return $match[1] . $img_tag . ($match[3] ?? '');
+ },
$content,
- $matches,
- PREG_SET_ORDER,
);
- if ($matches !== []) {
- $document = new Document();
- foreach ($matches as $match) {
- $full_a_tag = $match[0];
- $full_img_tag = $match[1];
- $docid = (int) $match[3];
-
- if (
- $document->getFromDB($docid)
- && $document->canViewFile()
- && isset($document->fields['filepath'])
- ) {
- $file_path = GLPI_DOC_DIR . '/' . $document->fields['filepath'];
- if (file_exists($file_path)) {
- // Get actual image dimensions from file
- $img_info_actual = @getimagesize($file_path);
- $actual_width = 450; // Default
- $actual_height = 300; // Default
-
- if ($img_info_actual !== false) {
- $actual_width = $img_info_actual[0];
- $actual_height = $img_info_actual[1];
- }
-
- // Extract width and height from the HTML img tag
- $html_width = null;
- $html_height = null;
-
- if (preg_match('/width=["\']?(\d+)["\']?/i', $full_img_tag, $w_match)) {
- $html_width = (int) $w_match[1];
- }
- if (preg_match('/height=["\']?(\d+)["\']?/i', $full_img_tag, $h_match)) {
- $html_height = (int) $h_match[1];
- }
-
- // Calculate display dimensions preserving aspect ratio
- $display_width = $html_width ?? $actual_width;
- $display_height = $html_height ?? $actual_height;
-
- // If only one dimension is specified, calculate the other to preserve ratio
- if ($html_width && !$html_height) {
- $display_height = (int) ($html_width * $actual_height / $actual_width);
- } elseif ($html_height && !$html_width) {
- $display_width = (int) ($html_height * $actual_width / $actual_height);
- }
-
- // Apply maximum width constraint (200 pixels max)
- $max_width = 200;
- if ($display_width > $max_width) {
- $ratio = $max_width / $display_width;
- $display_width = $max_width;
- $display_height = (int) ($display_height * $ratio);
- }
-
- // Replace the original image by a properly resized one + remove the a tag
- $display_image_tag = '
';
- $content = str_replace($full_a_tag, $display_image_tag, $content);
- }
- }
- }
- }
-
// Split content by tables, keeping tables in the result
$segments = preg_split('/(