Skip to content

Commit ed30a88

Browse files
committed
Add content types when pass-through
1 parent 7267164 commit ed30a88

File tree

4 files changed

+193
-1
lines changed

4 files changed

+193
-1
lines changed

src/PhpSpreadsheet/Writer/Xlsx.php

Lines changed: 3 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

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) {
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

tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingPassThroughTest.php

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,4 +677,167 @@ public function testDrawingPassThroughPreservesShapesAndSvg(): void
677677
$zip->close();
678678
unlink($tempFile);
679679
}
680+
681+
/**
682+
* Test that pass-through preserves grouped images.
683+
* Grouped images are not parsed into the drawing collection, so they would be lost
684+
* without proper pass-through of drawing XML, relationships, and media files.
685+
*
686+
* File structure of grouped_images.xlsx:
687+
* - Contains a group (xdr:grpSp) with 2 images inside
688+
* - xl/media/image1.png and xl/media/image2.png
689+
* - The images are referenced from within the group element
690+
*/
691+
public function testDrawingPassThroughPreservesGroupedImages(): void
692+
{
693+
$template = self::DIRECTORY . 'grouped_images.xlsx';
694+
695+
// Verify that the original file contains a group with images
696+
$originalZip = new ZipArchive();
697+
$originalZip->open($template);
698+
$originalDrawingXml = $originalZip->getFromName('xl/drawings/drawing1.xml');
699+
$originalRels = $originalZip->getFromName('xl/drawings/_rels/drawing1.xml.rels');
700+
$originalZip->close();
701+
702+
self::assertNotFalse($originalDrawingXml, 'Original file should have drawing XML');
703+
self::assertStringContainsString('<xdr:grpSp>', $originalDrawingXml, 'Original file should contain a group');
704+
self::assertStringContainsString('<xdr:pic>', $originalDrawingXml, 'Original file should contain images inside group');
705+
self::assertNotFalse($originalRels, 'Original file should have drawing relationships');
706+
self::assertStringContainsString('image1.png', $originalRels, 'Original rels should reference image1.png');
707+
self::assertStringContainsString('image2.bmp', $originalRels, 'Original rels should reference image2.bmp');
708+
self::assertStringContainsString('image4.gif', $originalRels, 'Original rels should reference image4.gif');
709+
self::assertStringContainsString('image6.svg', $originalRels, 'Original rels should reference image6.svg');
710+
711+
// Load with pass-through enabled
712+
$reader = new XlsxReader();
713+
$reader->setEnableDrawingPassThrough(true);
714+
$spreadsheet = $reader->load($template);
715+
716+
$sheet = $spreadsheet->getActiveSheet();
717+
718+
// Verify that drawing collection is empty (grouped images are not parsed)
719+
$drawings = $sheet->getDrawingCollection();
720+
self::assertCount(0, $drawings, 'Drawing collection should be empty (grouped images are not parsed)');
721+
722+
// Verify that pass-through data is stored
723+
$unparsedData = $spreadsheet->getUnparsedLoadedData();
724+
$codeName = $sheet->getCodeName();
725+
self::assertArrayHasKey('sheets', $unparsedData);
726+
self::assertIsArray($unparsedData['sheets']);
727+
self::assertArrayHasKey($codeName, $unparsedData['sheets']);
728+
self::assertIsArray($unparsedData['sheets'][$codeName]);
729+
self::assertTrue($unparsedData['sheets'][$codeName]['drawingPassThroughEnabled'] ?? false, 'Pass-through should be enabled');
730+
731+
// Verify that drawing relationships are stored
732+
self::assertArrayHasKey('drawingRelationships', $unparsedData['sheets'][$codeName], 'Drawing relationships should be stored');
733+
734+
// Verify that media files paths are stored
735+
self::assertArrayHasKey('drawingMediaFiles', $unparsedData['sheets'][$codeName], 'Media files should be stored');
736+
$mediaFiles = $unparsedData['sheets'][$codeName]['drawingMediaFiles'];
737+
self::assertIsArray($mediaFiles);
738+
self::assertGreaterThanOrEqual(2, count($mediaFiles), 'Should have at least 2 media files');
739+
740+
// Save to file
741+
$tempFile = File::temporaryFilename();
742+
$writer = new XlsxWriter($spreadsheet);
743+
$writer->save($tempFile);
744+
$spreadsheet->disconnectWorksheets();
745+
746+
// Verify that the saved file preserves the group and images
747+
$zip = new ZipArchive();
748+
$zip->open($tempFile);
749+
750+
// Check that drawing XML contains the group
751+
$drawingXml = $zip->getFromName('xl/drawings/drawing1.xml');
752+
self::assertNotFalse($drawingXml, 'Drawing XML should exist in saved file');
753+
self::assertStringContainsString('<xdr:grpSp>', $drawingXml, 'Group should be preserved');
754+
self::assertStringContainsString('<xdr:pic>', $drawingXml, 'Images inside group should be preserved');
755+
756+
// Check that drawing relationships are preserved
757+
$drawingRels = $zip->getFromName('xl/drawings/_rels/drawing1.xml.rels');
758+
self::assertNotFalse($drawingRels, 'Drawing relationships file should exist');
759+
self::assertStringContainsString('image1.png', $drawingRels, 'Drawing rels should reference image1.png');
760+
self::assertStringContainsString('image2.bmp', $drawingRels, 'Drawing rels should reference image2.bmp');
761+
self::assertStringContainsString('image4.gif', $drawingRels, 'Drawing rels should reference image4.gif');
762+
self::assertStringContainsString('image6.svg', $drawingRels, 'Drawing rels should reference image6.svg');
763+
764+
// Check that media files are present
765+
$image1Content = $zip->getFromName('xl/media/image1.png');
766+
$image2Content = $zip->getFromName('xl/media/image2.bmp');
767+
$image4Content = $zip->getFromName('xl/media/image4.gif');
768+
$image6Content = $zip->getFromName('xl/media/image6.svg');
769+
self::assertNotFalse($image1Content, 'image1.png should be preserved');
770+
self::assertNotFalse($image2Content, 'image2.bmp should be preserved');
771+
self::assertNotFalse($image4Content, 'image4.gif should be preserved');
772+
self::assertNotFalse($image6Content, 'image6.svg should be preserved');
773+
774+
// Check that Content_Types.xml contains all image extensions
775+
$contentTypes = $zip->getFromName('[Content_Types].xml');
776+
self::assertNotFalse($contentTypes, 'Content_Types.xml should exist');
777+
self::assertStringContainsString('image/png', $contentTypes, 'Content_Types should declare PNG mime type');
778+
self::assertStringContainsString('image/bmp', $contentTypes, 'Content_Types should declare BMP mime type');
779+
self::assertStringContainsString('image/gif', $contentTypes, 'Content_Types should declare GIF mime type');
780+
self::assertStringContainsString('image/svg+xml', $contentTypes, 'Content_Types should declare SVG mime type');
781+
782+
$zip->close();
783+
unlink($tempFile);
784+
}
785+
786+
/**
787+
* Test that WITHOUT pass-through, grouped images are lost.
788+
* This documents the expected behavior without the fix.
789+
*/
790+
public function testWithoutPassThroughGroupedImagesAreLost(): void
791+
{
792+
$template = self::DIRECTORY . 'grouped_images.xlsx';
793+
794+
// Verify that the original file contains grouped images
795+
$originalZip = new ZipArchive();
796+
$originalZip->open($template);
797+
$originalDrawingXml = $originalZip->getFromName('xl/drawings/drawing1.xml');
798+
$originalZip->close();
799+
self::assertStringContainsString('<xdr:grpSp>', $originalDrawingXml, 'Original file should contain a group');
800+
801+
// Load WITHOUT pass-through
802+
$reader = new XlsxReader();
803+
// Don't enable pass-through
804+
$spreadsheet = $reader->load($template);
805+
806+
$sheet = $spreadsheet->getActiveSheet();
807+
808+
// Verify that drawing collection is empty (grouped images are not parsed)
809+
$drawings = $sheet->getDrawingCollection();
810+
self::assertCount(0, $drawings, 'Drawing collection should be empty without pass-through too');
811+
812+
// Save to file
813+
$tempFile = File::temporaryFilename();
814+
$writer = new XlsxWriter($spreadsheet);
815+
$writer->save($tempFile);
816+
$spreadsheet->disconnectWorksheets();
817+
818+
// Verify that the group and images are lost
819+
$zip = new ZipArchive();
820+
$zip->open($tempFile);
821+
822+
// Drawing XML should not exist or not contain the group
823+
$drawingXml = $zip->getFromName('xl/drawings/drawing1.xml');
824+
825+
if ($drawingXml !== false) {
826+
// If drawing XML exists, it should NOT contain the group
827+
self::assertStringNotContainsString('<xdr:grpSp>', $drawingXml, 'Group should be lost without pass-through');
828+
}
829+
830+
// Media files should NOT be present (nothing was in the drawing collection)
831+
$image1Content = $zip->getFromName('xl/media/image1.png');
832+
$image2Content = $zip->getFromName('xl/media/image2.bmp');
833+
$image4Content = $zip->getFromName('xl/media/image4.gif');
834+
$image6Content = $zip->getFromName('xl/media/image6.svg');
835+
self::assertFalse($image1Content, 'image1.png should NOT be present without pass-through');
836+
self::assertFalse($image2Content, 'image2.bmp should NOT be present without pass-through');
837+
self::assertFalse($image4Content, 'image4.gif should NOT be present without pass-through');
838+
self::assertFalse($image6Content, 'image6.svg should NOT be present without pass-through');
839+
840+
$zip->close();
841+
unlink($tempFile);
842+
}
680843
}
21.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)