diff --git a/app/Services/TemplateFillService.php b/app/Services/TemplateFillService.php index 4b10121..bb5c405 100644 --- a/app/Services/TemplateFillService.php +++ b/app/Services/TemplateFillService.php @@ -4,30 +4,46 @@ namespace App\Services; use App\Enums\TemplateKeyType; use App\Models\TemplateGeneration; +use DOMDocument; +use DOMXPath; use Illuminate\Support\Facades\Storage; -use PhpOffice\PhpPresentation\IOFactory; -use PhpOffice\PhpPresentation\PhpPresentation; -use PhpOffice\PhpPresentation\Shape\Drawing\File as FileDrawing; -use PhpOffice\PhpPresentation\Shape\Drawing\Gd as GdDrawing; -use PhpOffice\PhpPresentation\Shape\Group as GroupShape; -use PhpOffice\PhpPresentation\Shape\RichText; -use PhpOffice\PhpPresentation\Shape\RichText\BreakElement; -use PhpOffice\PhpPresentation\Shape\RichText\Paragraph; -use PhpOffice\PhpPresentation\Shape\Table as TableShape; use RuntimeException; +use ZipArchive; +/** + * Fills a .pptx template by editing it directly as a zip/XML archive, instead of + * round-tripping it through a full presentation object model. Real-world .pptx files + * (themes, masters, layouts, animations, embedded fonts) can be lost or corrupted by + * libraries that fully re-serialize the document; this way only the exact text runs + * and image bytes we touch are changed, everything else stays byte-identical. + */ class TemplateFillService { + private const NS_PRESENTATION = 'http://schemas.openxmlformats.org/presentationml/2006/main'; + + private const NS_DRAWING = 'http://schemas.openxmlformats.org/drawingml/2006/main'; + + private const NS_RELATIONSHIPS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'; + public function fill(TemplateGeneration $generation): string { $template = $generation->template; - $sourcePath = Storage::disk('local')->path($template->file_path); - $presentation = IOFactory::load($sourcePath); + $sourceAbsolutePath = Storage::disk('local')->path($template->file_path); + + $outputRelativePath = 'templates/generations/'.$generation->uuid.'.pptx'; + $outputAbsolutePath = Storage::disk('local')->path($outputRelativePath); + + Storage::disk('local')->makeDirectory('templates/generations'); + + if (! copy($sourceAbsolutePath, $outputAbsolutePath)) { + throw new RuntimeException('امکان ساخت فایل خروجی وجود ندارد.'); + } $values = $generation->values ?? []; $textValues = []; + $imageValues = []; foreach ($template->keys as $key) { if (! array_key_exists($key->key, $values)) { @@ -36,80 +52,72 @@ class TemplateFillService match ($key->type) { TemplateKeyType::Text => $textValues[$key->key] = (string) $values[$key->key], - TemplateKeyType::Image => $this->fillImage($presentation, $key->shape_name, Storage::disk('local')->path($values[$key->key])), + TemplateKeyType::Image => $imageValues[$key->shape_name] = Storage::disk('local')->path($values[$key->key]), TemplateKeyType::Chart => throw new RuntimeException('پرکردن کلید از نوع چارت هنوز پشتیبانی نمی‌شود.'), }; } - if ($textValues !== []) { - foreach ($presentation->getAllSlides() as $slide) { - $this->replaceInShapes($slide->getShapeCollection(), $textValues); - } + $zip = new ZipArchive; + + if ($zip->open($outputAbsolutePath) !== true) { + throw new RuntimeException('باز کردن فایل پاورپوینت با خطا مواجه شد.'); } - $outputRelativePath = 'templates/generations/'.$generation->uuid.'.pptx'; - $outputAbsolutePath = Storage::disk('local')->path($outputRelativePath); + try { + $slideEntries = $this->findSlideEntries($zip); - Storage::disk('local')->makeDirectory('templates/generations'); + foreach ($slideEntries as $slideEntry) { + $xml = $zip->getFromName($slideEntry); - $writer = IOFactory::createWriter($presentation, 'PowerPoint2007'); - $writer->save($outputAbsolutePath); + if ($xml === false) { + continue; + } + + if ($textValues !== []) { + $newXml = $this->replaceTextInSlideXml($xml, $textValues); + + if ($newXml !== null) { + $zip->addFromString($slideEntry, $newXml); + $xml = $newXml; + } + } + + foreach ($imageValues as $shapeName => $imagePath) { + $this->replaceShapeImage($zip, $slideEntry, $xml, $shapeName, $imagePath); + } + } + } finally { + $zip->close(); + } return $outputRelativePath; } /** - * @param iterable $shapes - * @param array $textValues + * @return array */ - private function replaceInShapes(iterable $shapes, array $textValues): void + private function findSlideEntries(ZipArchive $zip): array { - foreach ($shapes as $shape) { - if ($shape instanceof RichText) { - foreach ($shape->getParagraphs() as $paragraph) { - $this->replaceInParagraph($paragraph, $textValues); - } + $entries = []; - continue; - } + for ($i = 0; $i < $zip->numFiles; $i++) { + $name = $zip->getNameIndex($i); - if ($shape instanceof TableShape) { - foreach ($shape->getRows() as $row) { - foreach ($row->getCells() as $cell) { - foreach ($cell->getParagraphs() as $paragraph) { - $this->replaceInParagraph($paragraph, $textValues); - } - } - } - - continue; - } - - if ($shape instanceof GroupShape) { - $this->replaceInShapes($shape->getShapeCollection(), $textValues); + if ($name !== false && preg_match('#^ppt/slides/slide\d+\.xml$#', $name)) { + $entries[] = $name; } } + + return $entries; } /** * @param array $textValues */ - private function replaceInParagraph(Paragraph $paragraph, array $textValues): void + private function replaceTextInSlideXml(string $xml, array $textValues): ?string { - $elements = $paragraph->getRichTextElements(); - - if ($elements === []) { - return; - } - - $originalText = ''; - - foreach ($elements as $element) { - $originalText .= $element->getText(); - } - - if (! str_contains($originalText, '{')) { - return; + if (! str_contains($xml, '{')) { + return null; } $lookup = []; @@ -118,36 +126,62 @@ class TemplateFillService $lookup[$this->normalizeKey($key)] = $value; } - $newText = preg_replace_callback( - '/\{\{?\s*([^{}]+?)\s*\}?\}/u', - function (array $matches) use ($lookup): string { - $normalizedKey = $this->normalizeKey($matches[1]); + $dom = new DOMDocument; + $previousSetting = libxml_use_internal_errors(true); + $loaded = $dom->loadXML($xml); + libxml_clear_errors(); + libxml_use_internal_errors($previousSetting); - return array_key_exists($normalizedKey, $lookup) ? $lookup[$normalizedKey] : $matches[0]; - }, - $originalText - ); - - if ($newText === null || $newText === $originalText) { - return; + if (! $loaded) { + return null; } - $target = null; + $xpath = new DOMXPath($dom); + $xpath->registerNamespace('a', self::NS_DRAWING); - foreach ($elements as $element) { - if ($element instanceof BreakElement) { + $changed = false; + + foreach ($xpath->query('//a:p') as $paragraph) { + $textNodes = iterator_to_array($xpath->query('.//a:r/a:t', $paragraph)); + + if ($textNodes === []) { continue; } - if ($target === null) { - $target = $element; - $element->setText($newText); + $originalText = ''; + foreach ($textNodes as $textNode) { + $originalText .= $textNode->textContent; + } + + if (! str_contains($originalText, '{')) { continue; } - $element->setText(''); + $newText = preg_replace_callback( + '/\{\{?\s*([^{}]+?)\s*\}?\}/u', + function (array $matches) use ($lookup): string { + $normalizedKey = $this->normalizeKey($matches[1]); + + return array_key_exists($normalizedKey, $lookup) ? $lookup[$normalizedKey] : $matches[0]; + }, + $originalText + ); + + if ($newText === null || $newText === $originalText) { + continue; + } + + $textNodes[0]->textContent = $newText; + + for ($i = 1; $i < count($textNodes); $i++) { + $textNodes[$i]->textContent = ''; + } + + $changed = true; } + + return $changed ? $dom->saveXML() : null; } private function normalizeKey(string $key): string @@ -155,52 +189,143 @@ class TemplateFillService return mb_strtolower(trim($key, "{} \t\n\r\0\x0B")); } - private function fillImage(PhpPresentation $presentation, ?string $shapeName, string $imagePath): void + private function replaceShapeImage(ZipArchive $zip, string $slideEntry, string $slideXml, string $shapeName, string $newImagePath): void { - if ($shapeName === null || $shapeName === '') { + $relationshipId = $this->findImageRelationshipId($slideXml, $shapeName); + + if ($relationshipId === null) { return; } - foreach ($presentation->getAllSlides() as $slide) { - $this->replaceImageInShapes($slide->getShapeCollection(), $shapeName, $imagePath); + $mediaPath = $this->resolveMediaPath($zip, $slideEntry, $relationshipId); + + if ($mediaPath === null) { + return; } + + $zip->addFromString($mediaPath, $this->reencodeImage($mediaPath, $newImagePath)); } - /** - * @param iterable $shapes - */ - private function replaceImageInShapes(iterable $shapes, string $shapeName, string $imagePath): void + private function findImageRelationshipId(string $slideXml, string $shapeName): ?string { - foreach ($shapes as $shape) { - if ($shape instanceof GroupShape) { - $this->replaceImageInShapes($shape->getShapeCollection(), $shapeName, $imagePath); + $dom = new DOMDocument; + $previousSetting = libxml_use_internal_errors(true); + $loaded = $dom->loadXML($slideXml); + libxml_clear_errors(); + libxml_use_internal_errors($previousSetting); + + if (! $loaded) { + return null; + } + + $xpath = new DOMXPath($dom); + $xpath->registerNamespace('p', self::NS_PRESENTATION); + $xpath->registerNamespace('a', self::NS_DRAWING); + $xpath->registerNamespace('r', self::NS_RELATIONSHIPS); + + foreach ($xpath->query('//p:cNvPr') as $cNvPr) { + if ($cNvPr->getAttribute('name') !== $shapeName) { + continue; + } + + $picNode = $cNvPr->parentNode; + + while ($picNode !== null && $picNode->nodeName !== 'p:pic') { + $picNode = $picNode->parentNode; + } + + if ($picNode === null) { + continue; + } + + $blips = $xpath->query('.//a:blip', $picNode); + + if ($blips->length === 0) { + continue; + } + + $embedId = $blips->item(0)->getAttributeNS(self::NS_RELATIONSHIPS, 'embed'); + + if ($embedId !== '') { + return $embedId; + } + } + + return null; + } + + private function resolveMediaPath(ZipArchive $zip, string $slideEntry, string $relationshipId): ?string + { + $relsEntry = 'ppt/slides/_rels/'.basename($slideEntry).'.rels'; + $relsXml = $zip->getFromName($relsEntry); + + if ($relsXml === false) { + return null; + } + + $dom = new DOMDocument; + $previousSetting = libxml_use_internal_errors(true); + $loaded = $dom->loadXML($relsXml); + libxml_clear_errors(); + libxml_use_internal_errors($previousSetting); + + if (! $loaded) { + return null; + } + + foreach ($dom->getElementsByTagName('Relationship') as $relationship) { + if ($relationship->getAttribute('Id') !== $relationshipId) { + continue; + } + + return $this->normalizeZipPath('ppt/slides/'.$relationship->getAttribute('Target')); + } + + return null; + } + + private function normalizeZipPath(string $path): string + { + $segments = []; + + foreach (explode('/', $path) as $segment) { + if ($segment === '' || $segment === '.') { + continue; + } + + if ($segment === '..') { + array_pop($segments); continue; } - if ($shape->getName() !== $shapeName) { - continue; - } - - if ($shape instanceof GdDrawing) { - $this->replaceGdImage($shape, $imagePath); - } elseif ($shape instanceof FileDrawing) { - $shape->setPath($imagePath); - } + $segments[] = $segment; } + + return implode('/', $segments); } - private function replaceGdImage(GdDrawing $shape, string $imagePath): void + private function reencodeImage(string $mediaPath, string $newImagePath): string { - $info = getimagesize($imagePath); - $image = imagecreatefromstring(file_get_contents($imagePath)); + $resource = imagecreatefromstring(file_get_contents($newImagePath)); - if ($info === false || $image === false) { + if ($resource === false) { throw new RuntimeException('فایل تصویر ارسالی معتبر نیست.'); } - $shape->setImageResource($image); - $shape->setMimeType($info['mime']); - $shape->setRenderingFunction(str_replace('/', '', $info['mime'])); + $extension = strtolower(pathinfo($mediaPath, PATHINFO_EXTENSION)); + + ob_start(); + + match ($extension) { + 'jpg', 'jpeg' => imagejpeg($resource, quality: 92), + 'gif' => imagegif($resource), + 'bmp' => imagebmp($resource), + default => imagepng($resource), + }; + + $bytes = ob_get_clean(); + + return $bytes; } } diff --git a/composer.json b/composer.json index bca2134..a407477 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,7 @@ "filament/filament": "^4.0", "laravel/framework": "^13.8", "laravel/sanctum": "^4.3", - "laravel/tinker": "^3.0", - "phpoffice/phppresentation": "^1.2" + "laravel/tinker": "^3.0" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index cb0f62b..6245a25 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "228c4071ac2afaa94e16b892dc1fbdc4", + "content-hash": "c479d2908801be9c7f146d6a1c4662b8", "packages": [ { "name": "blade-ui-kit/blade-heroicons", @@ -447,85 +447,6 @@ ], "time": "2026-03-20T21:10:52+00:00" }, - { - "name": "composer/pcre", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" - }, - "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-11-12T16:29:46+00:00" - }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -3465,191 +3386,6 @@ ], "time": "2026-04-30T23:56:43+00:00" }, - { - "name": "maennchen/zipstream-php", - "version": "3.2.2", - "source": { - "type": "git", - "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e", - "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "ext-zlib": "*", - "php-64bit": "^8.3" - }, - "require-dev": { - "brianium/paratest": "^7.7", - "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.86", - "guzzlehttp/guzzle": "^7.5", - "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^12.0", - "vimeo/psalm": "^6.0" - }, - "suggest": { - "guzzlehttp/psr7": "^2.4", - "psr/http-message": "^2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "ZipStream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paul Duncan", - "email": "pabs@pablotron.org" - }, - { - "name": "Jonatan Männchen", - "email": "jonatan@maennchen.ch" - }, - { - "name": "Jesse Donat", - "email": "donatj@gmail.com" - }, - { - "name": "András Kolesár", - "email": "kolesar@kolesar.hu" - } - ], - "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", - "keywords": [ - "stream", - "zip" - ], - "support": { - "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2" - }, - "funding": [ - { - "url": "https://github.com/maennchen", - "type": "github" - } - ], - "time": "2026-04-11T18:38:28+00:00" - }, - { - "name": "markbaker/complex", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/MarkBaker/PHPComplex.git", - "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", - "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "phpcompatibility/php-compatibility": "^9.3", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "squizlabs/php_codesniffer": "^3.7" - }, - "type": "library", - "autoload": { - "psr-4": { - "Complex\\": "classes/src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mark Baker", - "email": "mark@lange.demon.co.uk" - } - ], - "description": "PHP Class for working with complex numbers", - "homepage": "https://github.com/MarkBaker/PHPComplex", - "keywords": [ - "complex", - "mathematics" - ], - "support": { - "issues": "https://github.com/MarkBaker/PHPComplex/issues", - "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" - }, - "time": "2022-12-06T16:21:08+00:00" - }, - { - "name": "markbaker/matrix", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/MarkBaker/PHPMatrix.git", - "reference": "728434227fe21be27ff6d86621a1b13107a2562c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", - "reference": "728434227fe21be27ff6d86621a1b13107a2562c", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "phpcompatibility/php-compatibility": "^9.3", - "phpdocumentor/phpdocumentor": "2.*", - "phploc/phploc": "^4.0", - "phpmd/phpmd": "2.*", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "sebastian/phpcpd": "^4.0", - "squizlabs/php_codesniffer": "^3.7" - }, - "type": "library", - "autoload": { - "psr-4": { - "Matrix\\": "classes/src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mark Baker", - "email": "mark@demon-angel.eu" - } - ], - "description": "PHP Class for working with matrices", - "homepage": "https://github.com/MarkBaker/PHPMatrix", - "keywords": [ - "mathematics", - "matrix", - "vector" - ], - "support": { - "issues": "https://github.com/MarkBaker/PHPMatrix/issues", - "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" - }, - "time": "2022-12-02T22:17:43+00:00" - }, { "name": "monolog/monolog", "version": "3.10.0", @@ -4397,279 +4133,6 @@ }, "time": "2025-09-24T15:06:41+00:00" }, - { - "name": "pclzip/pclzip", - "version": "2.8.2", - "source": { - "type": "git", - "url": "https://github.com/ivanlanin/pclzip.git", - "reference": "19dd1de9d3f5fc4d7d70175b4c344dee329f45fd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ivanlanin/pclzip/zipball/19dd1de9d3f5fc4d7d70175b4c344dee329f45fd", - "reference": "19dd1de9d3f5fc4d7d70175b4c344dee329f45fd", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "pclzip.lib.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1" - ], - "authors": [ - { - "name": "Vincent Blavet" - } - ], - "description": "A PHP library that offers compression and extraction functions for Zip formatted archives", - "homepage": "http://www.phpconcept.net/pclzip", - "keywords": [ - "php", - "zip" - ], - "support": { - "issues": "https://github.com/ivanlanin/pclzip/issues", - "source": "https://github.com/ivanlanin/pclzip/tree/master" - }, - "time": "2014-06-05T11:42:24+00:00" - }, - { - "name": "phpoffice/common", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/Common.git", - "reference": "5a2eeb82d4dfce4ce2163819063ba6f5a80c3e91" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/Common/zipball/5a2eeb82d4dfce4ce2163819063ba6f5a80c3e91", - "reference": "5a2eeb82d4dfce4ce2163819063ba6f5a80c3e91", - "shasum": "" - }, - "require": { - "pclzip/pclzip": "^2.8", - "php": ">=7.1" - }, - "require-dev": { - "phpmd/phpmd": "2.*", - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpunit/phpunit": ">=7" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\Common\\": "src/Common/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-only" - ], - "authors": [ - { - "name": "Mark Baker" - }, - { - "name": "Franck Lefevre", - "homepage": "http://rootslabs.net" - } - ], - "description": "PHPOffice Common", - "homepage": "http://phpoffice.github.io", - "keywords": [ - "common", - "component", - "office", - "php" - ], - "support": { - "issues": "https://github.com/PHPOffice/Common/issues", - "source": "https://github.com/PHPOffice/Common/tree/1.0.5" - }, - "time": "2025-02-28T12:39:45+00:00" - }, - { - "name": "phpoffice/phppresentation", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/PHPPresentation.git", - "reference": "b9cdcd413e2d463ba3198f912f32716adc88f340" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPPresentation/zipball/b9cdcd413e2d463ba3198f912f32716adc88f340", - "reference": "b9cdcd413e2d463ba3198f912f32716adc88f340", - "shasum": "" - }, - "require": { - "ext-xml": "*", - "ext-zip": "*", - "php": "^7.1|^8.0", - "phpoffice/common": "^1", - "phpoffice/phpspreadsheet": "^1.9 || ^2.0 || ^3.0 || ^4.0" - }, - "require-dev": { - "dompdf/dompdf": "^3.1", - "phpmd/phpmd": "2.*", - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpunit/phpunit": ">=7.0" - }, - "suggest": { - "ext-gd": "Required to add images" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\PhpPresentation\\": "src/PhpPresentation/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-only" - ], - "authors": [ - { - "name": "Mark Baker" - }, - { - "name": "Franck Lefevre", - "homepage": "http://rootslabs.net" - }, - { - "name": "Ivan Lanin", - "homepage": "http://ivan.lanin.org" - } - ], - "description": "PHPPresentation - Read, Create and Write Presentations documents in PHP", - "homepage": "http://phpoffice.github.io", - "keywords": [ - "LibreOffice", - "PowerPoint", - "odp", - "php", - "ppt", - "pptx", - "presentations" - ], - "support": { - "issues": "https://github.com/PHPOffice/PHPPresentation/issues", - "source": "https://github.com/PHPOffice/PHPPresentation/tree/1.2.0" - }, - "time": "2025-06-06T10:27:25+00:00" - }, - { - "name": "phpoffice/phpspreadsheet", - "version": "4.5.0", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "2ea9786632e6fac1aee601b6e426bcc723d8ce13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2ea9786632e6fac1aee601b6e426bcc723d8ce13", - "reference": "2ea9786632e6fac1aee601b6e426bcc723d8ce13", - "shasum": "" - }, - "require": { - "composer/pcre": "^1||^2||^3", - "ext-ctype": "*", - "ext-dom": "*", - "ext-fileinfo": "*", - "ext-gd": "*", - "ext-iconv": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "ext-xml": "*", - "ext-xmlreader": "*", - "ext-xmlwriter": "*", - "ext-zip": "*", - "ext-zlib": "*", - "maennchen/zipstream-php": "^2.1 || ^3.0", - "markbaker/complex": "^3.0", - "markbaker/matrix": "^3.0", - "php": "^8.1", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^2.0 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.3", - "mpdf/mpdf": "^8.1.1", - "phpcompatibility/php-compatibility": "^9.3", - "phpstan/phpstan": "^1.1 || ^2.0", - "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", - "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": "^10.5", - "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.5" - }, - "suggest": { - "dompdf/dompdf": "Option for rendering PDF with PDF Writer", - "ext-intl": "PHP Internationalization Functions", - "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", - "mpdf/mpdf": "Option for rendering PDF with PDF Writer", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Maarten Balliauw", - "homepage": "https://blog.maartenballiauw.be" - }, - { - "name": "Mark Baker", - "homepage": "https://markbakeruk.net" - }, - { - "name": "Franck Lefevre", - "homepage": "https://rootslabs.net" - }, - { - "name": "Erik Tilt" - }, - { - "name": "Adrien Crivelli" - } - ], - "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", - "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", - "keywords": [ - "OpenXML", - "excel", - "gnumeric", - "ods", - "php", - "spreadsheet", - "xls", - "xlsx" - ], - "support": { - "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/4.5.0" - }, - "time": "2025-07-24T05:15:59+00:00" - }, { "name": "phpoption/phpoption", "version": "1.9.5", @@ -9224,6 +8687,85 @@ ], "time": "2026-03-29T15:46:14+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "composer/xdebug-handler", "version": "3.0.5",