Skip to content

Commit a673399

Browse files
authored
Merge pull request #4712 from rpg600/5.1.0-patched
feat: add drawing pass-through support for unsupported elements
2 parents 8120b65 + 1bb6ae8 commit a673399

File tree

10 files changed

+1012
-1
lines changed

10 files changed

+1012
-1
lines changed

src/PhpSpreadsheet/Reader/BaseReader.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ abstract class BaseReader implements IReader
6060
*/
6161
protected bool $createBlankSheetIfNoneRead = false;
6262

63+
/**
64+
* Enable drawing pass-through?
65+
* Identifies whether the Reader should preserve unsupported drawing elements (shapes, grouped images, etc.)
66+
* by storing the original XML for pass-through during write operations.
67+
* When enabled, drawings cannot be modified programmatically but are preserved exactly.
68+
*/
69+
protected bool $enableDrawingPassThrough = false;
70+
6371
/**
6472
* IReadFilter instance.
6573
*/
@@ -125,6 +133,18 @@ public function setIncludeCharts(bool $includeCharts): self
125133
return $this;
126134
}
127135

136+
public function getEnableDrawingPassThrough(): bool
137+
{
138+
return $this->enableDrawingPassThrough;
139+
}
140+
141+
public function setEnableDrawingPassThrough(bool $enableDrawingPassThrough): self
142+
{
143+
$this->enableDrawingPassThrough = $enableDrawingPassThrough;
144+
145+
return $this;
146+
}
147+
128148
/** @return null|string[] */
129149
public function getLoadSheetsOnly(): ?array
130150
{

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,29 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
14861486
$xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
14871487
$xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);
14881488

1489+
// Store drawing XML for pass-through if enabled
1490+
if ($this->enableDrawingPassThrough) {
1491+
$unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
1492+
// Mark that pass-through is enabled for this sheet
1493+
$sheetCodeName = $docSheet->getCodeName();
1494+
if (!isset($unparsedLoadedData['sheets']) || !is_array($unparsedLoadedData['sheets'])) {
1495+
$unparsedLoadedData['sheets'] = [];
1496+
}
1497+
if (!isset($unparsedLoadedData['sheets'][$sheetCodeName]) || !is_array($unparsedLoadedData['sheets'][$sheetCodeName])) {
1498+
$unparsedLoadedData['sheets'][$sheetCodeName] = [];
1499+
}
1500+
/** @var array<string, mixed> $sheetUnparsedData */
1501+
$sheetUnparsedData = &$unparsedLoadedData['sheets'][$sheetCodeName];
1502+
$sheetUnparsedData['drawingPassThroughEnabled'] = true;
1503+
// Store original drawing relationships for pass-through
1504+
if ($relsDrawing) {
1505+
$sheetUnparsedData['drawingRelationships'] = $relsDrawing->asXML();
1506+
}
1507+
// Store original media files paths and source file for pass-through
1508+
$sheetUnparsedData['drawingMediaFiles'] = $images;
1509+
$sheetUnparsedData['drawingSourceFile'] = File::realpath($filename);
1510+
}
1511+
14891512
if ($xmlDrawingChildren->oneCellAnchor) {
14901513
foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
14911514
$oneCellAnchor = self::testSimpleXml($oneCellAnchor);

src/PhpSpreadsheet/Writer/Xlsx.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,9 @@ public function save($filename, int $flags = 0): void
437437
}
438438

439439
// Add drawing and image relationship parts
440-
if (($drawingCount > 0) || ($chartCount > 0)) {
440+
/** @var bool $hasPassThroughDrawing */
441+
$hasPassThroughDrawing = $unparsedSheet['drawingPassThroughEnabled'] ?? false;
442+
if (($drawingCount > 0) || ($chartCount > 0) || $hasPassThroughDrawing) {
441443
// Drawing relationships
442444
$zipContent['xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts);
443445

@@ -558,6 +560,9 @@ public function save($filename, int $flags = 0): void
558560
}
559561
}
560562

563+
// Add pass-through media files (original media that may not be in the drawing collection)
564+
$this->addPassThroughMediaFiles($zipContent); // @phpstan-ignore argument.type
565+
561566
Functions::setReturnDateType($saveDateReturnType);
562567
Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
563568

@@ -843,4 +848,43 @@ public function getRestrictMaxColumnWidth(): bool
843848
{
844849
return $this->restrictMaxColumnWidth;
845850
}
851+
852+
/**
853+
* Add pass-through media files from original spreadsheet.
854+
* This copies media files that are referenced in pass-through drawing XML
855+
* but may not be in the drawing collection (e.g., unsupported formats like SVG).
856+
*
857+
* @param string[] $zipContent
858+
*/
859+
private function addPassThroughMediaFiles(array &$zipContent): void
860+
{
861+
/** @var array<string, array<string, mixed>> $sheets */
862+
$sheets = $this->spreadSheet->getUnparsedLoadedData()['sheets'] ?? [];
863+
foreach ($sheets as $sheetData) {
864+
/** @var string[] $mediaFiles */
865+
$mediaFiles = $sheetData['drawingMediaFiles'] ?? [];
866+
/** @var ?string $sourceFile */
867+
$sourceFile = $sheetData['drawingSourceFile'] ?? null;
868+
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true || $mediaFiles === [] || !is_string($sourceFile) || !file_exists($sourceFile)) {
869+
continue;
870+
}
871+
872+
$sourceZip = new ZipArchive();
873+
if ($sourceZip->open($sourceFile) !== true) {
874+
continue; // @codeCoverageIgnore
875+
}
876+
877+
foreach ($mediaFiles as $mediaPath) {
878+
$zipPath = 'xl/media/' . basename($mediaPath);
879+
if (!isset($zipContent[$zipPath])) {
880+
$mediaContent = $sourceZip->getFromName($mediaPath);
881+
if ($mediaContent !== false) {
882+
$zipContent[$zipPath] = $mediaContent;
883+
}
884+
}
885+
}
886+
887+
$sourceZip->close();
888+
}
889+
}
846890
}

src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,33 @@ public function writeContentTypes(Spreadsheet $spreadsheet, bool $includeCharts
156156
$this->writeDefaultContentType($objWriter, $extension, $mimeType);
157157
}
158158
}
159+
160+
// Add pass-through media content types
161+
/** @var array<string, array<string, mixed>> $sheets */
162+
$sheets = $unparsedLoadedData['sheets'] ?? [];
163+
foreach ($sheets as $sheetData) {
164+
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true) {
165+
continue;
166+
}
167+
/** @var string[] $mediaFiles */
168+
$mediaFiles = $sheetData['drawingMediaFiles'] ?? [];
169+
foreach ($mediaFiles as $mediaPath) {
170+
$extension = strtolower(pathinfo($mediaPath, PATHINFO_EXTENSION));
171+
if ($extension !== '' && !isset($aMediaContentTypes[$extension])) {
172+
$mimeType = match ($extension) { // @phpstan-ignore match.unhandled
173+
'png' => 'image/png',
174+
'jpg', 'jpeg' => 'image/jpeg',
175+
'gif' => 'image/gif',
176+
'bmp' => 'image/bmp',
177+
'tif', 'tiff' => 'image/tiff',
178+
'svg' => 'image/svg+xml',
179+
};
180+
$aMediaContentTypes[$extension] = $mimeType;
181+
$this->writeDefaultContentType($objWriter, $extension, $mimeType);
182+
}
183+
}
184+
}
185+
159186
if ($spreadsheet->hasRibbonBinObjects()) {
160187
// Some additional objects in the ribbon ?
161188
// we need to write "Extension" but not already write for media content

src/PhpSpreadsheet/Writer/Xlsx/Drawing.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ class Drawing extends WriterPart
2323
*/
2424
public function writeDrawings(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, bool $includeCharts = false): string
2525
{
26+
// Try to use pass-through drawing XML if available
27+
if ($passThroughXml = $this->getPassThroughDrawingXml($worksheet)) {
28+
return $passThroughXml;
29+
}
30+
2631
// Create XML writer
2732
$objWriter = null;
2833
if ($this->getParentWriter()->getUseDiskCaching()) {
@@ -592,4 +597,27 @@ private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition,
592597
$objWriter->writeAttribute($attr, $val);
593598
}
594599
}
600+
601+
/**
602+
* Get pass-through drawing XML if available.
603+
*
604+
* Returns the original drawing XML stored during load (when Reader pass-through was enabled).
605+
* This preserves unsupported drawing elements (shapes, textboxes) that PhpSpreadsheet cannot parse.
606+
*
607+
* @return ?string The pass-through XML, or null if not available or should not be used
608+
*/
609+
private function getPassThroughDrawingXml(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): ?string
610+
{
611+
/** @var array<string, array<string, mixed>> $sheets */
612+
$sheets = $worksheet->getParentOrThrow()->getUnparsedLoadedData()['sheets'] ?? [];
613+
$sheetData = $sheets[$worksheet->getCodeName()] ?? [];
614+
// Only use pass-through XML if the Reader flag was explicitly enabled
615+
/** @var string[] $drawings */
616+
$drawings = $sheetData['Drawings'] ?? [];
617+
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true || $drawings === []) {
618+
return null;
619+
}
620+
621+
return reset($drawings) ?: null;
622+
}
595623
}

src/PhpSpreadsheet/Writer/Xlsx/Rels.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\W
345345
*/
346346
public function writeDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, int &$chartRef, bool $includeCharts = false): string
347347
{
348+
// Check if we should use pass-through relationships
349+
$passThroughRels = $this->getPassThroughDrawingRelationships($worksheet);
350+
if ($passThroughRels !== null) {
351+
return $passThroughRels;
352+
}
353+
348354
// Create XML writer
349355
$objWriter = null;
350356
if ($this->getParentWriter()->getUseDiskCaching()) {
@@ -523,4 +529,24 @@ private function writeDrawingHyperLink(XMLWriter $objWriter, BaseDrawing $drawin
523529

524530
return $i;
525531
}
532+
533+
/**
534+
* Get pass-through drawing relationships XML if available.
535+
*
536+
* Note: When pass-through is used, the original relationships are returned as-is.
537+
* This means any drawings (images, charts, shapes) added programmatically after
538+
* loading will not be included in the relationships. This is a known limitation
539+
* when combining pass-through with drawing modifications.
540+
*/
541+
private function getPassThroughDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): ?string
542+
{
543+
/** @var array<string, array<string, mixed>> $sheets */
544+
$sheets = $worksheet->getParentOrThrow()->getUnparsedLoadedData()['sheets'] ?? [];
545+
$sheetData = $sheets[$worksheet->getCodeName()] ?? [];
546+
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true || !is_string($sheetData['drawingRelationships'] ?? null)) {
547+
return null;
548+
}
549+
550+
return $sheetData['drawingRelationships'];
551+
}
526552
}

0 commit comments

Comments
 (0)