diff --git a/docs/changes/1.x/1.5.0.md b/docs/changes/1.x/1.5.0.md index b96865bada..30cbae8707 100644 --- a/docs/changes/1.x/1.5.0.md +++ b/docs/changes/1.x/1.5.0.md @@ -4,6 +4,8 @@ ## Enhancements +- Word2007 Writer: Support for embedding SVG images by [@prog-klk1](https://github.com/prog-klk1) in [#2790](https://github.com/PHPOffice/PHPWord/pull/2790) + ### 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/samples/Sample_47_SVG.php b/samples/Sample_47_SVG.php new file mode 100644 index 0000000000..878e5468ca --- /dev/null +++ b/samples/Sample_47_SVG.php @@ -0,0 +1,41 @@ +addSection(); +$section->addText('SVG image without any styles:'); +$svg = $section->addImage(__DIR__ . '/resources/sample.svg'); + +printSeparator($section); + +$section->addText('SVG image with styles:'); +$svg = $section->addImage( + __DIR__ . '/resources/sample.svg', + [ + 'width' => 200, + 'height' => 200, + 'align' => 'center', + 'wrappingStyle' => PhpOffice\PhpWord\Style\Image::WRAPPING_STYLE_BEHIND, + ] +); + +function printSeparator(Section $section): void +{ + $section->addTextBreak(); + $lineStyle = ['weight' => 0.2, 'width' => 150, 'height' => 0, 'align' => 'center']; + $section->addLine($lineStyle); + $section->addTextBreak(2); +} + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/samples/resources/sample.svg b/samples/resources/sample.svg new file mode 100644 index 0000000000..8a800c4a2c --- /dev/null +++ b/samples/resources/sample.svg @@ -0,0 +1,96 @@ + + + Official PHP Logo + + + + image/svg+xml + + Official PHP Logo + + + Colin Viebrock + + + + + + + + + + + + Copyright Colin Viebrock 1997 - All rights reserved. + + + 1997 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/PhpWord/Element/Image.php b/src/PhpWord/Element/Image.php index c47e5effa5..843f0b078a 100644 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Element; +use DOMDocument; use PhpOffice\PhpWord\Exception\CreateTemporaryFileException; use PhpOffice\PhpWord\Exception\InvalidImageException; use PhpOffice\PhpWord\Exception\UnsupportedImageTypeException; @@ -432,6 +433,20 @@ private function checkImage(): void { $this->setSourceType(); + $ext = strtolower(pathinfo($this->source, PATHINFO_EXTENSION)); + if ($ext === 'svg') { + [$actualWidth, $actualHeight] = $this->getSvgDimensions($this->source); + $this->imageType = 'image/svg+xml'; + $this->imageExtension = 'svg'; + $this->imageFunc = null; + $this->imageQuality = null; + $this->memoryImage = false; + $this->sourceType = self::SOURCE_LOCAL; + $this->setProportionalSize($actualWidth, $actualHeight); + + return; + } + // Check image data if ($this->sourceType == self::SOURCE_ARCHIVE) { $imageData = $this->getArchiveImageSize($this->source); @@ -598,4 +613,38 @@ private function setProportionalSize($actualWidth, $actualHeight): void } } } + + public function getSvgDimensions(string $file): array + { + $xml = @file_get_contents($file); + if ($xml === false) { + throw new InvalidImageException("Impossible de lire le fichier SVG: $file"); + } + libxml_use_internal_errors(true); + $dom = new DOMDocument(); + if (!$dom->loadXML($xml)) { + throw new InvalidImageException('SVG invalide ou mal formé'); + } + $svg = $dom->documentElement; + + $wAttr = round((float) $svg->getAttribute('width')); + $hAttr = round((float) $svg->getAttribute('height')); + + $w = (int) filter_var($wAttr, FILTER_SANITIZE_NUMBER_INT); + $h = (int) filter_var($hAttr, FILTER_SANITIZE_NUMBER_INT); + + if ($w <= 0 || $h <= 0) { + $vb = $svg->getAttribute('viewBox'); + if (preg_match('/^\s*[\d.+-]+[\s,]+[\d.+-]+[\s,]+([\d.+-]+)[\s,]+([\d.+-]+)\s*$/', $vb, $m)) { + $w = (int) round((float) $m[1]); + $h = (int) round((float) $m[2]); + } + } + + if ($w <= 0 || $h <= 0) { + throw new InvalidImageException('Impossible de déterminer width/height ou viewBox valides pour le SVG'); + } + + return [$w, $h]; + } } diff --git a/src/PhpWord/Writer/Word2007/Element/Image.php b/src/PhpWord/Writer/Word2007/Element/Image.php index 7835f32ad5..813f1e7582 100644 --- a/src/PhpWord/Writer/Word2007/Element/Image.php +++ b/src/PhpWord/Writer/Word2007/Element/Image.php @@ -42,6 +42,12 @@ public function write(): void if (!$element instanceof ImageElement) { return; } + $ext = strtolower(pathinfo($element->getSource(), PATHINFO_EXTENSION)); + if ($ext === 'svg') { + $this->writeSvgDrawing($xmlWriter, $element); + + return; + } if ($element->isWatermark()) { $this->writeWatermark($xmlWriter, $element); @@ -127,4 +133,136 @@ private function writeWatermark(XMLWriter $xmlWriter, ImageElement $element): vo $xmlWriter->endElement(); // w:p } } + + private function writeSvgDrawing(XMLWriter $xmlWriter, ImageElement $element): void + { + $rId = $element->getRelationId() + ($element->isInSection() ? 6 : 0); + + $style = $element->getStyle(); + // dimensions px, fallback sur getSvgDimensions() + $pxW = $style->getWidth() ?: 0; + $pxH = $style->getHeight() ?: 0; + if ($pxW <= 0 || $pxH <= 0) { + [$pxW, $pxH] = $element->getSvgDimensions($element->getSource()); + } + $cx = \PhpOffice\PhpWord\Shared\Drawing::pixelsToEmu($pxW); + $cy = \PhpOffice\PhpWord\Shared\Drawing::pixelsToEmu($pxH); + + // + align + if (!$this->withoutP) { + $xmlWriter->startElement('w:p'); + (new ImageStyleWriter($xmlWriter, $style))->writeAlignment(); + } + // + $xmlWriter->startElement('w:r'); + // + $xmlWriter->startElement('w:drawing'); + + // avec déclarations xmlns comme python-docx-oss + $xmlWriter->startElement('wp:inline'); + $xmlWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + $xmlWriter->writeAttribute('xmlns:pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture'); + $xmlWriter->writeAttribute('xmlns:asvg', 'http://schemas.microsoft.com/office/drawing/2016/SVG/main'); + + // + $xmlWriter->startElement('wp:extent'); + $xmlWriter->writeAttribute('cx', (string) $cx); + $xmlWriter->writeAttribute('cy', (string) $cy); + $xmlWriter->endElement(); + + // + $xmlWriter->startElement('wp:docPr'); + $xmlWriter->writeAttribute('id', '1'); + $xmlWriter->writeAttribute('name', 'Picture 1'); + $xmlWriter->endElement(); + + // + $xmlWriter->startElement('wp:cNvGraphicFramePr'); + $xmlWriter->startElement('a:graphicFrameLocks'); + $xmlWriter->writeAttribute('noChangeAspect', '1'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + + // + $xmlWriter->startElement('a:graphic'); + // + $xmlWriter->startElement('a:graphicData'); + $xmlWriter->writeAttribute( + 'uri', + 'http://schemas.openxmlformats.org/drawingml/2006/picture' + ); + + // + $xmlWriter->startElement('pic:pic'); + + // + $xmlWriter->startElement('pic:nvPicPr'); + $xmlWriter->startElement('pic:cNvPr'); + $xmlWriter->writeAttribute('id', '0'); + $xmlWriter->writeAttribute('name', basename($element->getSource())); + $xmlWriter->endElement(); + $xmlWriter->startElement('pic:cNvPicPr'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + + // + $xmlWriter->startElement('pic:blipFill'); + $xmlWriter->startElement('a:blip'); + // uniquement extLst avec svgBlip + $xmlWriter->startElement('a:extLst'); + $xmlWriter->startElement('a:ext'); + $xmlWriter->writeAttribute( + 'uri', + '{96DAC541-7B7A-43D3-8B79-37D633B846F1}' + ); + $xmlWriter->startElement('asvg:svgBlip'); + $xmlWriter->writeAttribute( + 'r:embed', + 'rId' . $rId + ); + $xmlWriter->endElement(); // asvg:svgBlip + $xmlWriter->endElement(); // a:ext + $xmlWriter->endElement(); // a:extLst + $xmlWriter->endElement(); // a:blip + + // + $xmlWriter->startElement('a:stretch'); + $xmlWriter->startElement('a:fillRect'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + + $xmlWriter->endElement(); // pic:blipFill + + // + $xmlWriter->startElement('pic:spPr'); + $xmlWriter->startElement('a:xfrm'); + $xmlWriter->startElement('a:off'); + $xmlWriter->writeAttribute('x', '0'); + $xmlWriter->writeAttribute('y', '0'); + $xmlWriter->endElement(); + $xmlWriter->startElement('a:ext'); + $xmlWriter->writeAttribute('cx', (string) $cx); + $xmlWriter->writeAttribute('cy', (string) $cy); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + $xmlWriter->startElement('a:prstGeom'); + $xmlWriter->writeAttribute('prst', 'rect'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); // pic:spPr + + $xmlWriter->endElement(); // pic:pic + + $xmlWriter->endElement(); // a:graphicData + $xmlWriter->endElement(); // a:graphic + + $xmlWriter->endElement(); // wp:inline + + $xmlWriter->endElement(); // w:drawing + $xmlWriter->endElement(); // w:r + + // + if (!$this->withoutP) { + $xmlWriter->endElement(); + } + } } diff --git a/tests/PhpWordTests/Element/SvgImageTest.php b/tests/PhpWordTests/Element/SvgImageTest.php new file mode 100644 index 0000000000..8873d2c261 --- /dev/null +++ b/tests/PhpWordTests/Element/SvgImageTest.php @@ -0,0 +1,47 @@ +addSection(); + $image = $section->addImage($svgPath); + + self::assertEquals($svgPath, $image->getSource()); + self::assertEquals('image/svg+xml', $image->getImageType()); + } + + public function testAddSvgImageWithStyles(): void + { + $svgPath = __DIR__ . '/../_files/images/sample.svg'; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $options = [ + 'width' => 200, + 'height' => 200, + 'wrappingStyle' => Image::WRAPPING_STYLE_BEHIND, + ]; + + $image = $section->addImage($svgPath, $options); + + self::assertEquals(200, $image->getStyle()->getWidth()); + self::assertEquals(200, $image->getStyle()->getHeight()); + self::assertEquals(Image::WRAPPING_STYLE_BEHIND, $image->getStyle()->getWrappingStyle()); + } +} diff --git a/tests/PhpWordTests/_files/images/sample.svg b/tests/PhpWordTests/_files/images/sample.svg new file mode 100644 index 0000000000..8a800c4a2c --- /dev/null +++ b/tests/PhpWordTests/_files/images/sample.svg @@ -0,0 +1,96 @@ + + + Official PHP Logo + + + + image/svg+xml + + Official PHP Logo + + + Colin Viebrock + + + + + + + + + + + + Copyright Colin Viebrock 1997 - All rights reserved. + + + 1997 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file