From ad8c248fac493e603267ca56708f6909e03197c5 Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 28 Jan 2025 11:55:20 +0000 Subject: [PATCH 01/12] Init Powerpoint 2007 reader support for media embed --- src/PhpPresentation/Reader/PowerPoint2007.php | 115 +++++++++++++----- 1 file changed, 84 insertions(+), 31 deletions(-) diff --git a/src/PhpPresentation/Reader/PowerPoint2007.php b/src/PhpPresentation/Reader/PowerPoint2007.php index bb7e17da9..f80f19b20 100644 --- a/src/PhpPresentation/Reader/PowerPoint2007.php +++ b/src/PhpPresentation/Reader/PowerPoint2007.php @@ -33,9 +33,11 @@ use PhpOffice\PhpPresentation\Exception\InvalidFileFormatException; use PhpOffice\PhpPresentation\PhpPresentation; use PhpOffice\PhpPresentation\PresentationProperties; +use PhpOffice\PhpPresentation\Shape\Drawing\AbstractDrawingAdapter; use PhpOffice\PhpPresentation\Shape\Drawing\Base64; use PhpOffice\PhpPresentation\Shape\Drawing\Gd; use PhpOffice\PhpPresentation\Shape\Hyperlink; +use PhpOffice\PhpPresentation\Shape\Media; use PhpOffice\PhpPresentation\Shape\Placeholder; use PhpOffice\PhpPresentation\Shape\RichText; use PhpOffice\PhpPresentation\Shape\RichText\Paragraph; @@ -791,7 +793,11 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr { // Core $document->registerNamespace('asvg', 'http://schemas.microsoft.com/office/drawing/2016/SVG/main'); - if ($document->getElement('p:blipFill/a:blip/a:extLst/a:ext/asvg:svgBlip', $node)) { + $embedNode = $document->getElements("p:nvPicPr/p:nvPr//*[local-name()='media']", $node); + $embedNode = $embedNode ? $embedNode->item(0) : false; + if ($embedNode) { + $oShape = new Media(); + } elseif ($document->getElement('p:blipFill/a:blip/a:extLst/a:ext/asvg:svgBlip', $node)) { $oShape = new Base64(); } else { $oShape = new Gd(); @@ -814,36 +820,10 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr } } - $oElement = $document->getElement('p:blipFill/a:blip', $node); - if ($oElement instanceof DOMElement) { - if ($oElement->hasAttribute('r:embed') && isset($this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target'])) { - $pathImage = 'ppt/slides/' . $this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target']; - $pathImage = explode('/', $pathImage); - foreach ($pathImage as $key => $partPath) { - if ('..' == $partPath) { - unset($pathImage[$key - 1], $pathImage[$key]); - } - } - $pathImage = implode('/', $pathImage); - $imageFile = $this->oZip->getFromName($pathImage); - if (!empty($imageFile)) { - if ($oShape instanceof Gd) { - $info = getimagesizefromstring($imageFile); - if (!$info) { - return; - } - $oShape->setMimeType($info['mime']); - $oShape->setRenderingFunction(str_replace('/', '', $info['mime'])); - $image = @imagecreatefromstring($imageFile); - if (!$image) { - return; - } - $oShape->setImageResource($image); - } elseif ($oShape instanceof Base64) { - $oShape->setData('data:image/svg+xml;base64,' . base64_encode($imageFile)); - } - } - } + if ($oShape instanceof Media) { + $oShape = $this->loadShapeDrawingEmbed($embedNode, $fileRels, $oShape); + } else { + $oShape = $this->loadShapeDrawingImage($document, $oElement, $fileRels, $oShape); } $oElement = $document->getElement('p:spPr', $node); @@ -888,6 +868,79 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr $oSlide->addShape($oShape); } + protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, AbstractDrawingAdapter $oShape): AbstractDrawingAdapter + { + if (!$oElement->hasAttribute('r:embed')) { + return $oShape; + } + if (!isset($this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target'])) { + return $oShape; + } + + $pathEmbed = 'ppt/slides/' . $this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target']; + + $pathEmbed = explode('/', $pathEmbed); + foreach ($pathEmbed as $key => $partPath) { + if ('..' == $partPath) { + unset($pathEmbed[$key - 1], $pathEmbed[$key]); + } + } + $pathEmbed = implode('/', $pathEmbed); + + $tmpEmbed = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderPPT2007Embed'); + + $contentEmbed = $this->oZip->getFromName($pathEmbed); + file_put_contents($tmpEmbed, $contentEmbed); + + $oShape->setPath($tmpEmbed, false); + return $oShape; + } + + protected function loadShapeDrawingImage(XMLReader $document, DOMElement $node, string $fileRels, AbstractDrawingAdapter $oShape) + { + $oElement = $document->getElement('p:blipFill/a:blip', $node); + if (!($oElement instanceof DOMElement)) { + return $oShape; + } + if (!$oElement->hasAttribute('r:embed')) { + return $oShape; + } + if (!isset($this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target'])) { + return $oShape; + } + + $pathImage = 'ppt/slides/' . $this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target']; + $pathImage = explode('/', $pathImage); + foreach ($pathImage as $key => $partPath) { + if ('..' == $partPath) { + unset($pathImage[$key - 1], $pathImage[$key]); + } + } + $pathImage = implode('/', $pathImage); + $imageFile = $this->oZip->getFromName($pathImage); + if (!$imageFile) { + return $oShape; + } + + if ($oShape instanceof Gd) { + $info = getimagesizefromstring($imageFile); + if (!$info) { + return $oShape; + } + $oShape->setMimeType($info['mime']); + $oShape->setRenderingFunction(str_replace('/', '', $info['mime'])); + $image = @imagecreatefromstring($imageFile); + if (!$image) { + return $oShape; + } + $oShape->setImageResource($image); + } elseif ($oShape instanceof Base64) { + $oShape->setData('data:image/svg+xml;base64,' . base64_encode($imageFile)); + } + + return $oShape; + } + /** * Load Shadow for shape or paragraph. */ From 677bbbae5800b268e5b544d5e8463c899f5f7864 Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 28 Jan 2025 13:18:13 +0000 Subject: [PATCH 02/12] Fixed incorrect DOMElement passed in PowerPoint2007::loadShapeDrawingImage signature Fixed embed shape name to use just the filename --- src/PhpPresentation/Reader/PowerPoint2007.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PhpPresentation/Reader/PowerPoint2007.php b/src/PhpPresentation/Reader/PowerPoint2007.php index f80f19b20..9d842e1e5 100644 --- a/src/PhpPresentation/Reader/PowerPoint2007.php +++ b/src/PhpPresentation/Reader/PowerPoint2007.php @@ -823,7 +823,7 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr if ($oShape instanceof Media) { $oShape = $this->loadShapeDrawingEmbed($embedNode, $fileRels, $oShape); } else { - $oShape = $this->loadShapeDrawingImage($document, $oElement, $fileRels, $oShape); + $oShape = $this->loadShapeDrawingImage($document, $node, $fileRels, $oShape); } $oElement = $document->getElement('p:spPr', $node); @@ -877,7 +877,9 @@ protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, return $oShape; } - $pathEmbed = 'ppt/slides/' . $this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target']; + $embedPath = $this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target']; + + $pathEmbed = "ppt/slides/{$embedPath}"; $pathEmbed = explode('/', $pathEmbed); foreach ($pathEmbed as $key => $partPath) { @@ -892,6 +894,7 @@ protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, $contentEmbed = $this->oZip->getFromName($pathEmbed); file_put_contents($tmpEmbed, $contentEmbed); + $oShape->setName(basename($embedPath)); $oShape->setPath($tmpEmbed, false); return $oShape; } From ccbb4b4d86a43820e50684aa1c3c5d4730945448 Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 28 Jan 2025 15:44:19 +0000 Subject: [PATCH 03/12] Added support for keeping the file's name in the shape A presentation may have a label for the file (e.g. Media file 1), which differs from the actual file name (foobar.mp4) --- src/PhpPresentation/Reader/PowerPoint2007.php | 10 +++++--- src/PhpPresentation/Shape/Drawing/File.php | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/PhpPresentation/Reader/PowerPoint2007.php b/src/PhpPresentation/Reader/PowerPoint2007.php index 9d842e1e5..75f34609e 100644 --- a/src/PhpPresentation/Reader/PowerPoint2007.php +++ b/src/PhpPresentation/Reader/PowerPoint2007.php @@ -888,14 +888,18 @@ protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, } } $pathEmbed = implode('/', $pathEmbed); + $contentEmbed = $this->oZip->getFromName($pathEmbed); $tmpEmbed = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderPPT2007Embed'); - $contentEmbed = $this->oZip->getFromName($pathEmbed); file_put_contents($tmpEmbed, $contentEmbed); - $oShape->setName(basename($embedPath)); - $oShape->setPath($tmpEmbed, false); + $fileName = basename($embedPath); + + $oShape + ->setName($fileName) + ->setFileName($fileName) + ->setPath($tmpEmbed, false); return $oShape; } diff --git a/src/PhpPresentation/Shape/Drawing/File.php b/src/PhpPresentation/Shape/Drawing/File.php index 7fd894ac3..d2c7d62e7 100644 --- a/src/PhpPresentation/Shape/Drawing/File.php +++ b/src/PhpPresentation/Shape/Drawing/File.php @@ -30,6 +30,11 @@ class File extends AbstractDrawingAdapter */ protected $path = ''; + /** + * @var string Name of the file + */ + protected $fileName = ''; + /** * Get Path. */ @@ -62,6 +67,24 @@ public function setPath(string $pValue = '', bool $pVerifyFile = true): self return $this; } + /** + * @return string + */ + public function getFileName(): string + { + return $this->fileName; + } + + /** + * @param string $fileName + * @return File + */ + public function setFileName(string $fileName): File + { + $this->fileName = $fileName; + return $this; + } + public function getContents(): string { return CommonFile::fileGetContents($this->getPath()); From 70ace9448b3cf75646b19708cbffc94b2b66e8de Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 28 Jan 2025 15:45:21 +0000 Subject: [PATCH 04/12] Init ODPresentation reader support for media embeds --- src/PhpPresentation/Reader/ODPresentation.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/PhpPresentation/Reader/ODPresentation.php b/src/PhpPresentation/Reader/ODPresentation.php index 7b1a45c29..12bc6ac5a 100644 --- a/src/PhpPresentation/Reader/ODPresentation.php +++ b/src/PhpPresentation/Reader/ODPresentation.php @@ -31,6 +31,7 @@ use PhpOffice\PhpPresentation\PresentationProperties; use PhpOffice\PhpPresentation\Shape\Drawing\Base64; use PhpOffice\PhpPresentation\Shape\Drawing\Gd; +use PhpOffice\PhpPresentation\Shape\Media; use PhpOffice\PhpPresentation\Shape\RichText; use PhpOffice\PhpPresentation\Shape\RichText\Paragraph; use PhpOffice\PhpPresentation\Slide\Background\Color as BackgroundColor; @@ -540,6 +541,11 @@ protected function loadSlide(DOMElement $nodeSlide): bool if ($this->oXMLReader->getElement('draw:text-box', $oNodeFrame)) { $this->loadShapeRichText($oNodeFrame); + continue; + } + if ($this->oXMLReader->getElement('draw:plugin', $oNodeFrame)) { + $this->loadShapeMedia($oNodeFrame); + continue; } } @@ -636,6 +642,56 @@ protected function loadShapeRichText(DOMElement $oNodeFrame): void } } + /** + * Read Shape Media. + */ + protected function loadShapeMedia(DOMElement $oNodeFrame): void + { + $oNodePlugin = $this->oXMLReader->getElement('draw:plugin', $oNodeFrame); + if (!($oNodePlugin instanceof DOMElement)) { + return; + } + + $mediaFile = null; + if ($oNodePlugin->hasAttribute('xlink:href')) { + $filePath = $oNodePlugin->getAttribute('xlink:href'); + $filePathParts = explode('/', $filePath); + if (!$filePathParts || $filePathParts[0] !== 'Media') { + return; + } + + $mediaFile = $this->oZip->getFromName($filePath); + } + + $tmpEmbed = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderODPEmbed'); + file_put_contents($tmpEmbed, $mediaFile); + + $shape = new Media(); + $shape + ->setFileName(basename($filePath)) + ->setPath($tmpEmbed, false); + + $shape->getShadow()->setVisible(false); + $shape->setName($oNodeFrame->hasAttribute('draw:name') ? $oNodeFrame->getAttribute('draw:name') : ''); + $shape->setDescription($oNodeFrame->hasAttribute('draw:name') ? $oNodeFrame->getAttribute('draw:name') : ''); + $shape->setResizeProportional(false); + $shape->setWidth($oNodeFrame->hasAttribute('svg:width') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:width'), 0, -2)) : 0); + $shape->setHeight($oNodeFrame->hasAttribute('svg:height') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:height'), 0, -2)) : 0); + $shape->setResizeProportional(true); + $shape->setOffsetX($oNodeFrame->hasAttribute('svg:x') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:x'), 0, -2)) : 0); + $shape->setOffsetY($oNodeFrame->hasAttribute('svg:y') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:y'), 0, -2)) : 0); + + if ($oNodeFrame->hasAttribute('draw:style-name')) { + $keyStyle = $oNodeFrame->getAttribute('draw:style-name'); + if (isset($this->arrayStyles[$keyStyle])) { + $shape->setShadow($this->arrayStyles[$keyStyle]['shadow']); + $shape->setFill($this->arrayStyles[$keyStyle]['fill']); + } + } + + $this->oPhpPresentation->getActiveSlide()->addShape($shape); + } + /** * Read Paragraph. */ From e51c2cb5db321ca34f69637e523f552c2b5bd29d Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 28 Jan 2025 16:36:39 +0000 Subject: [PATCH 05/12] PHPCS & Static analysis fixes --- src/PhpPresentation/Reader/ODPresentation.php | 5 +++++ src/PhpPresentation/Reader/PowerPoint2007.php | 5 +++-- src/PhpPresentation/Shape/Drawing/File.php | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/PhpPresentation/Reader/ODPresentation.php b/src/PhpPresentation/Reader/ODPresentation.php index 12bc6ac5a..2e274c292 100644 --- a/src/PhpPresentation/Reader/ODPresentation.php +++ b/src/PhpPresentation/Reader/ODPresentation.php @@ -653,8 +653,13 @@ protected function loadShapeMedia(DOMElement $oNodeFrame): void } $mediaFile = null; + $filePath = null; if ($oNodePlugin->hasAttribute('xlink:href')) { $filePath = $oNodePlugin->getAttribute('xlink:href'); + if (!$filePath) { + return; + } + $filePathParts = explode('/', $filePath); if (!$filePathParts || $filePathParts[0] !== 'Media') { return; diff --git a/src/PhpPresentation/Reader/PowerPoint2007.php b/src/PhpPresentation/Reader/PowerPoint2007.php index 75f34609e..89f2d1a03 100644 --- a/src/PhpPresentation/Reader/PowerPoint2007.php +++ b/src/PhpPresentation/Reader/PowerPoint2007.php @@ -868,7 +868,7 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr $oSlide->addShape($oShape); } - protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, AbstractDrawingAdapter $oShape): AbstractDrawingAdapter + protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, Media $oShape): Media { if (!$oElement->hasAttribute('r:embed')) { return $oShape; @@ -900,10 +900,11 @@ protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, ->setName($fileName) ->setFileName($fileName) ->setPath($tmpEmbed, false); + return $oShape; } - protected function loadShapeDrawingImage(XMLReader $document, DOMElement $node, string $fileRels, AbstractDrawingAdapter $oShape) + protected function loadShapeDrawingImage(XMLReader $document, DOMElement $node, string $fileRels, AbstractDrawingAdapter $oShape): AbstractDrawingAdapter { $oElement = $document->getElement('p:blipFill/a:blip', $node); if (!($oElement instanceof DOMElement)) { diff --git a/src/PhpPresentation/Shape/Drawing/File.php b/src/PhpPresentation/Shape/Drawing/File.php index d2c7d62e7..6be3cccb1 100644 --- a/src/PhpPresentation/Shape/Drawing/File.php +++ b/src/PhpPresentation/Shape/Drawing/File.php @@ -79,9 +79,10 @@ public function getFileName(): string * @param string $fileName * @return File */ - public function setFileName(string $fileName): File + public function setFileName(string $fileName): self { $this->fileName = $fileName; + return $this; } From 64719ac6e44157601ce2af356119501a004bf680 Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 28 Jan 2025 16:46:12 +0000 Subject: [PATCH 06/12] PHPCS - removal of File docblocks --- src/PhpPresentation/Shape/Drawing/File.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/PhpPresentation/Shape/Drawing/File.php b/src/PhpPresentation/Shape/Drawing/File.php index 6be3cccb1..196df40f7 100644 --- a/src/PhpPresentation/Shape/Drawing/File.php +++ b/src/PhpPresentation/Shape/Drawing/File.php @@ -67,18 +67,11 @@ public function setPath(string $pValue = '', bool $pVerifyFile = true): self return $this; } - /** - * @return string - */ public function getFileName(): string { return $this->fileName; } - /** - * @param string $fileName - * @return File - */ public function setFileName(string $fileName): self { $this->fileName = $fileName; From 54e51a5a51d5330ee54028486b9dd52c0f20ce4e Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 28 Jan 2025 17:41:58 +0000 Subject: [PATCH 07/12] Fixed negated negative expression always being false --- src/PhpPresentation/Reader/ODPresentation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpPresentation/Reader/ODPresentation.php b/src/PhpPresentation/Reader/ODPresentation.php index 2e274c292..3fe6e6103 100644 --- a/src/PhpPresentation/Reader/ODPresentation.php +++ b/src/PhpPresentation/Reader/ODPresentation.php @@ -661,7 +661,7 @@ protected function loadShapeMedia(DOMElement $oNodeFrame): void } $filePathParts = explode('/', $filePath); - if (!$filePathParts || $filePathParts[0] !== 'Media') { + if ($filePathParts[0] !== 'Media') { return; } From a1a18cf6ba2199bc6e6ee6c48b0c9295e7abefdc Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Fri, 9 May 2025 15:54:03 +0100 Subject: [PATCH 08/12] Changes to load files into temp files instead of memory --- src/PhpPresentation/Reader/PowerPoint2007.php | 13 +++- src/PhpPresentation/Shape/Drawing/Gd.php | 60 +++++++++++++++++-- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/PhpPresentation/Reader/PowerPoint2007.php b/src/PhpPresentation/Reader/PowerPoint2007.php index 89f2d1a03..94663318a 100644 --- a/src/PhpPresentation/Reader/PowerPoint2007.php +++ b/src/PhpPresentation/Reader/PowerPoint2007.php @@ -937,11 +937,18 @@ protected function loadShapeDrawingImage(XMLReader $document, DOMElement $node, } $oShape->setMimeType($info['mime']); $oShape->setRenderingFunction(str_replace('/', '', $info['mime'])); - $image = @imagecreatefromstring($imageFile); - if (!$image) { + if (!@imagecreatefromstring($imageFile)) { return $oShape; } - $oShape->setImageResource($image); + + $tmpEmbed = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderPPT2007ImageGd'); + file_put_contents($tmpEmbed, $imageFile); + + $fileName = basename($pathImage); + + $oShape + ->setName($fileName) + ->setPath($tmpEmbed); } elseif ($oShape instanceof Base64) { $oShape->setData('data:image/svg+xml;base64,' . base64_encode($imageFile)); } diff --git a/src/PhpPresentation/Shape/Drawing/Gd.php b/src/PhpPresentation/Shape/Drawing/Gd.php index bbc278a84..b0fc5384d 100644 --- a/src/PhpPresentation/Shape/Drawing/Gd.php +++ b/src/PhpPresentation/Shape/Drawing/Gd.php @@ -76,10 +76,48 @@ public function __construct() /** * Get image resource. * - * @return resource + * @param bool $isTransient Avoid the image resource being stored in memory to avoid OOM + * @return ?resource */ - public function getImageResource() + public function getImageResource(bool $isTransient = false) { + // Lazy load image resource if not already loaded + if (!$this->imageResource) { + $imageString = file_get_contents($this->getPath()); + if ($imageString === false) { + return null; // Failed to read file + } + + $image = imagecreatefromstring($imageString); + if ($image === false) { + return null; // Failed to create image resource + } + + $this->setImageResource($image); + } + + if ($isTransient) { + // Create a new image resource and copy the original + $width = imagesx($this->imageResource); + $height = imagesy($this->imageResource); + $imageCopy = imagecreatetruecolor($width, $height); + + // Preserve transparency for PNG/GIF images + if (imageistruecolor($this->imageResource)) { + imagealphablending($imageCopy, false); + imagesavealpha($imageCopy, true); + } + + // Copy the image data + imagecopy($imageCopy, $this->imageResource, 0, 0, 0, 0, $width, $height); + + // Destroy the original resource to free memory + imagedestroy($this->imageResource); + $this->imageResource = null; + + return $imageCopy; + } + return $this->imageResource; } @@ -93,14 +131,26 @@ public function getImageResource() public function setImageResource($value = null) { $this->imageResource = $value; + if (!$this->imageResource) { + return $this; + } + + $this->getDimensions(); + + return $this; + } - if (null !== $this->imageResource && false !== $this->imageResource) { - // Get width/height + public function getDimensions(): array + { + // Lazy load dimensions + if (!$this->width) { $this->width = imagesx($this->imageResource); + } + if (!$this->height) { $this->height = imagesy($this->imageResource); } - return $this; + return [$this->width, $this->height]; } /** From 79fada686f1309c07b7b2341de1eab81ed7d118d Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 13 May 2025 17:58:51 +0100 Subject: [PATCH 09/12] Added support for loading images/resources to disk instead of holding them in memory --- .../Shape/Drawing/AbstractDrawingAdapter.php | 34 ++++++++ src/PhpPresentation/Shape/Drawing/File.php | 59 +++++++++++++ src/PhpPresentation/Shape/Drawing/Gd.php | 86 ++++++++++++++++++- .../Slide/Background/Image.php | 24 +++++- 4 files changed, 199 insertions(+), 4 deletions(-) diff --git a/src/PhpPresentation/Shape/Drawing/AbstractDrawingAdapter.php b/src/PhpPresentation/Shape/Drawing/AbstractDrawingAdapter.php index 31aef9e14..8f3192828 100644 --- a/src/PhpPresentation/Shape/Drawing/AbstractDrawingAdapter.php +++ b/src/PhpPresentation/Shape/Drawing/AbstractDrawingAdapter.php @@ -35,7 +35,41 @@ abstract public function getMimeType(): string; abstract public function getPath(): string; /** + * @param string $path File path * @return self */ abstract public function setPath(string $path); + + /** + * Set whether this is a temporary file that should be cleaned up + * + * @param bool $isTemporary + * @return self + */ + abstract public function setIsTemporaryFile(bool $isTemporary); + + /** + * Load content into this object using a temporary file + * + * @param string $content Binary content + * @param string $fileName Optional fileName for reference + * @param string $prefix Prefix for the temporary file + * @return self + */ + public function loadFromContent(string $content, string $fileName = '', string $prefix = 'PhpPresentation'): self + { + $tmpFile = tempnam(sys_get_temp_dir(), $prefix); + file_put_contents($tmpFile, $content); + + // Set path and mark as temporary for automatic cleanup + $this->setPath($tmpFile); + $this->setIsTemporaryFile(true); + + // Set filename if provided + if (!empty($fileName)) { + $this->setName($fileName); + } + + return $this; + } } diff --git a/src/PhpPresentation/Shape/Drawing/File.php b/src/PhpPresentation/Shape/Drawing/File.php index 196df40f7..cbb62d74c 100644 --- a/src/PhpPresentation/Shape/Drawing/File.php +++ b/src/PhpPresentation/Shape/Drawing/File.php @@ -35,6 +35,11 @@ class File extends AbstractDrawingAdapter */ protected $fileName = ''; + /** + * @var bool Flag indicating if this is a temporary file that should be cleaned up + */ + protected $isTemporaryFile = false; + /** * Get Path. */ @@ -67,6 +72,28 @@ public function setPath(string $pValue = '', bool $pVerifyFile = true): self return $this; } + /** + * Set whether this is a temporary file that should be cleaned up + * + * @param bool $isTemporary + * @return self + */ + public function setIsTemporaryFile(bool $isTemporary): self + { + $this->isTemporaryFile = $isTemporary; + return $this; + } + + /** + * Check if this is a temporary file that should be cleaned up + * + * @return bool + */ + public function isTemporaryFile(): bool + { + return $this->isTemporaryFile; + } + public function getFileName(): string { return $this->fileName; @@ -112,4 +139,36 @@ public function getIndexedFilename(): string return $output; } + + /** + * {@inheritDoc} + */ + public function loadFromContent(string $content, string $fileName = '', string $prefix = 'PhpPresentation'): AbstractDrawingAdapter + { + // Create temporary file + $tmpFile = tempnam(sys_get_temp_dir(), $prefix); + file_put_contents($tmpFile, $content); + + // Set path and mark as temporary + $this->setPath($tmpFile); + $this->setIsTemporaryFile(true); + + // Set filename if provided + if (!empty($fileName)) { + $this->setFileName($fileName); + } + + return $this; + } + + /** + * Clean up resources when object is destroyed + */ + public function __destruct() + { + // Remove temporary file if needed + if ($this->isTemporaryFile() && $this->path && file_exists($this->path)) { + @unlink($this->path); + } + } } diff --git a/src/PhpPresentation/Shape/Drawing/Gd.php b/src/PhpPresentation/Shape/Drawing/Gd.php index b0fc5384d..f06589895 100644 --- a/src/PhpPresentation/Shape/Drawing/Gd.php +++ b/src/PhpPresentation/Shape/Drawing/Gd.php @@ -39,7 +39,7 @@ class Gd extends AbstractDrawingAdapter /** * Image resource. * - * @var resource + * @var GdImage|resource|null */ protected $imageResource; @@ -64,6 +64,11 @@ class Gd extends AbstractDrawingAdapter */ protected $uniqueName; + /** + * @var bool Flag indicating if this is a temporary file that should be cleaned up + */ + protected $isTemporaryFile = false; + /** * Gd constructor. */ @@ -77,7 +82,7 @@ public function __construct() * Get image resource. * * @param bool $isTransient Avoid the image resource being stored in memory to avoid OOM - * @return ?resource + * @return ?GdImage|?resource */ public function getImageResource(bool $isTransient = false) { @@ -240,9 +245,86 @@ public function getPath(): string return $this->path; } + /** + * Set Path. + * + * @param string $path File path + * @return self + */ public function setPath(string $path): self { $this->path = $path; + return $this; + } + + /** + * Set whether this is a temporary file that should be cleaned up + * + * @param bool $isTemporary + * @return self + */ + public function setIsTemporaryFile(bool $isTemporary): self + { + $this->isTemporaryFile = $isTemporary; + return $this; + } + + /** + * Check if this is a temporary file that should be cleaned up + * + * @return bool + */ + public function isTemporaryFile(): bool + { + return $this->isTemporaryFile; + } + + /** + * Clean up resources when object is destroyed + */ + public function __destruct() + { + // Free GD image resource if it exists + if ($this->imageResource) { + imagedestroy($this->imageResource); + $this->imageResource = null; + } + + // Remove temporary file if needed + if ($this->isTemporaryFile && !empty($this->path) && file_exists($this->path)) { + @unlink($this->path); + } + } + + /** + * {@inheritDoc} + */ + public function loadFromContent(string $content, string $fileName = '', string $prefix = 'PhpPresentationGd'): AbstractDrawingAdapter + { + $image = @imagecreatefromstring($content); + if ($image === false) { + return $this; + } + + $tmpFile = tempnam(sys_get_temp_dir(), $prefix); + file_put_contents($tmpFile, $content); + + // Set image resource + $this->setImageResource($image); + + // Set path and mark as temporary for automatic cleanup + $this->setPath($tmpFile); + $this->setIsTemporaryFile(true); + + if (!empty($fileName)) { + $this->setName($fileName); + } + + $info = getimagesizefromstring($content); + if (isset($info['mime'])) { + $this->setMimeType($info['mime']); + $this->setRenderingFunction(str_replace('/', '', $info['mime'])); + } return $this; } diff --git a/src/PhpPresentation/Slide/Background/Image.php b/src/PhpPresentation/Slide/Background/Image.php index 093fd005d..0a7de8bde 100644 --- a/src/PhpPresentation/Slide/Background/Image.php +++ b/src/PhpPresentation/Slide/Background/Image.php @@ -21,6 +21,7 @@ namespace PhpOffice\PhpPresentation\Slide\Background; use PhpOffice\PhpPresentation\Exception\FileNotFoundException; +use PhpOffice\PhpPresentation\Shape\Drawing\AbstractDrawingAdapter; use PhpOffice\PhpPresentation\Slide\AbstractBackground; class Image extends AbstractBackground @@ -47,6 +48,11 @@ class Image extends AbstractBackground */ protected $width; + /** + * @var AbstractDrawingAdapter|null + */ + protected $image; + /** * Get Path. */ @@ -63,7 +69,7 @@ public function getPath(): ?string * * @return self */ - public function setPath(string $pValue = '', bool $pVerifyFile = true) + public function setPath(string $pValue = '', bool $pVerifyFile = true): Image { if ($pVerifyFile) { if (!file_exists($pValue)) { @@ -80,6 +86,20 @@ public function setPath(string $pValue = '', bool $pVerifyFile = true) return $this; } + /** + * Set the image using a drawing adapter (keeps a reference to the object to manage file lifecycle) + * + * @param AbstractDrawingAdapter $image Drawing adapter containing image data + * @return self + * @throws FileNotFoundException + */ + public function setImage(AbstractDrawingAdapter $image): self + { + $this->image = $image; + $this->setPath($image->getPath()); + return $this; + } + /** * Get Filename. */ @@ -105,7 +125,7 @@ public function getExtension(): string * * @return string */ - public function getIndexedFilename($numSlide) + public function getIndexedFilename($numSlide): string { return 'background_' . $numSlide . '.' . $this->getExtension(); } From a8cc93a6d970961eb622692cd0da123bedc61208 Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 13 May 2025 18:02:33 +0100 Subject: [PATCH 10/12] PowerPoint2007 - load images/resources to disk instead of loading into memory --- src/PhpPresentation/Reader/PowerPoint2007.php | 71 +++++-------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/src/PhpPresentation/Reader/PowerPoint2007.php b/src/PhpPresentation/Reader/PowerPoint2007.php index 94663318a..d6d437219 100644 --- a/src/PhpPresentation/Reader/PowerPoint2007.php +++ b/src/PhpPresentation/Reader/PowerPoint2007.php @@ -440,7 +440,6 @@ protected function loadSlide(string $sPart, string $baseFile): void $oSlide = $this->oPhpPresentation->createSlide(); $this->oPhpPresentation->setActiveSlideIndex($this->oPhpPresentation->getSlideCount() - 1); $oSlide->setRelsIndex('ppt/slides/_rels/' . $baseFile . '.rels'); - // Background $oElement = $xmlReader->getElement('/p:sld/p:cSld/p:bg/p:bgPr'); if ($oElement instanceof DOMElement) { @@ -482,23 +481,24 @@ protected function loadSlide(string $sPart, string $baseFile): void } $pathImage = implode('/', $pathImage); $contentImg = $this->oZip->getFromName($pathImage); + $fileName = basename($pathImage); + + $tmpFile = new Gd(); + $tmpFile->loadFromContent($contentImg, $fileName); - $tmpBkgImg = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderPpt2007Bkg'); - file_put_contents($tmpBkgImg, $contentImg); // Background $oBackground = new Slide\Background\Image(); - $oBackground->setPath($tmpBkgImg); + $oBackground->setImage($tmpFile); + // Slide Background $oSlide = $this->oPhpPresentation->getActiveSlide(); $oSlide->setBackground($oBackground); } } } - // Shapes $arrayElements = $xmlReader->getElements('/p:sld/p:cSld/p:spTree/*'); $this->loadSlideShapes($oSlide, $arrayElements, $xmlReader); - // Layout $oSlide = $this->oPhpPresentation->getActiveSlide(); foreach ($this->arrayRels['ppt/slides/_rels/' . $baseFile . '.rels'] as $valueRel) { @@ -507,7 +507,6 @@ protected function loadSlide(string $sPart, string $baseFile): void if (array_key_exists($layoutBasename, $this->arraySlideLayouts)) { $oSlide->setSlideLayout($this->arraySlideLayouts[$layoutBasename]); } - break; } } @@ -555,7 +554,6 @@ protected function loadMasterSlide(string $sPart, string $baseFile): void continue; } $oRTParagraph = new Paragraph(); - if ('a:defPPr' == $oElementLvl->nodeName) { $level = 0; } else { @@ -563,7 +561,6 @@ protected function loadMasterSlide(string $sPart, string $baseFile): void $level = str_replace('pPr', '', $level); $level = (int) $level; } - if ($oElementLvl->hasAttribute('algn')) { $oRTParagraph->getAlignment()->setHorizontal($oElementLvl->getAttribute('algn')); } @@ -602,7 +599,6 @@ protected function loadMasterSlide(string $sPart, string $baseFile): void $oRTParagraph->getFont()->setColor($oSchemeColor); } } - switch ($oElementTxStyle->nodeName) { case 'p:bodyStyle': $oSlideMaster->getTextStyles()->setBodyStyleAtLvl($oRTParagraph, $level); @@ -627,7 +623,6 @@ protected function loadMasterSlide(string $sPart, string $baseFile): void if (false !== $pptTheme) { $this->loadTheme($pptTheme, $oSlideMaster); } - break; } } @@ -692,7 +687,6 @@ protected function loadLayoutSlide(string $sPart, string $baseFile, SlideMaster return $oSlideLayout; } - // @phpstan-ignore-next-line return null; } @@ -764,12 +758,15 @@ protected function loadSlideBackground(XMLReader $xmlReader, DOMElement $oElemen } $pathImage = implode('/', $pathImage); $contentImg = $this->oZip->getFromName($pathImage); + $fileName = basename($pathImage); + + $tmpFile = new Gd(); + $tmpFile->loadFromContent($contentImg, $fileName); - $tmpBkgImg = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderPpt2007Bkg'); - file_put_contents($tmpBkgImg, $contentImg); // Background $oBackground = new Slide\Background\Image(); - $oBackground->setPath($tmpBkgImg); + $oBackground->setImage($tmpFile); + // Slide Background $oSlide->setBackground($oBackground); } @@ -783,7 +780,6 @@ protected function loadSlideNote(string $baseFile, Slide $oSlide): void // @phpstan-ignore-next-line if ($xmlReader->getDomFromString($sPart)) { $oNote = $oSlide->getNote(); - $arrayElements = $xmlReader->getElements('/p:notes/p:cSld/p:spTree/*'); $this->loadSlideShapes($oNote, $arrayElements, $xmlReader); } @@ -805,12 +801,10 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr $oShape->getShadow()->setVisible(false); // Variables $fileRels = $oSlide->getRelsIndex(); - $oElement = $document->getElement('p:nvPicPr/p:cNvPr', $node); if ($oElement instanceof DOMElement) { $oShape->setName($oElement->hasAttribute('name') ? $oElement->getAttribute('name') : ''); $oShape->setDescription($oElement->hasAttribute('descr') ? $oElement->getAttribute('descr') : ''); - // Hyperlink $oElementHlinkClick = $document->getElement('a:hlinkClick', $oElement); if (is_object($oElementHlinkClick)) { @@ -819,26 +813,22 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr ); } } - if ($oShape instanceof Media) { $oShape = $this->loadShapeDrawingEmbed($embedNode, $fileRels, $oShape); } else { $oShape = $this->loadShapeDrawingImage($document, $node, $fileRels, $oShape); } - $oElement = $document->getElement('p:spPr', $node); if ($oElement instanceof DOMElement) { $oFill = $this->loadStyleFill($document, $oElement); $oShape->setFill($oFill); } - $oElement = $document->getElement('p:spPr/a:xfrm', $node); if ($oElement instanceof DOMElement) { if ($oElement->hasAttribute('rot')) { $oShape->setRotation((int) CommonDrawing::angleToDegrees((int) $oElement->getAttribute('rot'))); } } - $oElement = $document->getElement('p:spPr/a:xfrm/a:off', $node); if ($oElement instanceof DOMElement) { if ($oElement->hasAttribute('x')) { @@ -848,7 +838,6 @@ protected function loadShapeDrawing(XMLReader $document, DOMElement $node, Abstr $oShape->setOffsetY(CommonDrawing::emuToPixels((int) $oElement->getAttribute('y'))); } } - $oElement = $document->getElement('p:spPr/a:xfrm/a:ext', $node); if ($oElement instanceof DOMElement) { if ($oElement->hasAttribute('cx')) { @@ -878,7 +867,6 @@ protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, } $embedPath = $this->arrayRels[$fileRels][$oElement->getAttribute('r:embed')]['Target']; - $pathEmbed = "ppt/slides/{$embedPath}"; $pathEmbed = explode('/', $pathEmbed); @@ -889,17 +877,9 @@ protected function loadShapeDrawingEmbed(DOMElement $oElement, string $fileRels, } $pathEmbed = implode('/', $pathEmbed); $contentEmbed = $this->oZip->getFromName($pathEmbed); - - $tmpEmbed = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderPPT2007Embed'); - - file_put_contents($tmpEmbed, $contentEmbed); - $fileName = basename($embedPath); - $oShape - ->setName($fileName) - ->setFileName($fileName) - ->setPath($tmpEmbed, false); + $oShape->loadFromContent($contentEmbed, $fileName); return $oShape; } @@ -926,29 +906,14 @@ protected function loadShapeDrawingImage(XMLReader $document, DOMElement $node, } $pathImage = implode('/', $pathImage); $imageFile = $this->oZip->getFromName($pathImage); + $fileName = basename($pathImage); + if (!$imageFile) { return $oShape; } if ($oShape instanceof Gd) { - $info = getimagesizefromstring($imageFile); - if (!$info) { - return $oShape; - } - $oShape->setMimeType($info['mime']); - $oShape->setRenderingFunction(str_replace('/', '', $info['mime'])); - if (!@imagecreatefromstring($imageFile)) { - return $oShape; - } - - $tmpEmbed = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderPPT2007ImageGd'); - file_put_contents($tmpEmbed, $imageFile); - - $fileName = basename($pathImage); - - $oShape - ->setName($fileName) - ->setPath($tmpEmbed); + $oShape->loadFromContent($imageFile, $fileName); } elseif ($oShape instanceof Base64) { $oShape->setData('data:image/svg+xml;base64,' . base64_encode($imageFile)); } @@ -1106,7 +1071,6 @@ protected function loadShapeRichText(XMLReader $document, DOMElement $node, $oSl protected function loadShapeTable(XMLReader $document, DOMElement $node, AbstractSlide $oSlide): void { $this->fileRels = $oSlide->getRelsIndex(); - $oShape = $oSlide->createTableShape(); $oElement = $document->getElement('p:cNvPr', $node); @@ -1331,6 +1295,7 @@ protected function loadParagraph(XMLReader $document, DOMElement $oElement, $oSh $oParagraph->getBulletStyle()->setBulletColor($oColor); } } + $arraySubElements = $document->getElements('(a:r|a:br)', $oElement); foreach ($arraySubElements as $oSubElement) { if (!($oSubElement instanceof DOMElement)) { @@ -1343,7 +1308,6 @@ protected function loadParagraph(XMLReader $document, DOMElement $oElement, $oSh $oElementrPr = $document->getElement('a:rPr', $oSubElement); if (is_object($oElementrPr)) { $oText = $oParagraph->createTextRun(); - if ($oElementrPr->hasAttribute('b')) { $att = $oElementrPr->getAttribute('b'); $oText->getFont()->setBold('true' == $att || '1' == $att ? true : false); @@ -1420,7 +1384,6 @@ protected function loadParagraph(XMLReader $document, DOMElement $oElement, $oSh $oText->getFont()->setCharset((int) $oElementFont->getAttribute('charset')); } } - $oSubSubElement = $document->getElement('a:t', $oSubElement); $oText->setText($oSubSubElement->nodeValue); } From 58ec68a13bbf314b7730d352a1822de927ae17b8 Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Tue, 13 May 2025 18:02:43 +0100 Subject: [PATCH 11/12] ODPresentation - load images/resources to disk instead of loading into memory --- src/PhpPresentation/Reader/ODPresentation.php | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/PhpPresentation/Reader/ODPresentation.php b/src/PhpPresentation/Reader/ODPresentation.php index 3fe6e6103..896a9a02f 100644 --- a/src/PhpPresentation/Reader/ODPresentation.php +++ b/src/PhpPresentation/Reader/ODPresentation.php @@ -270,12 +270,14 @@ protected function loadStyle(DOMElement $nodeStyle): bool if ('bitmap' == $nodeDrawingPageProps->getAttribute('draw:fill') && $nodeDrawingPageProps->hasAttribute('draw:fill-image-name')) { $nameStyle = $nodeDrawingPageProps->getAttribute('draw:fill-image-name'); if (!empty($this->arrayCommonStyles[$nameStyle]) && 'image' == $this->arrayCommonStyles[$nameStyle]['type'] && !empty($this->arrayCommonStyles[$nameStyle]['path'])) { - $tmpBkgImg = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderODPBkg'); $contentImg = $this->oZip->getFromName($this->arrayCommonStyles[$nameStyle]['path']); - file_put_contents($tmpBkgImg, $contentImg); + if ($contentImg) { + $tmpFile = new Gd(); + $tmpFile->loadFromContent($contentImg, basename($this->arrayCommonStyles[$nameStyle]['path'])); - $oBackground = new Image(); - $oBackground->setPath($tmpBkgImg); + $oBackground = new Image(); + $oBackground->setImage($tmpFile); + } } } } @@ -584,7 +586,7 @@ protected function loadShapeDrawing(DOMElement $oNodeFrame): void // Contents of file if (empty($mimetype)) { $shape = new Gd(); - $shape->setImageResource(imagecreatefromstring($imageFile)); + $shape->loadFromContent($imageFile, basename($sFilename)); } else { $shape = new Base64(); $shape->setData('data:' . $mimetype . ';base64,' . base64_encode($imageFile)); @@ -668,13 +670,12 @@ protected function loadShapeMedia(DOMElement $oNodeFrame): void $mediaFile = $this->oZip->getFromName($filePath); } - $tmpEmbed = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderODPEmbed'); - file_put_contents($tmpEmbed, $mediaFile); + if (!$mediaFile) { + return; + } $shape = new Media(); - $shape - ->setFileName(basename($filePath)) - ->setPath($tmpEmbed, false); + $shape->loadFromContent($mediaFile, basename($filePath)); $shape->getShadow()->setVisible(false); $shape->setName($oNodeFrame->hasAttribute('draw:name') ? $oNodeFrame->getAttribute('draw:name') : ''); From 54747756c01772d76e8a3e86bbc95bba152b7509 Mon Sep 17 00:00:00 2001 From: Phil Wilkinson Date: Fri, 16 May 2025 20:21:06 +0100 Subject: [PATCH 12/12] Fixed memory leak caused by loading GD images from string --- src/PhpPresentation/Shape/Drawing/Gd.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpPresentation/Shape/Drawing/Gd.php b/src/PhpPresentation/Shape/Drawing/Gd.php index f06589895..1149ff014 100644 --- a/src/PhpPresentation/Shape/Drawing/Gd.php +++ b/src/PhpPresentation/Shape/Drawing/Gd.php @@ -301,17 +301,17 @@ public function __destruct() */ public function loadFromContent(string $content, string $fileName = '', string $prefix = 'PhpPresentationGd'): AbstractDrawingAdapter { + // Check if the content is a valid image $image = @imagecreatefromstring($content); if ($image === false) { return $this; } + // Clean up the image resource to avoid memory leaks + @imagedestroy($image); $tmpFile = tempnam(sys_get_temp_dir(), $prefix); file_put_contents($tmpFile, $content); - // Set image resource - $this->setImageResource($image); - // Set path and mark as temporary for automatic cleanup $this->setPath($tmpFile); $this->setIsTemporaryFile(true);