Skip to content

Commit 524b263

Browse files
committed
Template Processor Support for SVG
Based on PR #2806 by @geo-fret. Fix #2795 (which also requests ico, but geo-fret believes that isn't supported in docx).
1 parent fc77d44 commit 524b263

File tree

4 files changed

+194
-18
lines changed

4 files changed

+194
-18
lines changed

src/PhpWord/Shared/Converter.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,10 +464,12 @@ public static function cssToCm($value)
464464
*
465465
* @param string $value
466466
*
467-
* @return float
467+
* @return ?float
468468
*/
469469
public static function cssToEmu($value)
470470
{
471-
return self::pointToEmu(self::cssToPoint($value));
471+
$point = self::cssToPoint($value);
472+
473+
return ($point === null) ? null : self::pointToEmu($point);
472474
}
473475
}

src/PhpWord/TemplateProcessor.php

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PhpOffice\PhpWord\Exception\CopyFileException;
2525
use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
2626
use PhpOffice\PhpWord\Exception\Exception;
27+
use PhpOffice\PhpWord\Shared\Converter;
2728
use PhpOffice\PhpWord\Shared\Text;
2829
use PhpOffice\PhpWord\Shared\XMLWriter;
2930
use PhpOffice\PhpWord\Shared\ZipArchive;
@@ -570,11 +571,28 @@ private function prepareImageAttrs($replaceImage, $varInlineArgs)
570571
$width = $this->chooseImageDimension($width, $varInlineArgs['width'] ?? null, 115);
571572
$height = $this->chooseImageDimension($height, $varInlineArgs['height'] ?? null, 70);
572573

573-
$imageData = @getimagesize($imgPath);
574-
if (!is_array($imageData)) {
575-
throw new Exception(sprintf('Invalid image: %s', $imgPath));
574+
$mime = mime_content_type($imgPath);
575+
if ($mime !== 'image/svg+xml') {
576+
$imageData = @getimagesize($imgPath);
577+
if (!is_array($imageData)) {
578+
throw new Exception(sprintf('Invalid image: %s', $imgPath));
579+
}
580+
[$actualWidth, $actualHeight, $imageType] = $imageData;
581+
} else {
582+
$content = file_get_contents($imgPath);
583+
if (!$content) {
584+
throw new Exception(sprintf('Invalid image: %s', $imgPath));
585+
}
586+
$svgXml = simplexml_load_string($content);
587+
if (!$svgXml) {
588+
throw new Exception(sprintf('Invalid image: %s', $imgPath));
589+
}
590+
$svgAttributes = $svgXml->attributes();
591+
$actualWidth = $svgAttributes->width;
592+
$actualHeight = $svgAttributes->height;
593+
$actualWidth = is_numeric($actualWidth) ? $actualWidth . 'px' : $actualWidth;
594+
$actualHeight = is_numeric($actualHeight) ? $actualHeight . 'px' : $actualHeight;
576595
}
577-
[$actualWidth, $actualHeight, $imageType] = $imageData;
578596

579597
// fix aspect ratio (by default)
580598
if (null === $ratio && isset($varInlineArgs['ratio'])) {
@@ -586,9 +604,11 @@ private function prepareImageAttrs($replaceImage, $varInlineArgs)
586604

587605
$imageAttrs = [
588606
'src' => $imgPath,
589-
'mime' => image_type_to_mime_type($imageType),
607+
'mime' => $mime,
590608
'width' => $width,
591609
'height' => $height,
610+
'originalWidth' => $actualWidth,
611+
'originalHeight' => $actualHeight,
592612
];
593613

594614
return $imageAttrs;
@@ -606,6 +626,7 @@ private function addImageToRelations($partFileName, $rid, $imgPath, $imageMimeTy
606626
'image/png' => 'png',
607627
'image/bmp' => 'bmp',
608628
'image/gif' => 'gif',
629+
'image/svg+xml' => 'svg',
609630
];
610631

611632
// get image embed name
@@ -681,6 +702,48 @@ public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEM
681702
// define templates
682703
// result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425)
683704
$imgTpl = '<w:pict><v:shape type="#_x0000_t75" style="width:{WIDTH};height:{HEIGHT}" stroked="f" filled="f"><v:imagedata r:id="{RID}" o:title=""/></v:shape></w:pict>';
705+
// use drawing for svg, see https://www.datypic.com/sc/ooxml/e-w_drawing-1.html
706+
$svgTpl = '<w:drawing>
707+
<wp:inline distT="0" distB="0" distL="0" distR="0">
708+
<wp:extent cx="{WIDTH}" cy="{HEIGHT}"/>
709+
<wp:effectExtent l="0" t="0" r="0" b="0"/>
710+
<wp:docPr id="{ID}" name="{NAME}"/>
711+
<wp:cNvGraphicFramePr>
712+
<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
713+
</wp:cNvGraphicFramePr>
714+
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
715+
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
716+
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
717+
<pic:nvPicPr>
718+
<pic:cNvPr id="{ID}" name="{NAME}"/>
719+
<pic:cNvPicPr/>
720+
</pic:nvPicPr>
721+
<pic:blipFill>
722+
<a:blip>
723+
<a:extLst>
724+
<a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">
725+
<asvg:svgBlip xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main" r:embed="{RID}"/>
726+
</a:ext>
727+
</a:extLst>
728+
</a:blip>
729+
<a:stretch>
730+
<a:fillRect/>
731+
</a:stretch>
732+
</pic:blipFill>
733+
<pic:spPr>
734+
<a:xfrm>
735+
<a:off x="0" y="0"/>
736+
<a:ext cx="{WIDTH}" cy="{HEIGHT}"/>
737+
</a:xfrm>
738+
<a:prstGeom prst="rect">
739+
<a:avLst/>
740+
</a:prstGeom>
741+
</pic:spPr>
742+
</pic:pic>
743+
</a:graphicData>
744+
</a:graphic>
745+
</wp:inline>
746+
</w:drawing>';
684747

685748
$i = 0;
686749
foreach ($searchParts as $partFileName => &$partContent) {
@@ -702,7 +765,57 @@ public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEM
702765

703766
// replace preparations
704767
$this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']);
705-
$xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']], $imgTpl);
768+
if ($preparedImageAttrs['mime'] !== 'image/svg+xml') {
769+
$xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}'], [$rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']], $imgTpl);
770+
} else {
771+
$width = Converter::cssToEmu($preparedImageAttrs['width']);
772+
$height = Converter::cssToEmu($preparedImageAttrs['height']);
773+
if ($width === null) {
774+
if (preg_match('/^[+-]?([0-9]+\.?[0-9]*)?(em|ex|%)$/i', $preparedImageAttrs['width'], $matches)) {
775+
$size = (float) ($matches[1]);
776+
$unit = $matches[2];
777+
switch ($unit) {
778+
case 'ex':
779+
$size = $size * 2;
780+
781+
// no break
782+
case 'em':
783+
$width = $size * 152400;
784+
785+
break;
786+
case '%':
787+
$width = Converter::cssToEmu($preparedImageAttrs['originalWidth']) * $size;
788+
789+
break;
790+
}
791+
} else {
792+
$width = Converter::cssToEmu($preparedImageAttrs['originalWidth']);
793+
}
794+
}
795+
if ($height === null) {
796+
if (preg_match('/^[+-]?([0-9]+\.?[0-9]*)?(em|ex|%)$/i', $preparedImageAttrs['height'], $matches)) {
797+
$size = (float) ($matches[1]);
798+
$unit = $matches[2];
799+
switch ($unit) {
800+
case 'ex':
801+
$size *= 2;
802+
803+
// no break
804+
case 'em':
805+
$height = $size * 152400;
806+
807+
break;
808+
case '%':
809+
$height = Converter::cssToEmu($preparedImageAttrs['originalHeight']) * $size;
810+
811+
break;
812+
}
813+
} else {
814+
$height = Converter::cssToEmu($preparedImageAttrs['originalHeight']);
815+
}
816+
}
817+
$xmlImage = str_replace(['{RID}', '{WIDTH}', '{HEIGHT}', '{ID}', '{NAME}'], [$rid, (string) $width, (string) $height, $imgIndex, 'graphic'], $svgTpl);
818+
}
706819

707820
// replace variable
708821
$varNameWithArgsFixed = static::ensureMacroCompleted($varNameWithArgs);

tests/PhpWordTests/TemplateProcessorTest.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -884,14 +884,17 @@ public function testSetCheckboxWithCustomMacro(): void
884884
public function testSetImageValue(): void
885885
{
886886
$templateProcessor = $this->getTemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx');
887-
$imagePath = __DIR__ . '/_files/images/earth.jpg';
887+
$imageJpg = __DIR__ . '/_files/images/earth.jpg';
888+
$imageGif = __DIR__ . '/_files/images/mario.gif';
889+
$imagePng = __DIR__ . '/_files/images/firefox.png';
890+
$imageSvg = __DIR__ . '/_files/images/phpword.svg';
888891

889892
$variablesReplace = [
890-
'headerValue' => function () use ($imagePath) {
891-
return $imagePath;
893+
'headerValue' => function () use ($imageJpg) {
894+
return $imageJpg;
892895
},
893-
'documentContent' => ['path' => $imagePath, 'width' => 500, 'height' => 500],
894-
'footerValue' => ['path' => $imagePath, 'width' => 100, 'height' => 50, 'ratio' => false],
896+
'documentContent' => ['path' => $imageJpg, 'width' => 500, 'height' => 500],
897+
'footerValue' => ['path' => $imageJpg, 'width' => 100, 'height' => 50, 'ratio' => false],
895898
];
896899
$templateProcessor->setImageValue(array_keys($variablesReplace), $variablesReplace);
897900

@@ -931,17 +934,25 @@ public function testSetImageValue(): void
931934
$testFileName = 'images-test-sample.docx';
932935
$phpWord = new PhpWord();
933936
$section = $phpWord->addSection();
934-
$section->addText('${Test:width=100:ratio=true}');
937+
$section->addText('${Test0:width=100:ratio=true}');
938+
$section->addText('${Test1::50:true}');
939+
$section->addText('${Test2}');
940+
$section->addText('${Test3:size=10cmx7cm:ratio=false}');
941+
$section->addText('${Test4:size=100mmx70mm:ratio=true}');
942+
$section->addText('${Test5:4in::true}');
943+
$section->addText('${Test6:300pt:200pt}');
944+
$section->addText('${Test7:25pc:}');
945+
$section->addText('${Test8:50%:50%}');
946+
$section->addText('${Test9::5ex}');
935947
$objWriter = IOFactory::createWriter($phpWord, 'Word2007');
936948
$objWriter->save($testFileName);
937949
self::assertFileExists($testFileName, "Generated file '{$testFileName}' not found!");
938950

939951
$resultFileName = 'images-test-result.docx';
940952
$templateProcessor = new TemplateProcessor($testFileName);
941953
unlink($testFileName);
942-
$templateProcessor->setImageValue('Test', $imagePath);
943-
$templateProcessor->setImageValue('Test1', $imagePath);
944-
$templateProcessor->setImageValue('Test2', $imagePath);
954+
$templateProcessor->setImageValue('Test0', $imageJpg);
955+
$templateProcessor->setImageValue(['Test1', 'Test2', 'Test3', 'Test4', 'Test5', 'Test6', 'Test7', 'Test8', 'Test9'], [$imageGif, $imagePng, $imageSvg, $imageSvg, $imageSvg, $imageSvg, $imageSvg, $imageSvg, $imageSvg]);
945956
$templateProcessor->saveAs($resultFileName);
946957
self::assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!");
947958

@@ -953,7 +964,7 @@ public function testSetImageValue(): void
953964
}
954965
unlink($resultFileName);
955966

956-
self::assertStringNotContainsString('${Test}', $expectedMainPartXml, 'word/document.xml has no image.');
967+
self::assertStringNotContainsString('${Test', $expectedMainPartXml, 'word/document.xml has not inserted all images.');
957968
}
958969

959970
/**
Lines changed: 50 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)