setEnableDrawingPassThrough(true); $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); // Verify that drawing collection contains only the image (supported element) $drawings = $sheet->getDrawingCollection(); self::assertCount(1, $drawings, 'Drawing collection should contain only the image (supported element)'); // Verify that unparsed data contains the original drawing XML with shapes $unparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); self::assertArrayHasKey('sheets', $unparsedData); self::assertIsArray($unparsedData['sheets']); self::assertArrayHasKey($codeName, $unparsedData['sheets']); self::assertIsArray($unparsedData['sheets'][$codeName]); self::assertArrayHasKey('Drawings', $unparsedData['sheets'][$codeName]); // Verify that the drawing XML contains shapes and textboxes self::assertIsArray($unparsedData['sheets'][$codeName]['Drawings']); $drawings = $unparsedData['sheets'][$codeName]['Drawings']; $originalDrawingXml = reset($drawings); self::assertIsString($originalDrawingXml); self::assertStringContainsString('', $originalDrawingXml, 'Original XML should contain textbox element'); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify that the saved XLSX file contains shapes by reading the drawing XML directly $zip = new ZipArchive(); $zip->open($tempFile); $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); $zip->close(); unlink($tempFile); self::assertNotFalse($drawingXml, 'Drawing XML should exist in saved file'); self::assertStringContainsString('', $drawingXml, 'Shapes should be preserved in saved file'); self::assertStringContainsString('', $drawingXml, 'Textboxes should be preserved in saved file'); } /** * Test that WITHOUT Reader pass-through flag, shapes are NOT stored and are LOST. * This test uses a file with both an image (supported) and a shape (unsupported). */ public function testWithoutReaderPassThroughShapesAreLost(): void { // First, verify that the original file contains a shape // Load WITH pass-through to check file contents $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load(self::TEMPLATE); $unparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $spreadsheet->getActiveSheet()->getCodeName(); self::assertIsArray($unparsedData['sheets']); self::assertArrayHasKey($codeName, $unparsedData['sheets']); self::assertIsArray($unparsedData['sheets'][$codeName]); self::assertArrayHasKey('Drawings', $unparsedData['sheets'][$codeName], 'Original file should have drawings'); self::assertIsArray($unparsedData['sheets'][$codeName]['Drawings']); $drawings = $unparsedData['sheets'][$codeName]['Drawings']; $drawingXml = reset($drawings); self::assertIsString($drawingXml); self::assertStringContainsString('disconnectWorksheets(); // Now test: Load WITHOUT Reader pass-through (XML not stored) $reader = new XlsxReader(); // Don't enable pass-through! $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); // Verify that image is in collection (supported element) $drawings = $sheet->getDrawingCollection(); self::assertGreaterThan(0, count($drawings), 'Drawing collection should contain the image'); // Verify that shape XML is NOT stored (because pass-through disabled) $unparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); self::assertIsArray($unparsedData['sheets']); $sheetData = $unparsedData['sheets'][$codeName] ?? []; self::assertArrayNotHasKey('Drawings', $sheetData, 'Drawings should NOT be stored without Reader pass-through flag'); // Save to file $writer = new XlsxWriter($spreadsheet); $tempFile = File::temporaryFilename(); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify that shape is LOST by reading the drawing XML directly $zip = new ZipArchive(); $zip->open($tempFile); $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); $zip->close(); unlink($tempFile); // The saved file should have drawing XML (for the image) but NOT the shape or textbox self::assertNotFalse($drawingXml, 'Drawing XML should exist (for the image)'); self::assertStringNotContainsString('', $drawingXml, 'Shape should be lost without Reader pass-through'); self::assertStringNotContainsString('', $drawingXml, 'Textbox should be lost without Reader pass-through'); } /** * Test that pass-through preserves drawings when a comment is deleted. * Comments are independent from drawings, so deleting a comment should not affect drawings. */ public function testDrawingPassThroughWithCommentDeletion(): void { // Load with pass-through enabled $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); // Verify that image is in collection $drawings = $sheet->getDrawingCollection(); self::assertGreaterThan(0, count($drawings), 'Drawing collection should contain the image'); // Verify that shapes and textboxes are in unparsed data $unparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); self::assertIsArray($unparsedData['sheets']); self::assertArrayHasKey($codeName, $unparsedData['sheets']); self::assertIsArray($unparsedData['sheets'][$codeName]); self::assertArrayHasKey('Drawings', $unparsedData['sheets'][$codeName]); self::assertIsArray($unparsedData['sheets'][$codeName]['Drawings']); $drawings = $unparsedData['sheets'][$codeName]['Drawings']; $originalDrawingXml = reset($drawings); self::assertIsString($originalDrawingXml); self::assertStringContainsString('', $originalDrawingXml, 'Original XML should contain textbox'); // Verify that a comment exists and delete it $comments = $sheet->getComments(); self::assertGreaterThan(0, count($comments), 'Original file should have at least one comment'); $firstCommentCell = array_key_first($comments); self::assertIsString($firstCommentCell, 'Comment cell should be a string'); $originalCommentText = $sheet->getComment($firstCommentCell)->getText()->getPlainText(); self::assertNotEmpty($originalCommentText, 'Comment should have text'); // Delete the comment $sheet->removeComment($firstCommentCell); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify that shapes are still present and comment was deleted $zip = new ZipArchive(); $zip->open($tempFile); $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); $commentsXml = $zip->getFromName('xl/comments1.xml'); $zip->close(); unlink($tempFile); self::assertNotFalse($drawingXml, 'Drawing XML should exist in saved file'); self::assertStringContainsString('', $drawingXml, 'Shapes should be preserved after comment deletion'); self::assertStringContainsString('', $drawingXml, 'Textboxes should be preserved after comment deletion'); // Verify that comment was deleted (comments XML should not exist or not contain the original comment) if ($commentsXml !== false) { self::assertStringNotContainsString($originalCommentText, $commentsXml, 'Original comment text should be deleted'); } } /** * Test that WITH pass-through, drawing modifications are NOT applied. * When pass-through is enabled, the Writer uses the stored XML instead of regenerating, * so programmatic changes to drawings are ignored. */ public function testWithPassThroughDrawingModificationsAreIgnored(): void { // Load WITH pass-through $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); // Verify that image is in collection $drawings = $sheet->getDrawingCollection(); self::assertGreaterThan(0, count($drawings), 'Drawing collection should contain the image'); // Modify the drawing (change description) $drawing = null; foreach ($drawings as $d) { $drawing = $d; break; } self::assertNotNull($drawing, 'Should have at least one drawing'); $originalDescription = $drawing->getDescription(); $newDescription = 'Modified description by test'; $drawing->setDescription($newDescription); self::assertNotSame($originalDescription, $newDescription, 'Description should be different'); // Save to file (with pass-through, Writer uses stored XML, modifications ignored) $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Reload and verify that the modification was NOT applied (original description preserved) $reloadReader = new XlsxReader(); $reloadedSpreadsheet = $reloadReader->load($tempFile); unlink($tempFile); $reloadedDrawings = $reloadedSpreadsheet->getActiveSheet()->getDrawingCollection(); self::assertGreaterThan(0, count($reloadedDrawings), 'Reloaded file should have drawings'); $reloadedDrawing = null; foreach ($reloadedDrawings as $d) { $reloadedDrawing = $d; break; } self::assertNotNull($reloadedDrawing, 'Should have at least one reloaded drawing'); self::assertSame($originalDescription, $reloadedDrawing->getDescription(), 'Original description should be preserved (modification ignored with pass-through)'); self::assertNotSame($newDescription, $reloadedDrawing->getDescription(), 'Modified description should NOT be applied with pass-through'); $reloadedSpreadsheet->disconnectWorksheets(); } /** * Test that pass-through preserves drawings when columns are inserted, * but coordinates are NOT adjusted. */ public function testDrawingPassThroughWithColumnInsertion(): void { // Load with pass-through enabled $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); // Get original drawing coordinates from XML $originalUnparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); self::assertIsArray($originalUnparsedData['sheets']); self::assertArrayHasKey($codeName, $originalUnparsedData['sheets']); self::assertIsArray($originalUnparsedData['sheets'][$codeName]); self::assertArrayHasKey('Drawings', $originalUnparsedData['sheets'][$codeName]); self::assertIsArray($originalUnparsedData['sheets'][$codeName]['Drawings']); $originalDrawings = $originalUnparsedData['sheets'][$codeName]['Drawings']; $originalDrawingXml = reset($originalDrawings); self::assertIsString($originalDrawingXml); // Extract original column coordinate preg_match('/(\d+)<\/xdr:col>/', $originalDrawingXml, $originalColMatches); $originalCol = $originalColMatches[1] ?? null; self::assertNotNull($originalCol, 'Original drawing should have column coordinate'); // Insert a column before B (which should shift drawings at B or later) $sheet->insertNewColumnBefore('B', 1); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Read the drawing XML directly from the saved file $zip = new ZipArchive(); $zip->open($tempFile); $reloadedDrawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); $zip->close(); unlink($tempFile); self::assertNotFalse($reloadedDrawingXml, 'Drawing XML should exist in saved file'); // Extract reloaded column coordinate preg_match('/(\d+)<\/xdr:col>/', $reloadedDrawingXml, $reloadedColMatches); $reloadedCol = $reloadedColMatches[1] ?? null; // Coordinates are NOT adjusted // The column coordinate should remain the same (not shifted) self::assertSame($originalCol, $reloadedCol, 'Drawing column coordinate should NOT be adjusted'); } /** * Test that pass-through preserves drawings when rows are deleted, * but coordinates are NOT adjusted. */ public function testDrawingPassThroughWithRowDeletion(): void { // Load with pass-through enabled $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); // Get original drawing coordinates from XML $originalUnparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); self::assertIsArray($originalUnparsedData['sheets']); self::assertArrayHasKey($codeName, $originalUnparsedData['sheets']); self::assertIsArray($originalUnparsedData['sheets'][$codeName]); self::assertArrayHasKey('Drawings', $originalUnparsedData['sheets'][$codeName]); self::assertIsArray($originalUnparsedData['sheets'][$codeName]['Drawings']); $originalDrawings = $originalUnparsedData['sheets'][$codeName]['Drawings']; $originalDrawingXml = reset($originalDrawings); self::assertIsString($originalDrawingXml); // Extract original row coordinate preg_match('/(\d+)<\/xdr:row>/', $originalDrawingXml, $originalRowMatches); $originalRow = $originalRowMatches[1] ?? null; self::assertNotNull($originalRow, 'Original drawing should have row coordinate'); // Delete row 1 (which should shift drawings at row 2 or later) $sheet->removeRow(1, 1); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Read the drawing XML directly from the saved file $zip = new ZipArchive(); $zip->open($tempFile); $reloadedDrawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); $zip->close(); unlink($tempFile); self::assertNotFalse($reloadedDrawingXml, 'Drawing XML should exist in saved file'); // Extract reloaded row coordinate preg_match('/(\d+)<\/xdr:row>/', $reloadedDrawingXml, $reloadedRowMatches); $reloadedRow = $reloadedRowMatches[1] ?? null; // Coordinates are NOT adjusted // The row coordinate should remain the same (not shifted) self::assertSame($originalRow, $reloadedRow, 'Drawing row coordinate should NOT be adjusted after row deletion'); } public function testDrawingPassThroughGetterSetter(): void { // Test Reader getter/setter $reader = new XlsxReader(); // Default should be false self::assertFalse($reader->getEnableDrawingPassThrough()); // Enable pass-through $result = $reader->setEnableDrawingPassThrough(true); self::assertTrue($reader->getEnableDrawingPassThrough()); // Disable pass-through $reader->setEnableDrawingPassThrough(false); self::assertFalse($reader->getEnableDrawingPassThrough()); } /** * Test that the drawingPassThroughEnabled flag is correctly set in unparsedLoadedData. * This verifies the Reader sets the flag and the Writer's getPassThroughDrawingXml checks it. */ public function testDrawingPassThroughEnabledFlagIsSetCorrectly(): void { // Test 1: Load WITHOUT pass-through (default) $reader = new XlsxReader(); self::assertFalse($reader->getEnableDrawingPassThrough(), 'Pass-through should be disabled by default'); $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); $unparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); // Verify that drawingPassThroughEnabled flag is NOT set when pass-through is disabled self::assertArrayHasKey('sheets', $unparsedData); self::assertIsArray($unparsedData['sheets']); // The sheet may exist in unparsedData (legacy empty drawings), but the flag should be absent or false if (isset($unparsedData['sheets'][$codeName])) { $sheetData = $unparsedData['sheets'][$codeName]; self::assertIsArray($sheetData); $flag = $sheetData['drawingPassThroughEnabled'] ?? false; self::assertFalse($flag, 'drawingPassThroughEnabled should be false/absent when pass-through is disabled'); } $spreadsheet->disconnectWorksheets(); // Test 2: Load WITH pass-through enabled $reader2 = new XlsxReader(); $reader2->setEnableDrawingPassThrough(true); self::assertTrue($reader2->getEnableDrawingPassThrough(), 'Pass-through should be enabled'); $spreadsheet2 = $reader2->load(self::TEMPLATE); $sheet2 = $spreadsheet2->getActiveSheet(); $unparsedData2 = $spreadsheet2->getUnparsedLoadedData(); $codeName2 = $sheet2->getCodeName(); // Verify that drawingPassThroughEnabled flag IS set when pass-through is enabled self::assertArrayHasKey('sheets', $unparsedData2); self::assertIsArray($unparsedData2['sheets']); self::assertArrayHasKey($codeName2, $unparsedData2['sheets']); self::assertIsArray($unparsedData2['sheets'][$codeName2]); self::assertArrayHasKey('drawingPassThroughEnabled', $unparsedData2['sheets'][$codeName2], 'drawingPassThroughEnabled flag should exist'); self::assertTrue($unparsedData2['sheets'][$codeName2]['drawingPassThroughEnabled'], 'drawingPassThroughEnabled should be true when pass-through is enabled'); // Verify that the drawing XML is also stored self::assertArrayHasKey('Drawings', $unparsedData2['sheets'][$codeName2]); self::assertIsArray($unparsedData2['sheets'][$codeName2]['Drawings']); self::assertNotEmpty($unparsedData2['sheets'][$codeName2]['Drawings'], 'Drawing XML should be stored when pass-through is enabled'); $spreadsheet2->disconnectWorksheets(); } /** * Test that VML drawings (used by comments) and DrawingML (used by shapes/images) * coexist without interference when pass-through is enabled. * This addresses the concern that comments use the drawings folder with VML files. * The template file already contains a comment in D1, so this test verifies that * existing comments are preserved AND new comments can be added. */ public function testCommentsAndPassThroughCoexist(): void { // Load file with drawings (image + shape) and enable pass-through // Note: The template already contains a comment in D1 $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load(self::TEMPLATE); $sheet = $spreadsheet->getActiveSheet(); // Verify the existing comment is loaded $existingComment = $sheet->getComment('D1'); $existingCommentText = $existingComment->getText()->getPlainText(); self::assertNotEmpty($existingCommentText, 'Template should contain a comment in D1'); // Add a new comment to the sheet $sheet->getComment('A1')->getText()->createText('Test comment with pass-through'); $sheet->getComment('A1')->setAuthor('Test Author'); // Save the file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify the file structure contains both VML (for comments) and DrawingML (for shapes) $zip = new ZipArchive(); $zip->open($tempFile); // Check for VML drawing (used by comments) $vmlDrawing = $zip->getFromName('xl/drawings/vmlDrawing1.vml'); self::assertNotFalse($vmlDrawing, 'VML drawing for comments should exist'); self::assertStringContainsString('urn:schemas-microsoft-com:vml', $vmlDrawing, 'VML should contain VML namespace'); // Check for DrawingML (used by shapes/images) $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); self::assertNotFalse($drawingXml, 'DrawingML for shapes/images should exist'); self::assertStringContainsString('getFromName('xl/comments1.xml'); self::assertNotFalse($commentsXml, 'Comments XML should exist'); self::assertStringContainsString('Test comment with pass-through', $commentsXml, 'New comment (A1) should be in comments XML'); self::assertStringContainsString($existingCommentText, $commentsXml, 'Existing comment (D1) should be preserved in comments XML'); $zip->close(); unlink($tempFile); } /** * Test that pass-through preserves SVG images and their relationships. * Excel stores SVG images with a PNG fallback using separate rIds. * Without proper pass-through of relationships and media files, the rIds * become misaligned and images break. * * File structure of merge.excel.xlsx: * - xl/media/image1.png (PNG fallback for SVG) * - xl/media/image2.svg (SVG image) * - xl/media/image3.jpeg (JPEG image) * - Drawing relationships: rId1=PNG, rId2=SVG, rId3=JPEG */ public function testDrawingPassThroughPreservesSvgImagesAndRelationships(): void { $template = self::DIRECTORY . 'merge.excel.xlsx'; // Load with pass-through enabled $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load($template); $sheet = $spreadsheet->getActiveSheet(); // Verify that drawing collection contains supported images (PNG and JPEG, not SVG) $drawings = $sheet->getDrawingCollection(); self::assertGreaterThanOrEqual(1, count($drawings), 'Drawing collection should contain at least one supported image'); // Verify that pass-through data is stored $unparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); self::assertArrayHasKey('sheets', $unparsedData); self::assertIsArray($unparsedData['sheets']); self::assertArrayHasKey($codeName, $unparsedData['sheets']); self::assertIsArray($unparsedData['sheets'][$codeName]); self::assertTrue($unparsedData['sheets'][$codeName]['drawingPassThroughEnabled'] ?? false); // Verify that drawing relationships are stored self::assertArrayHasKey('drawingRelationships', $unparsedData['sheets'][$codeName]); $relsXml = $unparsedData['sheets'][$codeName]['drawingRelationships']; self::assertIsString($relsXml); self::assertStringContainsString('image1.png', $relsXml, 'Relationships should reference PNG'); self::assertStringContainsString('image2.svg', $relsXml, 'Relationships should reference SVG'); self::assertStringContainsString('image3.jpeg', $relsXml, 'Relationships should reference JPEG'); // Verify that media files paths are stored self::assertArrayHasKey('drawingMediaFiles', $unparsedData['sheets'][$codeName]); $mediaFiles = $unparsedData['sheets'][$codeName]['drawingMediaFiles']; self::assertIsArray($mediaFiles); self::assertGreaterThanOrEqual(3, count($mediaFiles), 'Should have at least 3 media files (PNG, SVG, JPEG)'); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify that the saved file preserves all images and relationships $zip = new ZipArchive(); $zip->open($tempFile); // Check that all media files are present $pngContent = $zip->getFromName('xl/media/image1.png'); $svgContent = $zip->getFromName('xl/media/image2.svg'); $jpegContent = $zip->getFromName('xl/media/image3.jpeg'); self::assertNotFalse($pngContent, 'PNG image should be preserved'); self::assertNotFalse($svgContent, 'SVG image should be preserved'); self::assertNotFalse($jpegContent, 'JPEG image should be preserved'); // Check that drawing relationships are correct $drawingRels = $zip->getFromName('xl/drawings/_rels/drawing1.xml.rels'); self::assertNotFalse($drawingRels, 'Drawing relationships file should exist'); self::assertStringContainsString('image1.png', $drawingRels, 'Drawing rels should reference PNG'); self::assertStringContainsString('image2.svg', $drawingRels, 'Drawing rels should reference SVG'); self::assertStringContainsString('image3.jpeg', $drawingRels, 'Drawing rels should reference JPEG'); // Check that drawing XML references are intact $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); self::assertNotFalse($drawingXml, 'Drawing XML should exist'); // Verify SVG is referenced via svgBlip extension self::assertStringContainsString('svgBlip', $drawingXml, 'Drawing should contain SVG blip reference'); $zip->close(); unlink($tempFile); } /** * Test that WITHOUT pass-through, SVG images are lost due to rId misalignment. * This documents the expected behavior without the fix. */ public function testWithoutPassThroughSvgImagesAreLost(): void { $template = self::DIRECTORY . 'merge.excel.xlsx'; // Verify that the original file contains SVG $originalZip = new ZipArchive(); $originalZip->open($template); $originalSvgContent = $originalZip->getFromName('xl/media/image2.svg'); $originalZip->close(); self::assertNotFalse($originalSvgContent, 'Original file should contain SVG'); // Load WITHOUT pass-through $reader = new XlsxReader(); // Don't enable pass-through $spreadsheet = $reader->load($template); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify that SVG is lost $zip = new ZipArchive(); $zip->open($tempFile); // SVG file should NOT be present (PhpSpreadsheet doesn't support SVG) $svgContent = $zip->getFromName('xl/media/image2.svg'); self::assertFalse($svgContent, 'SVG image should NOT be present without pass-through'); // PNG and JPEG should still be present (supported formats) // Note: filenames may be different as they are regenerated $mediaFiles = []; for ($i = 0; $i < $zip->numFiles; ++$i) { $name = $zip->getNameIndex($i); if ($name !== false && str_starts_with($name, 'xl/media/')) { $mediaFiles[] = $name; } } // Should have some media files but not SVG self::assertNotEmpty($mediaFiles, 'Should have some media files'); foreach ($mediaFiles as $file) { self::assertStringNotContainsString('.svg', $file, 'No SVG files should be present without pass-through'); } $zip->close(); unlink($tempFile); } /** * Test that pass-through preserves shapes (textboxes, rectangles, etc.) * in addition to images with SVG. */ public function testDrawingPassThroughPreservesShapesAndSvg(): void { $template = self::DIRECTORY . 'merge.excel.xlsx'; // Load with pass-through enabled $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load($template); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify shapes are preserved $zip = new ZipArchive(); $zip->open($tempFile); $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); self::assertNotFalse($drawingXml, 'Drawing XML should exist'); // Check for shapes (xdr:sp elements) self::assertStringContainsString('', $drawingXml, 'Textboxes should be preserved'); // Check for images (xdr:pic elements) self::assertStringContainsString('close(); unlink($tempFile); } /** * Test that pass-through preserves grouped images. * Grouped images are not parsed into the drawing collection, so they would be lost * without proper pass-through of drawing XML, relationships, and media files. * * File structure of grouped_images.xlsx: * - Contains a group (xdr:grpSp) with 2 images inside * - xl/media/image1.png and xl/media/image2.png * - The images are referenced from within the group element */ public function testDrawingPassThroughPreservesGroupedImages(): void { $template = self::DIRECTORY . 'grouped_images.xlsx'; // Verify that the original file contains a group with images $originalZip = new ZipArchive(); $originalZip->open($template); $originalDrawingXml = $originalZip->getFromName('xl/drawings/drawing1.xml'); $originalRels = $originalZip->getFromName('xl/drawings/_rels/drawing1.xml.rels'); $originalZip->close(); self::assertNotFalse($originalDrawingXml, 'Original file should have drawing XML'); self::assertStringContainsString('', $originalDrawingXml, 'Original file should contain a group'); self::assertStringContainsString('', $originalDrawingXml, 'Original file should contain images inside group'); self::assertNotFalse($originalRels, 'Original file should have drawing relationships'); self::assertStringContainsString('image1.png', $originalRels, 'Original rels should reference image1.png'); self::assertStringContainsString('image2.bmp', $originalRels, 'Original rels should reference image2.bmp'); self::assertStringContainsString('image4.gif', $originalRels, 'Original rels should reference image4.gif'); self::assertStringContainsString('image6.svg', $originalRels, 'Original rels should reference image6.svg'); // Load with pass-through enabled $reader = new XlsxReader(); $reader->setEnableDrawingPassThrough(true); $spreadsheet = $reader->load($template); $sheet = $spreadsheet->getActiveSheet(); // Verify that drawing collection is empty (grouped images are not parsed) $drawings = $sheet->getDrawingCollection(); self::assertCount(0, $drawings, 'Drawing collection should be empty (grouped images are not parsed)'); // Verify that pass-through data is stored $unparsedData = $spreadsheet->getUnparsedLoadedData(); $codeName = $sheet->getCodeName(); self::assertArrayHasKey('sheets', $unparsedData); self::assertIsArray($unparsedData['sheets']); self::assertArrayHasKey($codeName, $unparsedData['sheets']); self::assertIsArray($unparsedData['sheets'][$codeName]); self::assertTrue($unparsedData['sheets'][$codeName]['drawingPassThroughEnabled'] ?? false, 'Pass-through should be enabled'); // Verify that drawing relationships are stored self::assertArrayHasKey('drawingRelationships', $unparsedData['sheets'][$codeName], 'Drawing relationships should be stored'); // Verify that media files paths are stored self::assertArrayHasKey('drawingMediaFiles', $unparsedData['sheets'][$codeName], 'Media files should be stored'); $mediaFiles = $unparsedData['sheets'][$codeName]['drawingMediaFiles']; self::assertIsArray($mediaFiles); self::assertGreaterThanOrEqual(2, count($mediaFiles), 'Should have at least 2 media files'); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify that the saved file preserves the group and images $zip = new ZipArchive(); $zip->open($tempFile); // Check that drawing XML contains the group $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); self::assertNotFalse($drawingXml, 'Drawing XML should exist in saved file'); self::assertStringContainsString('', $drawingXml, 'Group should be preserved'); self::assertStringContainsString('', $drawingXml, 'Images inside group should be preserved'); // Check that drawing relationships are preserved $drawingRels = $zip->getFromName('xl/drawings/_rels/drawing1.xml.rels'); self::assertNotFalse($drawingRels, 'Drawing relationships file should exist'); self::assertStringContainsString('image1.png', $drawingRels, 'Drawing rels should reference image1.png'); self::assertStringContainsString('image2.bmp', $drawingRels, 'Drawing rels should reference image2.bmp'); self::assertStringContainsString('image4.gif', $drawingRels, 'Drawing rels should reference image4.gif'); self::assertStringContainsString('image6.svg', $drawingRels, 'Drawing rels should reference image6.svg'); // Check that media files are present $image1Content = $zip->getFromName('xl/media/image1.png'); $image2Content = $zip->getFromName('xl/media/image2.bmp'); $image4Content = $zip->getFromName('xl/media/image4.gif'); $image6Content = $zip->getFromName('xl/media/image6.svg'); self::assertNotFalse($image1Content, 'image1.png should be preserved'); self::assertNotFalse($image2Content, 'image2.bmp should be preserved'); self::assertNotFalse($image4Content, 'image4.gif should be preserved'); self::assertNotFalse($image6Content, 'image6.svg should be preserved'); // Check that Content_Types.xml contains all image extensions $contentTypes = $zip->getFromName('[Content_Types].xml'); self::assertNotFalse($contentTypes, 'Content_Types.xml should exist'); self::assertStringContainsString('image/png', $contentTypes, 'Content_Types should declare PNG mime type'); self::assertStringContainsString('image/bmp', $contentTypes, 'Content_Types should declare BMP mime type'); self::assertStringContainsString('image/gif', $contentTypes, 'Content_Types should declare GIF mime type'); self::assertStringContainsString('image/svg+xml', $contentTypes, 'Content_Types should declare SVG mime type'); $zip->close(); unlink($tempFile); } /** * Test that WITHOUT pass-through, grouped images are lost. * This documents the expected behavior without the fix. */ public function testWithoutPassThroughGroupedImagesAreLost(): void { $template = self::DIRECTORY . 'grouped_images.xlsx'; // Verify that the original file contains grouped images $originalZip = new ZipArchive(); $originalZip->open($template); $originalDrawingXml = $originalZip->getFromName('xl/drawings/drawing1.xml'); $originalZip->close(); self::assertStringContainsString('', $originalDrawingXml, 'Original file should contain a group'); // Load WITHOUT pass-through $reader = new XlsxReader(); // Don't enable pass-through $spreadsheet = $reader->load($template); $sheet = $spreadsheet->getActiveSheet(); // Verify that drawing collection is empty (grouped images are not parsed) $drawings = $sheet->getDrawingCollection(); self::assertCount(0, $drawings, 'Drawing collection should be empty without pass-through too'); // Save to file $tempFile = File::temporaryFilename(); $writer = new XlsxWriter($spreadsheet); $writer->save($tempFile); $spreadsheet->disconnectWorksheets(); // Verify that the group and images are lost $zip = new ZipArchive(); $zip->open($tempFile); // Drawing XML should not exist or not contain the group $drawingXml = $zip->getFromName('xl/drawings/drawing1.xml'); if ($drawingXml !== false) { // If drawing XML exists, it should NOT contain the group self::assertStringNotContainsString('', $drawingXml, 'Group should be lost without pass-through'); } // Media files should NOT be present (nothing was in the drawing collection) $image1Content = $zip->getFromName('xl/media/image1.png'); $image2Content = $zip->getFromName('xl/media/image2.bmp'); $image4Content = $zip->getFromName('xl/media/image4.gif'); $image6Content = $zip->getFromName('xl/media/image6.svg'); self::assertFalse($image1Content, 'image1.png should NOT be present without pass-through'); self::assertFalse($image2Content, 'image2.bmp should NOT be present without pass-through'); self::assertFalse($image4Content, 'image4.gif should NOT be present without pass-through'); self::assertFalse($image6Content, 'image6.svg should NOT be present without pass-through'); $zip->close(); unlink($tempFile); } }