mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-13 03:26:26 +00:00
38a8330748
A follow-up to PR #4837. PR #4827, to which I am not necessarily committed, shows that there could be a use case for letting a user perform customized logic to convert a non-UTF8-encoded CSV. In addition, several other CSV Reader properties require the use of static or locale properties, which is somewhat problematic; this PR allows them to be set as instance variables, falling back to static/locale only when unset.
843 lines
40 KiB
PHP
843 lines
40 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
|
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
|
|
use PhpOffice\PhpSpreadsheet\Shared\File;
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
|
|
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
|
|
use ZipArchive;
|
|
|
|
class DrawingPassThroughTest extends AbstractFunctional
|
|
{
|
|
private const DIRECTORY = 'tests/data/Writer/XLSX/';
|
|
private const TEMPLATE = self::DIRECTORY . 'issue.4037.xlsx';
|
|
|
|
/**
|
|
* Test that unsupported drawing elements (shapes, textboxes) are preserved
|
|
* when pass-through is enabled and no drawings are modified.
|
|
*/
|
|
public function testDrawingPassThroughPreservesUnsupportedElements(): void
|
|
{
|
|
// Load with pass-through enabled
|
|
$reader = new XlsxReader();
|
|
$reader->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('<xdr:sp', $originalDrawingXml, 'Original XML should contain shape element');
|
|
self::assertStringContainsString('<xdr:txBody>', $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('<xdr:sp>', $drawingXml, 'Shapes should be preserved in saved file');
|
|
self::assertStringContainsString('<xdr:txBody>', $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('<xdr:sp', $drawingXml, 'Original file should contain shape');
|
|
$spreadsheet->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('<xdr:sp>', $drawingXml, 'Shape should be lost without Reader pass-through');
|
|
self::assertStringNotContainsString('<xdr:txBody>', $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('<xdr:sp', $originalDrawingXml, 'Original XML should contain shape');
|
|
self::assertStringContainsString('<xdr:txBody>', $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('<xdr:sp>', $drawingXml, 'Shapes should be preserved after comment deletion');
|
|
self::assertStringContainsString('<xdr:txBody>', $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('/<xdr:col>(\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('/<xdr:col>(\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('/<xdr:row>(\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('/<xdr:row>(\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('<xdr:sp', $drawingXml, 'DrawingML should contain shape (preserved by pass-through)');
|
|
self::assertStringContainsString('<xdr:pic', $drawingXml, 'DrawingML should contain image');
|
|
|
|
// Check for comments XML (should contain both existing and new comments)
|
|
$commentsXml = $zip->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('<xdr:sp', $drawingXml, 'Shapes should be preserved');
|
|
self::assertStringContainsString('<xdr:txBody>', $drawingXml, 'Textboxes should be preserved');
|
|
|
|
// Check for images (xdr:pic elements)
|
|
self::assertStringContainsString('<xdr:pic', $drawingXml, 'Images should be preserved');
|
|
|
|
// Check for SVG reference
|
|
self::assertStringContainsString('svgBlip', $drawingXml, 'SVG reference should be preserved');
|
|
|
|
$zip->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('<xdr:grpSp>', $originalDrawingXml, 'Original file should contain a group');
|
|
self::assertStringContainsString('<xdr:pic>', $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('<xdr:grpSp>', $drawingXml, 'Group should be preserved');
|
|
self::assertStringContainsString('<xdr:pic>', $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('<xdr:grpSp>', $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('<xdr:grpSp>', $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);
|
|
}
|
|
}
|