From e34604215902f9d234bc527cf1f6c4f5c1f9f963 Mon Sep 17 00:00:00 2001 From: Thomas Frei Date: Tue, 29 Jul 2025 17:13:18 +0200 Subject: [PATCH 1/4] Add svg support for template processor --- docs/changes/1.x/1.5.0.md | 2 + docs/usage/template.md | 2 +- src/PhpWord/TemplateProcessor.php | 74 ++++++++++++++++++-- tests/PhpWordTests/TemplateProcessorTest.php | 18 ++--- tests/PhpWordTests/_files/images/phpword.svg | 50 +++++++++++++ 5 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 tests/PhpWordTests/_files/images/phpword.svg diff --git a/docs/changes/1.x/1.5.0.md b/docs/changes/1.x/1.5.0.md index b96865bada..dbeab0e347 100644 --- a/docs/changes/1.x/1.5.0.md +++ b/docs/changes/1.x/1.5.0.md @@ -4,6 +4,8 @@ ## Enhancements +- Template Processor: Add support for svg images by [@geo-fret](https://github.com/geo-fret) fixing part of [#2795](https://github.com/PHPOffice/PHPWord/issues/2795) + ### Bug fixes - Set writeAttribute return type by [@radarhere](https://github.com/radarhere) fixing [#2204](https://github.com/PHPOffice/PHPWord/issues/2204) in [#2776](https://github.com/PHPOffice/PHPWord/pull/2776) diff --git a/docs/usage/template.md b/docs/usage/template.md index a0c885e75e..240e14574c 100644 --- a/docs/usage/template.md +++ b/docs/usage/template.md @@ -121,7 +121,7 @@ $templateProcessor = new TemplateProcessor('Template.docx'); $templateProcessor->setValue('Name', 'John Doe'); $templateProcessor->setValue(array('City', 'Street'), array('Detroit', '12th Street')); -$templateProcessor->setImageValue('CompanyLogo', 'path/to/company/logo.png'); +$templateProcessor->setImageValue('CompanyLogo', 'path/to/company/logo.svg'); $templateProcessor->setImageValue('UserLogo', array('path' => 'path/to/logo.png', 'width' => 100, 'height' => 100, 'ratio' => false)); $templateProcessor->setImageValue('FeatureImage', function () { // Closure will only be executed if the replacement tag is found in the template diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 073393ffc4..4bf36fe59a 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -563,11 +563,26 @@ private function prepareImageAttrs($replaceImage, $varInlineArgs) $width = $this->chooseImageDimension($width, $varInlineArgs['width'] ?? null, 115); $height = $this->chooseImageDimension($height, $varInlineArgs['height'] ?? null, 70); - $imageData = @getimagesize($imgPath); - if (!is_array($imageData)) { - throw new Exception(sprintf('Invalid image: %s', $imgPath)); + $mime = mime_content_type($imgPath); + if ($mime === 'image/svg+xml') { + $content = file_get_contents($imgPath); + if (!$content) { + throw new Exception(sprintf('Invalid image: %s', $imgPath)); + } + $svgXml = simplexml_load_string($content); + if (!$svgXml) { + throw new Exception(sprintf('Invalid image: %s', $imgPath)); + } + $svgAttributes = $svgXml->attributes(); + $actualWidth = $svgAttributes->width; + $actualHeight = $svgAttributes->height; + } else { + $imageData = @getimagesize($imgPath); + if (!is_array($imageData)) { + throw new Exception(sprintf('Invalid image: %s', $imgPath)); + } + [$actualWidth, $actualHeight] = $imageData; } - [$actualWidth, $actualHeight, $imageType] = $imageData; // fix aspect ratio (by default) if (null === $ratio && isset($varInlineArgs['ratio'])) { @@ -579,7 +594,7 @@ private function prepareImageAttrs($replaceImage, $varInlineArgs) $imageAttrs = [ 'src' => $imgPath, - 'mime' => image_type_to_mime_type($imageType), + 'mime' => $mime, 'width' => $width, 'height' => $height, ]; @@ -599,6 +614,7 @@ private function addImageToRelations($partFileName, $rid, $imgPath, $imageMimeTy 'image/png' => 'png', 'image/bmp' => 'bmp', 'image/gif' => 'gif', + 'image/svg+xml' => 'svg', ]; // get image embed name @@ -674,6 +690,48 @@ public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEM // define templates // result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425) $imgTpl = ''; + // use drawing for svg, see https://www.datypic.com/sc/ooxml/e-w_drawing-1.html + $svgTpl = ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + '; $i = 0; foreach ($searchParts as $partFileName => &$partContent) { @@ -695,7 +753,11 @@ public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEM // replace preparations $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']); - $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']], $imgTpl); + if ($preparedImageAttrs['mime'] === 'image/svg+xml') { + $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}', '{ID}', '{NAME}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height'], $imgIndex, 'graphic'], $imgTpl); + } else { + $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']], $imgTpl); + } // replace variable $varNameWithArgsFixed = static::ensureMacroCompleted($varNameWithArgs); diff --git a/tests/PhpWordTests/TemplateProcessorTest.php b/tests/PhpWordTests/TemplateProcessorTest.php index 8ae4dfa59a..6688b14dad 100644 --- a/tests/PhpWordTests/TemplateProcessorTest.php +++ b/tests/PhpWordTests/TemplateProcessorTest.php @@ -859,14 +859,16 @@ public function testSetCheckboxWithCustomMacro(): void public function testSetImageValue(): void { $templateProcessor = $this->getTemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx'); - $imagePath = __DIR__ . '/_files/images/earth.jpg'; + $imageJpg = __DIR__ . '/_files/images/earth.jpg'; + $imageGif = __DIR__ . '/_files/images/mario.gif'; + $imageSvg = __DIR__ . '/_files/images/phpword.svg'; $variablesReplace = [ - 'headerValue' => function () use ($imagePath) { - return $imagePath; + 'headerValue' => function () use ($imageJpg) { + return $imageJpg; }, - 'documentContent' => ['path' => $imagePath, 'width' => 500, 'height' => 500], - 'footerValue' => ['path' => $imagePath, 'width' => 100, 'height' => 50, 'ratio' => false], + 'documentContent' => ['path' => $imageJpg, 'width' => 500, 'height' => 500], + 'footerValue' => ['path' => $imageJpg, 'width' => 100, 'height' => 50, 'ratio' => false], ]; $templateProcessor->setImageValue(array_keys($variablesReplace), $variablesReplace); @@ -914,9 +916,9 @@ public function testSetImageValue(): void $resultFileName = 'images-test-result.docx'; $templateProcessor = new TemplateProcessor($testFileName); unlink($testFileName); - $templateProcessor->setImageValue('Test', $imagePath); - $templateProcessor->setImageValue('Test1', $imagePath); - $templateProcessor->setImageValue('Test2', $imagePath); + $templateProcessor->setImageValue('Test', $imageJpg); + $templateProcessor->setImageValue('Test1', $imageGif); + $templateProcessor->setImageValue('Test2', $imageSvg); $templateProcessor->saveAs($resultFileName); self::assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!"); diff --git a/tests/PhpWordTests/_files/images/phpword.svg b/tests/PhpWordTests/_files/images/phpword.svg new file mode 100644 index 0000000000..2fbeeb4af0 --- /dev/null +++ b/tests/PhpWordTests/_files/images/phpword.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0d8826d35af1c483f7cb59500036b92dcb30cc7a Mon Sep 17 00:00:00 2001 From: Thomas Frei Date: Tue, 29 Jul 2025 17:19:50 +0200 Subject: [PATCH 2/4] Add pull request to changelog --- docs/changes/1.x/1.5.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/1.x/1.5.0.md b/docs/changes/1.x/1.5.0.md index dbeab0e347..25683768db 100644 --- a/docs/changes/1.x/1.5.0.md +++ b/docs/changes/1.x/1.5.0.md @@ -4,7 +4,7 @@ ## Enhancements -- Template Processor: Add support for svg images by [@geo-fret](https://github.com/geo-fret) fixing part of [#2795](https://github.com/PHPOffice/PHPWord/issues/2795) +- Template Processor: Add support for svg images by [@geo-fret](https://github.com/geo-fret) fixing part of [#2795](https://github.com/PHPOffice/PHPWord/issues/2795) in [#2806](https://github.com/PHPOffice/PHPWord/pull/2806) ### Bug fixes From 181d539529ab9e2660005e941e45e2d2bb1aa87a Mon Sep 17 00:00:00 2001 From: Thomas Frei Date: Wed, 30 Jul 2025 07:27:44 +0200 Subject: [PATCH 3/4] Fix wrong template --- src/PhpWord/TemplateProcessor.php | 2 +- tests/PhpWordTests/TemplateProcessorTest.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 4bf36fe59a..ff64720578 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -754,7 +754,7 @@ public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEM // replace preparations $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']); if ($preparedImageAttrs['mime'] === 'image/svg+xml') { - $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}', '{ID}', '{NAME}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height'], $imgIndex, 'graphic'], $imgTpl); + $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}', '{ID}', '{NAME}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height'], $imgIndex, 'graphic'], $svgTpl); } else { $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']], $imgTpl); } diff --git a/tests/PhpWordTests/TemplateProcessorTest.php b/tests/PhpWordTests/TemplateProcessorTest.php index 6688b14dad..9d02d0bd83 100644 --- a/tests/PhpWordTests/TemplateProcessorTest.php +++ b/tests/PhpWordTests/TemplateProcessorTest.php @@ -908,7 +908,9 @@ public function testSetImageValue(): void $testFileName = 'images-test-sample.docx'; $phpWord = new PhpWord(); $section = $phpWord->addSection(); - $section->addText('${Test:width=100:ratio=true}'); + $section->addText('${Test0:width=100:ratio=true}'); + $section->addText('${Test1:height=50:ratio=true}'); + $section->addText('${Test2:width=10cm:height=7cm:ratio=false}'); $objWriter = IOFactory::createWriter($phpWord, 'Word2007'); $objWriter->save($testFileName); self::assertFileExists($testFileName, "Generated file '{$testFileName}' not found!"); @@ -916,9 +918,7 @@ public function testSetImageValue(): void $resultFileName = 'images-test-result.docx'; $templateProcessor = new TemplateProcessor($testFileName); unlink($testFileName); - $templateProcessor->setImageValue('Test', $imageJpg); - $templateProcessor->setImageValue('Test1', $imageGif); - $templateProcessor->setImageValue('Test2', $imageSvg); + $templateProcessor->setImageValue(['Test0', 'Test1', 'Test2'], [$imageJpg, $imageGif, $imageSvg]); $templateProcessor->saveAs($resultFileName); self::assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!"); From 1e3fc5305ff81e478d0071664a3fe734909e5523 Mon Sep 17 00:00:00 2001 From: Thomas Frei Date: Wed, 30 Jul 2025 08:36:52 +0200 Subject: [PATCH 4/4] Update test to cover more code --- docs/usage/template.md | 12 ++++++++++++ tests/PhpWordTests/TemplateProcessorTest.php | 10 ++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/usage/template.md b/docs/usage/template.md index 240e14574c..54798c1a6a 100644 --- a/docs/usage/template.md +++ b/docs/usage/template.md @@ -106,6 +106,9 @@ Where: - [width] and [height] can be just numbers or numbers with measure, which supported by Word (cm, mm, in, pt, pc, px, %, em, ex) - [ratio] uses only for ``false``, ``-`` or ``f`` to turn off respect aspect ration of image. By default template image size uses as 'container' size. +You can use an array as first argument to replace all search patterns with the same file. If you use an indexed array as second argument, +the first item in the first argument will be replaced by the first item in the second argument. + Example: ``` clean @@ -128,6 +131,15 @@ $templateProcessor->setImageValue('FeatureImage', function () { return array('path' => SlowFeatureImageGenerator::make(), 'width' => 100, 'height' => 100, 'ratio' => false); }); + +// use array to replace multiple values +$templateProcessor->setImageValue( + array('CompanyLogo', 'UserLogo'), + array( + 'path/to/company/logo.svg', + array('path' => 'path/to/logo.png', 'width' => 100, 'height' => 100, 'ratio' => false) + ) +); ``` ## cloneBlock diff --git a/tests/PhpWordTests/TemplateProcessorTest.php b/tests/PhpWordTests/TemplateProcessorTest.php index 9d02d0bd83..fc37884c16 100644 --- a/tests/PhpWordTests/TemplateProcessorTest.php +++ b/tests/PhpWordTests/TemplateProcessorTest.php @@ -861,6 +861,7 @@ public function testSetImageValue(): void $templateProcessor = $this->getTemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx'); $imageJpg = __DIR__ . '/_files/images/earth.jpg'; $imageGif = __DIR__ . '/_files/images/mario.gif'; + $imagePng = __DIR__ . '/_files/images/firefox.png'; $imageSvg = __DIR__ . '/_files/images/phpword.svg'; $variablesReplace = [ @@ -909,8 +910,9 @@ public function testSetImageValue(): void $phpWord = new PhpWord(); $section = $phpWord->addSection(); $section->addText('${Test0:width=100:ratio=true}'); - $section->addText('${Test1:height=50:ratio=true}'); - $section->addText('${Test2:width=10cm:height=7cm:ratio=false}'); + $section->addText('${Test1::50:true}'); + $section->addText('${Test2:size=10cmx7cm:ratio=false}'); + $section->addText('${Test3}'); $objWriter = IOFactory::createWriter($phpWord, 'Word2007'); $objWriter->save($testFileName); self::assertFileExists($testFileName, "Generated file '{$testFileName}' not found!"); @@ -918,7 +920,7 @@ public function testSetImageValue(): void $resultFileName = 'images-test-result.docx'; $templateProcessor = new TemplateProcessor($testFileName); unlink($testFileName); - $templateProcessor->setImageValue(['Test0', 'Test1', 'Test2'], [$imageJpg, $imageGif, $imageSvg]); + $templateProcessor->setImageValue(['Test0', 'Test1', 'Test2', 'Test3'], [$imageJpg, $imageGif, $imageSvg, $imagePng]); $templateProcessor->saveAs($resultFileName); self::assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!"); @@ -930,7 +932,7 @@ public function testSetImageValue(): void } unlink($resultFileName); - self::assertStringNotContainsString('${Test}', $expectedMainPartXml, 'word/document.xml has no image.'); + self::assertStringNotContainsString('${Test', $expectedMainPartXml, 'word/document.xml has no image.'); } /**