fix(templates): fill pptx via direct zip/XML editing instead of PhpPresentation

Round-tripping real-world .pptx files through PhpPresentation's object model
was silently corrupting/losing content it doesn't fully model (themes, masters,
layouts, fonts), so placeholder replacement wasn't reliable. Now the service
edits the .pptx as a zip archive directly: only the touched slide XML text runs
and swapped image bytes change, everything else stays byte-identical to the
source file. Drops the now-unused phpoffice/phppresentation dependency.
This commit is contained in:
2026-07-23 00:32:47 +03:30
parent 0a7b7fea17
commit e58dcd8581
3 changed files with 311 additions and 645 deletions

View File

@@ -4,30 +4,46 @@ namespace App\Services;
use App\Enums\TemplateKeyType; use App\Enums\TemplateKeyType;
use App\Models\TemplateGeneration; use App\Models\TemplateGeneration;
use DOMDocument;
use DOMXPath;
use Illuminate\Support\Facades\Storage; 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 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 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 public function fill(TemplateGeneration $generation): string
{ {
$template = $generation->template; $template = $generation->template;
$sourcePath = Storage::disk('local')->path($template->file_path); $sourceAbsolutePath = Storage::disk('local')->path($template->file_path);
$presentation = IOFactory::load($sourcePath);
$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 ?? []; $values = $generation->values ?? [];
$textValues = []; $textValues = [];
$imageValues = [];
foreach ($template->keys as $key) { foreach ($template->keys as $key) {
if (! array_key_exists($key->key, $values)) { if (! array_key_exists($key->key, $values)) {
@@ -36,80 +52,72 @@ class TemplateFillService
match ($key->type) { match ($key->type) {
TemplateKeyType::Text => $textValues[$key->key] = (string) $values[$key->key], 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('پرکردن کلید از نوع چارت هنوز پشتیبانی نمی‌شود.'), TemplateKeyType::Chart => throw new RuntimeException('پرکردن کلید از نوع چارت هنوز پشتیبانی نمی‌شود.'),
}; };
} }
if ($textValues !== []) { $zip = new ZipArchive;
foreach ($presentation->getAllSlides() as $slide) {
$this->replaceInShapes($slide->getShapeCollection(), $textValues); if ($zip->open($outputAbsolutePath) !== true) {
} throw new RuntimeException('باز کردن فایل پاورپوینت با خطا مواجه شد.');
} }
$outputRelativePath = 'templates/generations/'.$generation->uuid.'.pptx'; try {
$outputAbsolutePath = Storage::disk('local')->path($outputRelativePath); $slideEntries = $this->findSlideEntries($zip);
Storage::disk('local')->makeDirectory('templates/generations'); foreach ($slideEntries as $slideEntry) {
$xml = $zip->getFromName($slideEntry);
$writer = IOFactory::createWriter($presentation, 'PowerPoint2007'); if ($xml === false) {
$writer->save($outputAbsolutePath); 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; return $outputRelativePath;
} }
/** /**
* @param iterable<mixed> $shapes * @return array<int, string>
* @param array<string, string> $textValues
*/ */
private function replaceInShapes(iterable $shapes, array $textValues): void private function findSlideEntries(ZipArchive $zip): array
{ {
foreach ($shapes as $shape) { $entries = [];
if ($shape instanceof RichText) {
foreach ($shape->getParagraphs() as $paragraph) {
$this->replaceInParagraph($paragraph, $textValues);
}
continue; for ($i = 0; $i < $zip->numFiles; $i++) {
} $name = $zip->getNameIndex($i);
if ($shape instanceof TableShape) { if ($name !== false && preg_match('#^ppt/slides/slide\d+\.xml$#', $name)) {
foreach ($shape->getRows() as $row) { $entries[] = $name;
foreach ($row->getCells() as $cell) {
foreach ($cell->getParagraphs() as $paragraph) {
$this->replaceInParagraph($paragraph, $textValues);
}
}
}
continue;
}
if ($shape instanceof GroupShape) {
$this->replaceInShapes($shape->getShapeCollection(), $textValues);
} }
} }
return $entries;
} }
/** /**
* @param array<string, string> $textValues * @param array<string, string> $textValues
*/ */
private function replaceInParagraph(Paragraph $paragraph, array $textValues): void private function replaceTextInSlideXml(string $xml, array $textValues): ?string
{ {
$elements = $paragraph->getRichTextElements(); if (! str_contains($xml, '{')) {
return null;
if ($elements === []) {
return;
}
$originalText = '';
foreach ($elements as $element) {
$originalText .= $element->getText();
}
if (! str_contains($originalText, '{')) {
return;
} }
$lookup = []; $lookup = [];
@@ -118,36 +126,62 @@ class TemplateFillService
$lookup[$this->normalizeKey($key)] = $value; $lookup[$this->normalizeKey($key)] = $value;
} }
$newText = preg_replace_callback( $dom = new DOMDocument;
'/\{\{?\s*([^{}]+?)\s*\}?\}/u', $previousSetting = libxml_use_internal_errors(true);
function (array $matches) use ($lookup): string { $loaded = $dom->loadXML($xml);
$normalizedKey = $this->normalizeKey($matches[1]); libxml_clear_errors();
libxml_use_internal_errors($previousSetting);
return array_key_exists($normalizedKey, $lookup) ? $lookup[$normalizedKey] : $matches[0]; if (! $loaded) {
}, return null;
$originalText
);
if ($newText === null || $newText === $originalText) {
return;
} }
$target = null; $xpath = new DOMXPath($dom);
$xpath->registerNamespace('a', self::NS_DRAWING);
foreach ($elements as $element) { $changed = false;
if ($element instanceof BreakElement) {
foreach ($xpath->query('//a:p') as $paragraph) {
$textNodes = iterator_to_array($xpath->query('.//a:r/a:t', $paragraph));
if ($textNodes === []) {
continue; continue;
} }
if ($target === null) { $originalText = '';
$target = $element;
$element->setText($newText);
foreach ($textNodes as $textNode) {
$originalText .= $textNode->textContent;
}
if (! str_contains($originalText, '{')) {
continue; 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 private function normalizeKey(string $key): string
@@ -155,52 +189,143 @@ class TemplateFillService
return mb_strtolower(trim($key, "{} \t\n\r\0\x0B")); 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; return;
} }
foreach ($presentation->getAllSlides() as $slide) { $mediaPath = $this->resolveMediaPath($zip, $slideEntry, $relationshipId);
$this->replaceImageInShapes($slide->getShapeCollection(), $shapeName, $imagePath);
if ($mediaPath === null) {
return;
} }
$zip->addFromString($mediaPath, $this->reencodeImage($mediaPath, $newImagePath));
} }
/** private function findImageRelationshipId(string $slideXml, string $shapeName): ?string
* @param iterable<mixed> $shapes
*/
private function replaceImageInShapes(iterable $shapes, string $shapeName, string $imagePath): void
{ {
foreach ($shapes as $shape) { $dom = new DOMDocument;
if ($shape instanceof GroupShape) { $previousSetting = libxml_use_internal_errors(true);
$this->replaceImageInShapes($shape->getShapeCollection(), $shapeName, $imagePath); $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; continue;
} }
if ($shape->getName() !== $shapeName) { $segments[] = $segment;
continue;
}
if ($shape instanceof GdDrawing) {
$this->replaceGdImage($shape, $imagePath);
} elseif ($shape instanceof FileDrawing) {
$shape->setPath($imagePath);
}
} }
return implode('/', $segments);
} }
private function replaceGdImage(GdDrawing $shape, string $imagePath): void private function reencodeImage(string $mediaPath, string $newImagePath): string
{ {
$info = getimagesize($imagePath); $resource = imagecreatefromstring(file_get_contents($newImagePath));
$image = imagecreatefromstring(file_get_contents($imagePath));
if ($info === false || $image === false) { if ($resource === false) {
throw new RuntimeException('فایل تصویر ارسالی معتبر نیست.'); throw new RuntimeException('فایل تصویر ارسالی معتبر نیست.');
} }
$shape->setImageResource($image); $extension = strtolower(pathinfo($mediaPath, PATHINFO_EXTENSION));
$shape->setMimeType($info['mime']);
$shape->setRenderingFunction(str_replace('/', '', $info['mime'])); 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;
} }
} }

View File

@@ -14,8 +14,7 @@
"filament/filament": "^4.0", "filament/filament": "^4.0",
"laravel/framework": "^13.8", "laravel/framework": "^13.8",
"laravel/sanctum": "^4.3", "laravel/sanctum": "^4.3",
"laravel/tinker": "^3.0", "laravel/tinker": "^3.0"
"phpoffice/phppresentation": "^1.2"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",

618
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "228c4071ac2afaa94e16b892dc1fbdc4", "content-hash": "c479d2908801be9c7f146d6a1c4662b8",
"packages": [ "packages": [
{ {
"name": "blade-ui-kit/blade-heroicons", "name": "blade-ui-kit/blade-heroicons",
@@ -447,85 +447,6 @@
], ],
"time": "2026-03-20T21:10:52+00:00" "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", "name": "danharrin/date-format-converter",
"version": "v0.3.1", "version": "v0.3.1",
@@ -3465,191 +3386,6 @@
], ],
"time": "2026-04-30T23:56:43+00:00" "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", "name": "monolog/monolog",
"version": "3.10.0", "version": "3.10.0",
@@ -4397,279 +4133,6 @@
}, },
"time": "2025-09-24T15:06:41+00:00" "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", "name": "phpoption/phpoption",
"version": "1.9.5", "version": "1.9.5",
@@ -9224,6 +8687,85 @@
], ],
"time": "2026-03-29T15:46:14+00:00" "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", "name": "composer/xdebug-handler",
"version": "3.0.5", "version": "3.0.5",