Skip to content
24 changes: 21 additions & 3 deletions src/PhpWord/Writer/PDF/MPDF.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*/
class MPDF extends AbstractRenderer implements WriterInterface
{
public const SIMULATED_BODY_START = '<!-- simulated body start -->';
public const SIMULATED_BODY_START = '';
private const BODY_TAG = '<body>';

/**
Expand Down Expand Up @@ -104,8 +104,26 @@ public function save(string $filename): void
$pdf->WriteHTML(substr($html, 0, $bodyLocation));
$html = substr($html, $bodyLocation);
}
foreach (explode("\n", $html) as $line) {
$pdf->WriteHTML("$line\n");
$pcreBacktrackLimitString = ini_get('pcre.backtrack_limit');
if (!is_string($pcreBacktrackLimitString)) {
$pcreBacktrackLimitString = '1000000';
}
$pcreBacktrackLimit = (int) $pcreBacktrackLimitString;
$origLimit = $pcreBacktrackLimit;

try {
foreach (explode("\n", $html) as $line) {
$lineLen = strlen($line);
if ($lineLen > $pcreBacktrackLimit) {
$pcreBacktrackLimit = $lineLen + 1;
ini_set('pcre.backtrack_limit', (string) $pcreBacktrackLimit);
}
$pdf->WriteHTML("$line\n");
}
} finally {
if ($pcreBacktrackLimit !== $origLimit) {
ini_set('pcre.backtrack_limit', $pcreBacktrackLimitString);
}
}

// Write to file
Expand Down
104 changes: 102 additions & 2 deletions tests/PhpWordTests/Writer/PDF/MPDFTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
*
* @see https://github.com/PHPOffice/PHPWord
* @see https://github.com/PHPOffice/PHPWord
*
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/

namespace PhpOffice\PhpWordTests\Writer\PDF;
Expand Down Expand Up @@ -99,6 +99,67 @@ public static function cbEditContent(string $html): string
return $html;
}

/**
* Test that a large embedded image does not trigger the pcre.backtrack_limit error (issue #2876).
*
* PHPWord embeds images as base64 data URIs on a single HTML line. When the image is large
* the line exceeds pcre.backtrack_limit and Mpdf refuses the WriteHTML call. The fix must
* handle this gracefully without requiring the caller to raise pcre.backtrack_limit.
*/
public function testLargeImageDoesNotExceedBacktrackLimit(): void
{
if (!extension_loaded('gd')) {
self::markTestSkipped('GD extension required to generate a large test image.');
}

$file = __DIR__ . '/../../_files/mpdf_large_image.pdf';

// Generate a random-noise JPEG (~700-900 KB) that resists JPEG compression.
// The resulting base64 data URI will be a single HTML line > 900 KB,
// which exceeds the default pcre.backtrack_limit of 1 000 000.
$img = imagecreatetruecolor(1600, 1200);
if ($img === false) {
self::markTestSkipped('imagecreatetruecolor() failed.');
}
for ($y = 0; $y < 1200; ++$y) {
for ($x = 0; $x < 1600; ++$x) {
imagesetpixel($img, $x, $y, (int) imagecolorallocate($img, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255)));
}
}
ob_start();
imagejpeg($img, null, 95);
$imageData = (string) ob_get_clean();

$tmpImage = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpword_test_large_2876.jpg';
file_put_contents($tmpImage, $imageData);

$testLimit = 1000000;
$origLimitStr = (string) ini_get('pcre.backtrack_limit');
ini_set('pcre.backtrack_limit', (string) $testLimit);

try {
self::assertGreaterThan($testLimit, strlen(base64_encode($imageData)), 'Test image base64 must exceed pcre.backtrack_limit to exercise the fix');

$phpWord = new PhpWord();
$section = $phpWord->addSection();
$section->addText('Large-image PDF — issue #2876 regression test');
$section->addImage($tmpImage, ['width' => 400, 'height' => 267]);

$writer = new MPDF($phpWord);
$writer->save($file);

self::assertFileExists($file);
self::assertGreaterThan(0, filesize($file));
self::assertSame($testLimit, (int) ini_get('pcre.backtrack_limit'));
} finally {
ini_set('pcre.backtrack_limit', $origLimitStr);
@unlink($tmpImage);
if (file_exists($file)) {
unlink($file);
}
}
}

/**
* Test set/get abstract renderer options.
*/
Expand All @@ -113,4 +174,43 @@ public function testSetGetAbstractRendererOptions(): void
$writer = new PDF(new PhpWord());
self::assertEquals('Arial', $writer->getFont());
}

/**
* Regression test for #2876: large inline Base64 images used to exhaust
* pcre.backtrack_limit when the Mpdf writer split the HTML line-by-line.
*/
public function testWriteLargeEmbeddedImageDoesNotExhaustBacktrackLimit(): void
{
if (!extension_loaded('gd')) {
self::markTestSkipped('GD extension required to generate a test image.');
}

$rendererName = Settings::PDF_RENDERER_MPDF;
$rendererLibraryPath = realpath(PHPWORD_TESTS_BASE_DIR . '/../vendor/mpdf/mpdf');
Settings::setPdfRenderer($rendererName, $rendererLibraryPath); // @phpstan-ignore-line

$phpWord = new PhpWord();
$section = $phpWord->addSection();

$imagePath = tempnam(sys_get_temp_dir(), 'phpword_') . '.png';
$gd = imagecreatetruecolor(100, 100);
if ($gd === false) {
self::markTestSkipped('imagecreatetruecolor() failed.');
}
imagepng($gd, $imagePath);
imagedestroy($gd);

$section->addImage($imagePath);

$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'PDF');
$outputPath = tempnam(sys_get_temp_dir(), 'phpword_') . '.pdf';

$writer->save($outputPath);

self::assertFileExists($outputPath);
self::assertGreaterThan(0, filesize($outputPath));

unlink($imagePath);
unlink($outputPath);
}
}
Loading