Skip to content

Commit 58a81f8

Browse files
committed
Feat: SVG Image Type Support + CI
1 parent 0ab0b49 commit 58a81f8

File tree

7 files changed

+469
-0
lines changed

7 files changed

+469
-0
lines changed

docs/changes/1.x/1.5.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
## Enhancements
66

7+
- Word2007 Writer: Support for embedding SVG images by [@prog-klk1](https://github.com/prog-klk1) in [#2790](https://github.com/PHPOffice/PHPWord/pull/2790)
8+
79
### Bug fixes
810

911
- 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)

samples/Sample_47_SVG.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
use PhpOffice\PhpWord\Element\Section;
4+
use PhpOffice\PhpWord\PhpWord;
5+
6+
include_once 'Sample_Header.php';
7+
8+
// New Word document
9+
echo date('H:i:s'), ' Create new PhpWord object', EOL;
10+
$phpWord = new PhpWord();
11+
12+
$section = $phpWord->addSection();
13+
$section->addText('SVG image without any styles:');
14+
$svg = $section->addImage(__DIR__ . '/resources/sample.svg');
15+
16+
printSeparator($section);
17+
18+
$section->addText('SVG image with styles:');
19+
$svg = $section->addImage(
20+
__DIR__ . '/resources/sample.svg',
21+
[
22+
'width' => 200,
23+
'height' => 200,
24+
'align' => 'center',
25+
'wrappingStyle' => PhpOffice\PhpWord\Style\Image::WRAPPING_STYLE_BEHIND,
26+
]
27+
);
28+
29+
function printSeparator(Section $section): void
30+
{
31+
$section->addTextBreak();
32+
$lineStyle = ['weight' => 0.2, 'width' => 150, 'height' => 0, 'align' => 'center'];
33+
$section->addLine($lineStyle);
34+
$section->addTextBreak(2);
35+
}
36+
37+
// Save file
38+
echo write($phpWord, basename(__FILE__, '.php'), $writers);
39+
if (!CLI) {
40+
include_once 'Sample_Footer.php';
41+
}

samples/resources/sample.svg

Lines changed: 96 additions & 0 deletions
Loading

src/PhpWord/Element/Image.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
namespace PhpOffice\PhpWord\Element;
2020

21+
use DOMDocument;
2122
use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
2223
use PhpOffice\PhpWord\Exception\InvalidImageException;
2324
use PhpOffice\PhpWord\Exception\UnsupportedImageTypeException;
@@ -432,6 +433,20 @@ private function checkImage(): void
432433
{
433434
$this->setSourceType();
434435

436+
$ext = strtolower(pathinfo($this->source, PATHINFO_EXTENSION));
437+
if ($ext === 'svg') {
438+
[$actualWidth, $actualHeight] = $this->getSvgDimensions($this->source);
439+
$this->imageType = 'image/svg+xml';
440+
$this->imageExtension = 'svg';
441+
$this->imageFunc = null;
442+
$this->imageQuality = null;
443+
$this->memoryImage = false;
444+
$this->sourceType = self::SOURCE_LOCAL;
445+
$this->setProportionalSize($actualWidth, $actualHeight);
446+
447+
return;
448+
}
449+
435450
// Check image data
436451
if ($this->sourceType == self::SOURCE_ARCHIVE) {
437452
$imageData = $this->getArchiveImageSize($this->source);
@@ -598,4 +613,38 @@ private function setProportionalSize($actualWidth, $actualHeight): void
598613
}
599614
}
600615
}
616+
617+
public function getSvgDimensions(string $file): array
618+
{
619+
$xml = @file_get_contents($file);
620+
if ($xml === false) {
621+
throw new InvalidImageException("Impossible de lire le fichier SVG: $file");
622+
}
623+
libxml_use_internal_errors(true);
624+
$dom = new DOMDocument();
625+
if (!$dom->loadXML($xml)) {
626+
throw new InvalidImageException('SVG invalide ou mal formé');
627+
}
628+
$svg = $dom->documentElement;
629+
630+
$wAttr = round((float) $svg->getAttribute('width'));
631+
$hAttr = round((float) $svg->getAttribute('height'));
632+
633+
$w = (int) filter_var($wAttr, FILTER_SANITIZE_NUMBER_INT);
634+
$h = (int) filter_var($hAttr, FILTER_SANITIZE_NUMBER_INT);
635+
636+
if ($w <= 0 || $h <= 0) {
637+
$vb = $svg->getAttribute('viewBox');
638+
if (preg_match('/^\s*[\d.+-]+[\s,]+[\d.+-]+[\s,]+([\d.+-]+)[\s,]+([\d.+-]+)\s*$/', $vb, $m)) {
639+
$w = (int) round((float) $m[1]);
640+
$h = (int) round((float) $m[2]);
641+
}
642+
}
643+
644+
if ($w <= 0 || $h <= 0) {
645+
throw new InvalidImageException('Impossible de déterminer width/height ou viewBox valides pour le SVG');
646+
}
647+
648+
return [$w, $h];
649+
}
601650
}

src/PhpWord/Writer/Word2007/Element/Image.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ public function write(): void
4242
if (!$element instanceof ImageElement) {
4343
return;
4444
}
45+
$ext = strtolower(pathinfo($element->getSource(), PATHINFO_EXTENSION));
46+
if ($ext === 'svg') {
47+
$this->writeSvgDrawing($xmlWriter, $element);
48+
49+
return;
50+
}
4551

4652
if ($element->isWatermark()) {
4753
$this->writeWatermark($xmlWriter, $element);
@@ -127,4 +133,136 @@ private function writeWatermark(XMLWriter $xmlWriter, ImageElement $element): vo
127133
$xmlWriter->endElement(); // w:p
128134
}
129135
}
136+
137+
private function writeSvgDrawing(XMLWriter $xmlWriter, ImageElement $element): void
138+
{
139+
$rId = $element->getRelationId() + ($element->isInSection() ? 6 : 0);
140+
141+
$style = $element->getStyle();
142+
// dimensions px, fallback sur getSvgDimensions()
143+
$pxW = $style->getWidth() ?: 0;
144+
$pxH = $style->getHeight() ?: 0;
145+
if ($pxW <= 0 || $pxH <= 0) {
146+
[$pxW, $pxH] = $element->getSvgDimensions($element->getSource());
147+
}
148+
$cx = \PhpOffice\PhpWord\Shared\Drawing::pixelsToEmu($pxW);
149+
$cy = \PhpOffice\PhpWord\Shared\Drawing::pixelsToEmu($pxH);
150+
151+
// <w:p> + align
152+
if (!$this->withoutP) {
153+
$xmlWriter->startElement('w:p');
154+
(new ImageStyleWriter($xmlWriter, $style))->writeAlignment();
155+
}
156+
// <w:r>
157+
$xmlWriter->startElement('w:r');
158+
// <w:drawing>
159+
$xmlWriter->startElement('w:drawing');
160+
161+
// <wp:inline> avec déclarations xmlns comme python-docx-oss
162+
$xmlWriter->startElement('wp:inline');
163+
$xmlWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
164+
$xmlWriter->writeAttribute('xmlns:pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture');
165+
$xmlWriter->writeAttribute('xmlns:asvg', 'http://schemas.microsoft.com/office/drawing/2016/SVG/main');
166+
167+
// <wp:extent>
168+
$xmlWriter->startElement('wp:extent');
169+
$xmlWriter->writeAttribute('cx', (string) $cx);
170+
$xmlWriter->writeAttribute('cy', (string) $cy);
171+
$xmlWriter->endElement();
172+
173+
// <wp:docPr>
174+
$xmlWriter->startElement('wp:docPr');
175+
$xmlWriter->writeAttribute('id', '1');
176+
$xmlWriter->writeAttribute('name', 'Picture 1');
177+
$xmlWriter->endElement();
178+
179+
// <wp:cNvGraphicFramePr>
180+
$xmlWriter->startElement('wp:cNvGraphicFramePr');
181+
$xmlWriter->startElement('a:graphicFrameLocks');
182+
$xmlWriter->writeAttribute('noChangeAspect', '1');
183+
$xmlWriter->endElement();
184+
$xmlWriter->endElement();
185+
186+
// <a:graphic>
187+
$xmlWriter->startElement('a:graphic');
188+
// <a:graphicData uri=".../picture">
189+
$xmlWriter->startElement('a:graphicData');
190+
$xmlWriter->writeAttribute(
191+
'uri',
192+
'http://schemas.openxmlformats.org/drawingml/2006/picture'
193+
);
194+
195+
// <pic:pic>
196+
$xmlWriter->startElement('pic:pic');
197+
198+
// <pic:nvPicPr>
199+
$xmlWriter->startElement('pic:nvPicPr');
200+
$xmlWriter->startElement('pic:cNvPr');
201+
$xmlWriter->writeAttribute('id', '0');
202+
$xmlWriter->writeAttribute('name', basename($element->getSource()));
203+
$xmlWriter->endElement();
204+
$xmlWriter->startElement('pic:cNvPicPr');
205+
$xmlWriter->endElement();
206+
$xmlWriter->endElement();
207+
208+
// <pic:blipFill>
209+
$xmlWriter->startElement('pic:blipFill');
210+
$xmlWriter->startElement('a:blip');
211+
// uniquement extLst avec svgBlip
212+
$xmlWriter->startElement('a:extLst');
213+
$xmlWriter->startElement('a:ext');
214+
$xmlWriter->writeAttribute(
215+
'uri',
216+
'{96DAC541-7B7A-43D3-8B79-37D633B846F1}'
217+
);
218+
$xmlWriter->startElement('asvg:svgBlip');
219+
$xmlWriter->writeAttribute(
220+
'r:embed',
221+
'rId' . $rId
222+
);
223+
$xmlWriter->endElement(); // asvg:svgBlip
224+
$xmlWriter->endElement(); // a:ext
225+
$xmlWriter->endElement(); // a:extLst
226+
$xmlWriter->endElement(); // a:blip
227+
228+
// <a:stretch><a:fillRect/>
229+
$xmlWriter->startElement('a:stretch');
230+
$xmlWriter->startElement('a:fillRect');
231+
$xmlWriter->endElement();
232+
$xmlWriter->endElement();
233+
234+
$xmlWriter->endElement(); // pic:blipFill
235+
236+
// <pic:spPr>
237+
$xmlWriter->startElement('pic:spPr');
238+
$xmlWriter->startElement('a:xfrm');
239+
$xmlWriter->startElement('a:off');
240+
$xmlWriter->writeAttribute('x', '0');
241+
$xmlWriter->writeAttribute('y', '0');
242+
$xmlWriter->endElement();
243+
$xmlWriter->startElement('a:ext');
244+
$xmlWriter->writeAttribute('cx', (string) $cx);
245+
$xmlWriter->writeAttribute('cy', (string) $cy);
246+
$xmlWriter->endElement();
247+
$xmlWriter->endElement();
248+
$xmlWriter->startElement('a:prstGeom');
249+
$xmlWriter->writeAttribute('prst', 'rect');
250+
$xmlWriter->endElement();
251+
$xmlWriter->endElement(); // pic:spPr
252+
253+
$xmlWriter->endElement(); // pic:pic
254+
255+
$xmlWriter->endElement(); // a:graphicData
256+
$xmlWriter->endElement(); // a:graphic
257+
258+
$xmlWriter->endElement(); // wp:inline
259+
260+
$xmlWriter->endElement(); // w:drawing
261+
$xmlWriter->endElement(); // w:r
262+
263+
// </w:p>
264+
if (!$this->withoutP) {
265+
$xmlWriter->endElement();
266+
}
267+
}
130268
}

0 commit comments

Comments
 (0)