From d54e8c7e162e299015bab26165f75f80413e9933 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 15 Oct 2026 00:49:41 +0200 Subject: [PATCH] Template Processor: Replace all occurrences in setImageValue when no limit is given --- docs/changes/1.x/1.5.0.md | 1 + src/PhpWord/TemplateProcessor.php | 19 +++--- tests/PhpWordTests/TemplateProcessorTest.php | 71 +++++++++++++++++++- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/docs/changes/1.x/1.5.0.md b/docs/changes/1.x/1.5.0.md index e7fa5fb..11d5be7 100644 --- a/docs/changes/1.x/1.5.0.md +++ b/docs/changes/1.x/1.5.0.md @@ -33,6 +33,7 @@ - Reader Word2007: Read the size, position and wrapping of an image, from VML and DrawingML, instead of giving it the size of its pixels by [@dkulyk](https://github.com/dkulyk) in [#2947](https://github.com/PHPOffice/PHPWord/pull/2947) - Writer ODText: Write the language of a text run, which was dropped, take any BCP 47 tag with its script, and use en-US for a document without a language as the Word2007 writer does, instead of fr-FR, zh-CN and hi-IN by [@dkulyk](https://github.com/dkulyk) in [#2944](https://github.com/PHPOffice/PHPWord/pull/2944) - Writer Word2007: Point the page number of a table of contents entry at the bookmark of its title (`PAGEREF _Toc…`), and write the text of a title made of a text run by [@dkulyk](https://github.com/dkulyk) in [#2952](https://github.com/PHPOffice/PHPWord/pull/2952) +- Template Processor: Replace every occurrence of an image variable in `setImageValue` when no limit is given, without adding unused image relations, by [@dualfroz](https://github.com/dualfroz) fixing [#2832](https://github.com/PHPOffice/PHPWord/issues/2832) ### Miscellaneous diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 2c6236c..65e0c38 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -698,18 +698,19 @@ class TemplateProcessor $preparedImageAttrs = $this->prepareImageAttrs($replaceImage, $varInlineArgs); $imgPath = $preparedImageAttrs['src']; - // get image index - $imgIndex = $this->getNextRelationsIndex($partFileName); - $rid = 'rId' . $imgIndex; - - // replace preparations - $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']); - $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']], $imgTpl); - // replace variable $varNameWithArgsFixed = static::ensureMacroCompleted($varNameWithArgs); $matches = []; + // the occurrence may already be gone, replaced together with an identical one if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed, '/') . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) { + // get image index + $imgIndex = $this->getNextRelationsIndex($partFileName); + $rid = 'rId' . $imgIndex; + + // replace preparations + $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']); + $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']], $imgTpl); + $wholeTag = $matches[0]; array_shift($matches); [$openTag, $prefix, , $postfix, $closeTag] = $matches; @@ -718,7 +719,7 @@ class TemplateProcessor $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent, $limit); } - if (++$i >= $limit) { + if (self::MAXIMUM_REPLACEMENTS_DEFAULT !== $limit && ++$i >= $limit) { break; } } diff --git a/tests/PhpWordTests/TemplateProcessorTest.php b/tests/PhpWordTests/TemplateProcessorTest.php index 7f8cd44..7a04e0b 100644 --- a/tests/PhpWordTests/TemplateProcessorTest.php +++ b/tests/PhpWordTests/TemplateProcessorTest.php @@ -1004,12 +1004,77 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase } /** - * Template processor of a document containing the variable ${Image}. + * @covers ::setImageValue */ - private function getImageTemplateProcessor(): TemplateProcessor + public function testSetImageValueReplacesAllOccurrencesByDefault(): void + { + $templateProcessor = $this->getImageTemplateProcessor(['${Image}', '${Image}', 'Signed: ${Image}']); + $templateProcessor->setImageValue('Image', __DIR__ . '/_files/images/earth.jpg'); + + [$mainPartXml, $relationsXml] = $this->saveAndReadMainPart($templateProcessor); + + self::assertStringNotContainsString('${Image}', $mainPartXml); + self::assertStringContainsString('Signed: ', $mainPartXml); + self::assertSame(3, substr_count($mainPartXml, 'getImageTemplateProcessor(['${Image}', '${Image}', 'Signed: ${Image}']); + $templateProcessor->setImageValue('Image', __DIR__ . '/_files/images/earth.jpg', 1); + + [$mainPartXml, $relationsXml] = $this->saveAndReadMainPart($templateProcessor); + + self::assertSame(2, substr_count($mainPartXml, '${Image}')); + self::assertSame(1, substr_count($mainPartXml, 'saveAs($resultFileName); + + $resultZip = new ZipArchive(); + $resultZip->open($resultFileName); + $mainPartXml = (string) $resultZip->getFromName('word/document.xml'); + $relationsXml = (string) $resultZip->getFromName('word/_rels/document.xml.rels'); + $resultZip->close(); + unlink($resultFileName); + + return [$mainPartXml, $relationsXml]; + } + + /** + * Every image relation declared for the main part is used by an image, and every image uses a declared relation. + */ + private static function assertImageRelationsMatchImages(string $mainPartXml, string $relationsXml): void + { + preg_match_all('#addSection()->addText('${Image}'); + $section = $phpWord->addSection(); + foreach ($texts as $text) { + $section->addText($text); + } $testFileName = (string) tempnam(Settings::getTempDir(), 'PHPWordTemplate'); IOFactory::createWriter($phpWord, 'Word2007')->save($testFileName); $templateProcessor = new TemplateProcessor($testFileName);