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 @@
+
+
\ 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/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php
index 45f9a53632..ab6d69caf8 100644
--- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php
+++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php
@@ -18,6 +18,8 @@
namespace PhpOffice\PhpWord\Shared\Microsoft;
+use InvalidArgumentException;
+
/**
* Password encoder for microsoft office applications.
*/
@@ -119,6 +121,11 @@ public static function hashPassword($password, $algorithmName = self::ALGORITHM_
// Get the single-byte values by iterating through the Unicode characters of the truncated password.
// For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte.
$passUtf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
+
+ if (!is_string($passUtf8)) {
+ throw new InvalidArgumentException('mb_convert_encoding() failed to convert password to UCS-2LE.');
+ }
+
$byteChars = [];
for ($i = 0; $i < mb_strlen($password); ++$i) {
diff --git a/src/PhpWord/Shared/Text.php b/src/PhpWord/Shared/Text.php
index 90550c0650..747e789aab 100644
--- a/src/PhpWord/Shared/Text.php
+++ b/src/PhpWord/Shared/Text.php
@@ -148,6 +148,9 @@ public static function toUTF8($value = '')
if (null !== $value && !self::isUTF8($value)) {
// PHP8.2 : utf8_encode is deprecated, but mb_convert_encoding always usable
$value = (function_exists('mb_convert_encoding')) ? mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1') : utf8_encode($value);
+ if (is_bool($value)) {
+ $value = null;
+ }
}
return $value;
diff --git a/src/PhpWord/Style/Chart.php b/src/PhpWord/Style/Chart.php
index 6cffc4d5b0..a4da4132cf 100644
--- a/src/PhpWord/Style/Chart.php
+++ b/src/PhpWord/Style/Chart.php
@@ -457,8 +457,7 @@ public function getValueLabelPosition()
* "low" - sets labels are below the graph
* "high" - sets labels above the graph.
*
- * @param string
- * @param mixed $labelPosition
+ * @param string $labelPosition
*/
public function setValueLabelPosition($labelPosition)
{
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..948220f7d2
--- /dev/null
+++ b/tests/PhpWordTests/Element/SvgImageTest.php
@@ -0,0 +1,47 @@
+markTestSkipped('SVG file not found: ' . $svgPath);
+ }
+ }
+
+ public function testAddSvgImageWithoutStyles(): void
+ {
+ $svgPath = __DIR__ . '/../_files/images/sample.svg';
+ $phpWord = new PhpWord();
+ $section = $phpWord->addSection();
+ $image = $section->addImage($svgPath);
+
+ $this->assertEquals($svgPath, $image->getSource());
+ $this->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);
+
+ $this->assertEquals(200, $image->getStyle()->getWidth());
+ $this->assertEquals(200, $image->getStyle()->getHeight());
+ $this->assertEquals(Image::WRAPPING_STYLE_BEHIND, $image->getStyle()->getWrappingStyle());
+ }
+}
diff --git a/tests/PhpWordTests/Writer/Word2007/Element/TOCTest.php b/tests/PhpWordTests/Writer/Word2007/Element/TOCTest.php
index 95e79114aa..d7451ec62d 100644
--- a/tests/PhpWordTests/Writer/Word2007/Element/TOCTest.php
+++ b/tests/PhpWordTests/Writer/Word2007/Element/TOCTest.php
@@ -66,7 +66,7 @@ public function testWriteTitleWithoutpageNumber(): void
//more than one title and random text for create more than one page
for ($i = 1; $i <= 10; ++$i) {
$section->addTitle('Title ' . $i, 1);
- $content = file_get_contents('https://loripsum.net/api/10/long');
+ $content = file_get_contents(dirname(__DIR__, 3) . '/_files/txt/loremipsum.txt');
\PhpOffice\PhpWord\Shared\Html::addHtml($section, $content ? $content : '', false, false);
$section->addPageBreak();
}
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 @@
+
+
\ No newline at end of file
diff --git a/tests/PhpWordTests/_files/txt/loremipsum.txt b/tests/PhpWordTests/_files/txt/loremipsum.txt
new file mode 100644
index 0000000000..de1c5d736a
--- /dev/null
+++ b/tests/PhpWordTests/_files/txt/loremipsum.txt
@@ -0,0 +1,13 @@
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc auctor felis eget tincidunt maximus. Duis imperdiet lacus neque, a blandit lorem faucibus id. Proin eget cursus risus. Sed vel placerat metus. Cras mollis accumsan urna ut vehicula. Vivamus ut metus laoreet, pulvinar tellus ac, mattis massa. Pellentesque semper, urna nec vestibulum gravida, lectus dolor fermentum tellus, sit amet sodales ipsum justo ut massa. Sed at viverra odio. Praesent fringilla mauris nibh, sed tincidunt orci pretium vel. Morbi lacinia tincidunt turpis egestas porta. Suspendisse potenti. Phasellus nulla leo, porttitor nec molestie a, elementum et odio. Nulla ut nibh eget augue egestas volutpat quis at ex. Etiam lacus leo, commodo ac felis ac, euismod aliquet nisl. Cras turpis odio, elementum vitae hendrerit non, facilisis eget velit.
+
+Ut quis imperdiet ante. Nullam vestibulum tellus in erat tristique pellentesque. Etiam massa quam, dictum sed fermentum quis, ullamcorper molestie justo. Morbi tempus vel risus in efficitur. Quisque porta ex ipsum. Donec a massa diam. Ut vulputate ipsum at odio vulputate porttitor. Vivamus non aliquam tellus, eu eleifend tellus. Nam feugiat eu orci vitae laoreet. Vivamus vestibulum ex ut erat tincidunt semper. Proin bibendum libero in ex sagittis faucibus. Nullam pulvinar malesuada lectus. Morbi luctus laoreet quam.
+
+Mauris finibus, nisi in vehicula lacinia, arcu odio imperdiet mauris, commodo mattis ante nulla et risus. Etiam at leo blandit, viverra ipsum eu, congue libero. Morbi porta leo arcu, vel pretium orci iaculis vel. Curabitur sollicitudin, libero eu rutrum aliquet, tellus erat maximus tortor, non aliquam eros sem ut erat. Morbi ut metus mauris. In vel vehicula tellus. Proin pulvinar, sem quis iaculis blandit, arcu ipsum luctus orci, a convallis ex velit sed eros. Donec est leo, sodales a scelerisque eget, pharetra eu nibh. Curabitur vestibulum tellus sed velit fringilla volutpat. In ultricies elementum odio, molestie accumsan est porttitor at. Donec nisl nisl, pellentesque sed diam vitae, maximus faucibus nulla. Nam vitae blandit quam. In vehicula risus et ligula lacinia dictum. In sollicitudin turpis ut pulvinar suscipit. Nulla blandit gravida mi ut volutpat.
+
+Morbi non sapien ac lectus tincidunt bibendum ac nec lacus. Mauris non vehicula velit, eu varius ex. Phasellus finibus magna vel nunc finibus cursus. Nullam consequat aliquet neque, a vestibulum nisl consectetur rhoncus. Nam posuere, elit eget dignissim rutrum, urna mauris vehicula nisl, auctor lobortis lacus risus vitae ex. Maecenas tellus urna, dictum sed feugiat eget, fermentum ut velit. Fusce quis turpis placerat, auctor leo sollicitudin, eleifend turpis. Nulla quis scelerisque ante. Donec maximus eleifend enim, et ornare erat sagittis eu. Phasellus sed tempor turpis. Suspendisse egestas lacus erat, nec congue augue bibendum nec. Sed dolor lectus, fringilla cursus enim in, scelerisque consectetur urna. Suspendisse vel sagittis sapien, vel ultricies tortor. Vivamus sed nisi venenatis, gravida ipsum in, auctor neque. Morbi sit amet mi volutpat augue vehicula tristique.
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam nec dui quis mi placerat elementum. Pellentesque eros mauris, varius et dictum quis, lobortis in elit. Nulla faucibus mauris eu commodo egestas. Donec eu dapibus nisi, quis finibus diam. Cras fermentum congue quam sed luctus. Vestibulum condimentum arcu felis, ut fermentum ex luctus nec. Maecenas sed tempor risus. Aenean posuere imperdiet massa, a condimentum dui cursus non. Morbi ex velit, congue vitae mauris eget, pellentesque luctus erat. Fusce venenatis, nibh sed pharetra ornare, lectus libero ultricies ante, ullamcorper molestie odio nunc in lectus. Quisque mattis non nisl non gravida. Duis vel orci id odio dapibus luctus at euismod velit.
+
+Vestibulum pellentesque eu nulla quis gravida. Quisque cursus justo augue, sit amet dictum nisi ultrices a. Donec finibus vitae velit sed tincidunt. Pellentesque placerat in tellus id aliquet. Praesent lectus nulla, tristique in nisl quis, iaculis consequat urna. Ut at neque vitae orci facilisis placerat. Vestibulum egestas dolor ipsum, ut dictum velit placerat ac. Quisque sit amet tortor mi. Vivamus nibh nunc, congue ut auctor nec, euismod quis lectus. Duis non ex vel nisi porttitor convallis vel in dui. Nulla vehicula lacus sit amet euismod accumsan. Proin pretium libero quis nulla iaculis mollis.
+
+Aliquam maximus quis felis vitae accumsan. Aenean elementum quam dignissim dignissim ornare. Integer libero dolor, sagittis vel diam luctus, feugiat congue nibh. Aliquam erat volutpat. Curabitur id semper turpis. Mauris velit erat, aliquet at metus ac, molestie consequat nunc. Nunc vel tellus elit.
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 7c4e0a3e1b..a773ab4a5e 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -54,7 +54,12 @@ function phpunit10ErrorHandler(int $errno, string $errstr, string $filename, int
function utf8decode(string $value, string $toEncoding = 'ISO-8859-1'): string
{
- return function_exists('mb_convert_encoding') ? mb_convert_encoding($value, $toEncoding, 'UTF-8') : utf8_decode($value);
+ $value = function_exists('mb_convert_encoding') ? mb_convert_encoding($value, $toEncoding, 'UTF-8') : utf8_decode($value);
+ if (is_bool($value)) {
+ $value = '';
+ }
+
+ return $value;
}
if (!method_exists(PHPUnit\Framework\TestCase::class, 'setOutputCallback')) {