diff --git a/docs/changes/1.x/1.5.0.md b/docs/changes/1.x/1.5.0.md index b96865bada..25683768db 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) in [#2806](https://github.com/PHPOffice/PHPWord/pull/2806) + ### 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..1e61ee5615 100644 --- a/docs/usage/template.md +++ b/docs/usage/template.md @@ -103,9 +103,13 @@ The search-pattern model for images can be like: - ``${search-image-pattern:width=[width]:height=[height]:ratio=false}`` Where: - - [width] and [height] can be just numbers or numbers with measure, which supported by Word (cm, mm, in, pt, pc, px, %, em, ex) + - [width] and [height] can be just numbers or numbers with measure, which supported by Word (cm, mm, in, pt, pc, px, %, em, ex). + For SVG the relative measures (px, %, em, ex) might have different results than other images. - [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 @@ -121,13 +125,22 @@ $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 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' => '100mm', 'height' => '100mm', 'ratio' => false) + ) +); ``` ## cloneBlock diff --git a/src/PhpWord/Shared/Converter.php b/src/PhpWord/Shared/Converter.php index 17d2e1a05d..2378dfb078 100644 --- a/src/PhpWord/Shared/Converter.php +++ b/src/PhpWord/Shared/Converter.php @@ -447,10 +447,15 @@ public static function cssToCm($value) * * @param string $value * - * @return float + * @return ?float */ public static function cssToEmu($value) { - return self::pointToEmu(self::cssToPoint($value)); + $point = self::cssToPoint($value); + if ($point === null) { + return null; + } + + return self::pointToEmu($point); } } diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 073393ffc4..53a8df6f9a 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -24,6 +24,7 @@ use PhpOffice\PhpWord\Exception\CopyFileException; use PhpOffice\PhpWord\Exception\CreateTemporaryFileException; use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Shared\Converter; use PhpOffice\PhpWord\Shared\Text; use PhpOffice\PhpWord\Shared\XMLWriter; use PhpOffice\PhpWord\Shared\ZipArchive; @@ -563,11 +564,28 @@ 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; + $actualWidth = is_numeric($actualWidth) ? $actualWidth . 'px' : $actualWidth; + $actualHeight = is_numeric($actualHeight) ? $actualHeight . 'px' : $actualHeight; + } 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,9 +597,11 @@ private function prepareImageAttrs($replaceImage, $varInlineArgs) $imageAttrs = [ 'src' => $imgPath, - 'mime' => image_type_to_mime_type($imageType), + 'mime' => $mime, 'width' => $width, 'height' => $height, + 'originalWidth' => $actualWidth, + 'originalHeight' => $actualHeight, ]; return $imageAttrs; @@ -599,6 +619,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 +695,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 +758,57 @@ 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') { + $width = Converter::cssToEmu($preparedImageAttrs['width']); + $height = Converter::cssToEmu($preparedImageAttrs['height']); + if ($width === null) { + if (preg_match('/^[+-]?([0-9]+\.?[0-9]*)?(em|ex|%)$/i', $preparedImageAttrs['width'], $matches)) { + $size = (float) ($matches[1]); + $unit = $matches[2]; + switch ($unit) { + case 'ex': + $size = $size * 2; + + // no break + case 'em': + $width = $size * 152400; + + break; + case '%': + $width = Converter::cssToEmu($preparedImageAttrs['originalWidth']) * $size; + + break; + } + } else { + $width = Converter::cssToEmu($preparedImageAttrs['originalWidth']); + } + } + if ($height === null) { + if (preg_match('/^[+-]?([0-9]+\.?[0-9]*)?(em|ex|%)$/i', $preparedImageAttrs['height'], $matches)) { + $size = (float) ($matches[1]); + $unit = $matches[2]; + switch ($unit) { + case 'ex': + $size *= 2; + + // no break + case 'em': + $height = $size * 152400; + + break; + case '%': + $height = Converter::cssToEmu($preparedImageAttrs['originalHeight']) * $size; + + break; + } + } else { + $height = Converter::cssToEmu($preparedImageAttrs['originalHeight']); + } + } + $xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}', '{ID}', '{NAME}'], [$rid, (string) $width, (string) $height, $imgIndex, 'graphic'], $svgTpl); + } 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..733de8e0e9 100644 --- a/tests/PhpWordTests/TemplateProcessorTest.php +++ b/tests/PhpWordTests/TemplateProcessorTest.php @@ -859,14 +859,17 @@ 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'; + $imagePng = __DIR__ . '/_files/images/firefox.png'; + $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); @@ -906,7 +909,16 @@ 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::50:true}'); + $section->addText('${Test2}'); + $section->addText('${Test3:size=10cmx7cm:ratio=false}'); + $section->addText('${Test4:size=100mmx70mm:ratio=true}'); + $section->addText('${Test5:4in::true}'); + $section->addText('${Test6:300pt:200pt}'); + $section->addText('${Test7:25pc:}'); + $section->addText('${Test8:50%:50%}'); + $section->addText('${Test9::5ex}'); $objWriter = IOFactory::createWriter($phpWord, 'Word2007'); $objWriter->save($testFileName); self::assertFileExists($testFileName, "Generated file '{$testFileName}' not found!"); @@ -914,9 +926,8 @@ 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('Test0', $imageJpg); + $templateProcessor->setImageValue(['Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6', 'Test7', 'Test8', 'Test9'], [$imageGif, $imagePng, $imageSvg, $imageSvg, $imageSvg, $imageSvg, $imageSvg, $imageSvg, $imageSvg]); $templateProcessor->saveAs($resultFileName); self::assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!"); @@ -928,7 +939,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 not inserted all images.'); } /** 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +