Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/PhpSpreadsheet/Reader/BaseReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ abstract class BaseReader implements IReader
*/
protected bool $createBlankSheetIfNoneRead = false;

/**
* Enable drawing pass-through?
* Identifies whether the Reader should preserve unsupported drawing elements (shapes, grouped images, etc.)
* by storing the original XML for pass-through during write operations.
* When enabled, drawings cannot be modified programmatically but are preserved exactly.
*/
protected bool $enableDrawingPassThrough = false;

/**
* IReadFilter instance.
*/
Expand Down Expand Up @@ -125,6 +133,18 @@ public function setIncludeCharts(bool $includeCharts): self
return $this;
}

public function getEnableDrawingPassThrough(): bool
{
return $this->enableDrawingPassThrough;
}

public function setEnableDrawingPassThrough(bool $enableDrawingPassThrough): self
{
$this->enableDrawingPassThrough = $enableDrawingPassThrough;

return $this;
}

/** @return null|string[] */
public function getLoadSheetsOnly(): ?array
{
Expand Down
23 changes: 23 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,29 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
$xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
$xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);

// Store drawing XML for pass-through if enabled
if ($this->enableDrawingPassThrough) {
$unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
// Mark that pass-through is enabled for this sheet
$sheetCodeName = $docSheet->getCodeName();
if (!isset($unparsedLoadedData['sheets']) || !is_array($unparsedLoadedData['sheets'])) {
$unparsedLoadedData['sheets'] = [];
}
if (!isset($unparsedLoadedData['sheets'][$sheetCodeName]) || !is_array($unparsedLoadedData['sheets'][$sheetCodeName])) {
$unparsedLoadedData['sheets'][$sheetCodeName] = [];
}
/** @var array<string, mixed> $sheetUnparsedData */
$sheetUnparsedData = &$unparsedLoadedData['sheets'][$sheetCodeName];
$sheetUnparsedData['drawingPassThroughEnabled'] = true;
// Store original drawing relationships for pass-through
if ($relsDrawing) {
$sheetUnparsedData['drawingRelationships'] = $relsDrawing->asXML();
}
// Store original media files paths and source file for pass-through
$sheetUnparsedData['drawingMediaFiles'] = $images;
$sheetUnparsedData['drawingSourceFile'] = File::realpath($filename);
}

if ($xmlDrawingChildren->oneCellAnchor) {
foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
$oneCellAnchor = self::testSimpleXml($oneCellAnchor);
Expand Down
46 changes: 45 additions & 1 deletion src/PhpSpreadsheet/Writer/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,9 @@ public function save($filename, int $flags = 0): void
}

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

Expand Down Expand Up @@ -558,6 +560,9 @@ public function save($filename, int $flags = 0): void
}
}

// Add pass-through media files (original media that may not be in the drawing collection)
$this->addPassThroughMediaFiles($zipContent); // @phpstan-ignore argument.type

Functions::setReturnDateType($saveDateReturnType);
Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);

Expand Down Expand Up @@ -843,4 +848,43 @@ public function getRestrictMaxColumnWidth(): bool
{
return $this->restrictMaxColumnWidth;
}

/**
* Add pass-through media files from original spreadsheet.
* This copies media files that are referenced in pass-through drawing XML
* but may not be in the drawing collection (e.g., unsupported formats like SVG).
*
* @param string[] $zipContent
*/
private function addPassThroughMediaFiles(array &$zipContent): void
{
/** @var array<string, array<string, mixed>> $sheets */
$sheets = $this->spreadSheet->getUnparsedLoadedData()['sheets'] ?? [];
foreach ($sheets as $sheetData) {
/** @var string[] $mediaFiles */
$mediaFiles = $sheetData['drawingMediaFiles'] ?? [];
/** @var ?string $sourceFile */
$sourceFile = $sheetData['drawingSourceFile'] ?? null;
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true || $mediaFiles === [] || !is_string($sourceFile) || !file_exists($sourceFile)) {
continue;
}

$sourceZip = new ZipArchive();
if ($sourceZip->open($sourceFile) !== true) {
continue; // @codeCoverageIgnore
}

foreach ($mediaFiles as $mediaPath) {
$zipPath = 'xl/media/' . basename($mediaPath);
if (!isset($zipContent[$zipPath])) {
$mediaContent = $sourceZip->getFromName($mediaPath);
if ($mediaContent !== false) {
$zipContent[$zipPath] = $mediaContent;
}
}
}

$sourceZip->close();
}
}
}
27 changes: 27 additions & 0 deletions src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,33 @@ public function writeContentTypes(Spreadsheet $spreadsheet, bool $includeCharts
$this->writeDefaultContentType($objWriter, $extension, $mimeType);
}
}

// Add pass-through media content types
/** @var array<string, array<string, mixed>> $sheets */
$sheets = $unparsedLoadedData['sheets'] ?? [];
foreach ($sheets as $sheetData) {
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true) {
continue;
}
/** @var string[] $mediaFiles */
$mediaFiles = $sheetData['drawingMediaFiles'] ?? [];
foreach ($mediaFiles as $mediaPath) {
$extension = strtolower(pathinfo($mediaPath, PATHINFO_EXTENSION));
if ($extension !== '' && !isset($aMediaContentTypes[$extension])) {
$mimeType = match ($extension) { // @phpstan-ignore match.unhandled
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
'tif', 'tiff' => 'image/tiff',
'svg' => 'image/svg+xml',
};
$aMediaContentTypes[$extension] = $mimeType;
$this->writeDefaultContentType($objWriter, $extension, $mimeType);
}
}
}

if ($spreadsheet->hasRibbonBinObjects()) {
// Some additional objects in the ribbon ?
// we need to write "Extension" but not already write for media content
Expand Down
28 changes: 28 additions & 0 deletions src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class Drawing extends WriterPart
*/
public function writeDrawings(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, bool $includeCharts = false): string
{
// Try to use pass-through drawing XML if available
if ($passThroughXml = $this->getPassThroughDrawingXml($worksheet)) {
return $passThroughXml;
}

// Create XML writer
$objWriter = null;
if ($this->getParentWriter()->getUseDiskCaching()) {
Expand Down Expand Up @@ -592,4 +597,27 @@ private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition,
$objWriter->writeAttribute($attr, $val);
}
}

/**
* Get pass-through drawing XML if available.
*
* Returns the original drawing XML stored during load (when Reader pass-through was enabled).
* This preserves unsupported drawing elements (shapes, textboxes) that PhpSpreadsheet cannot parse.
*
* @return ?string The pass-through XML, or null if not available or should not be used
*/
private function getPassThroughDrawingXml(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): ?string
{
/** @var array<string, array<string, mixed>> $sheets */
$sheets = $worksheet->getParentOrThrow()->getUnparsedLoadedData()['sheets'] ?? [];
$sheetData = $sheets[$worksheet->getCodeName()] ?? [];
// Only use pass-through XML if the Reader flag was explicitly enabled
/** @var string[] $drawings */
$drawings = $sheetData['Drawings'] ?? [];
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true || $drawings === []) {
return null;
}

return reset($drawings) ?: null;
}
}
26 changes: 26 additions & 0 deletions src/PhpSpreadsheet/Writer/Xlsx/Rels.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\W
*/
public function writeDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, int &$chartRef, bool $includeCharts = false): string
{
// Check if we should use pass-through relationships
$passThroughRels = $this->getPassThroughDrawingRelationships($worksheet);
if ($passThroughRels !== null) {
return $passThroughRels;
}

// Create XML writer
$objWriter = null;
if ($this->getParentWriter()->getUseDiskCaching()) {
Expand Down Expand Up @@ -523,4 +529,24 @@ private function writeDrawingHyperLink(XMLWriter $objWriter, BaseDrawing $drawin

return $i;
}

/**
* Get pass-through drawing relationships XML if available.
*
* Note: When pass-through is used, the original relationships are returned as-is.
* This means any drawings (images, charts, shapes) added programmatically after
* loading will not be included in the relationships. This is a known limitation
* when combining pass-through with drawing modifications.
*/
private function getPassThroughDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): ?string
{
/** @var array<string, array<string, mixed>> $sheets */
$sheets = $worksheet->getParentOrThrow()->getUnparsedLoadedData()['sheets'] ?? [];
$sheetData = $sheets[$worksheet->getCodeName()] ?? [];
if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true || !is_string($sheetData['drawingRelationships'] ?? null)) {
return null;
}

return $sheetData['drawingRelationships'];
}
}
Loading