From 69b65d4e2adea72356e04d0719e8d1f765e9e6ff Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 26 Jul 2025 00:08:33 -0700 Subject: [PATCH 1/2] Xlsx Writer Eliminate xml:space From Non-Text Nodes Fix #4542. PhpSpreadsheet has been writing attribute `xml:space="preserve"` to the `table` tag when writing a Table. According to the issue, Excel 2016 is treating the resulting file as corrupt. I do not have access to a version of Excel 2016 to confirm. This seems to be a bug with that release. Nevertheless, the OOXML spec, with over 100 references to `xml:space` does not indicate that it is a permitted attribute for `table`. It should only be specified for text nodes. This PR eliminates the undocumented, and unneeded, usage. Investigating further, PhpSpreadsheet also writes this attribute for `workbook`, `styleSheet`, and `worksheet` tags. It is again undocumented and unneeded in those cases. Although all Excel releases, including 2016, apparently tolerate such usage, this PR also eliminates those. Finally, there is one case where PhpSpreadsheet omits this tag when it is needed. When writing a cell whose data type is an inline string, and the string contains leading or trailing whitespace, the text tag needs to specify `xml:space`, and is now changed to do so. --- src/PhpSpreadsheet/Writer/Xlsx.php | 19 ++- src/PhpSpreadsheet/Writer/Xlsx/Style.php | 1 - src/PhpSpreadsheet/Writer/Xlsx/Table.php | 1 - src/PhpSpreadsheet/Writer/Xlsx/Workbook.php | 1 - src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 16 +-- .../Writer/Xlsx/Issue4542Test.php | 109 ++++++++++++++++++ 6 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4542Test.php diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index dcbbde72e5..bca6243185 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -5,6 +5,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\HashTable; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Conditional; @@ -283,6 +284,19 @@ public function createStyleDictionaries(): void ); } + /** + * @return (RichText|string)[] $stringTable + */ + public function createStringTable(): array + { + $this->stringTable = []; + for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { + $this->stringTable = $this->getWriterPartStringTable()->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable); + } + + return $this->stringTable; + } + /** * Save PhpSpreadsheet to file. * @@ -303,10 +317,7 @@ public function save($filename, int $flags = 0): void Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); // Create string lookup table - $this->stringTable = []; - for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { - $this->stringTable = $this->getWriterPartStringTable()->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable); - } + $this->createStringTable(); // Create styles dictionaries $this->createStyleDictionaries(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/src/PhpSpreadsheet/Writer/Xlsx/Style.php index 201071ce7f..2ffbacfe88 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Style.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -37,7 +37,6 @@ public function writeStyles(Spreadsheet $spreadsheet): string // styleSheet $objWriter->startElement('styleSheet'); - $objWriter->writeAttribute('xml:space', 'preserve'); $objWriter->writeAttribute('xmlns', Namespaces::MAIN); // numFmts diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Table.php b/src/PhpSpreadsheet/Writer/Xlsx/Table.php index f88b2a0423..4a1364eb1d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Table.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Table.php @@ -34,7 +34,6 @@ public function writeTable(WorksheetTable $table, int $tableRef): string $range = $table->getRange(); $objWriter->startElement('table'); - $objWriter->writeAttribute('xml:space', 'preserve'); $objWriter->writeAttribute('xmlns', Namespaces::MAIN); $objWriter->writeAttribute('id', (string) $tableRef); $objWriter->writeAttribute('name', $name); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index c907e100b7..1f5dcce7d6 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -33,7 +33,6 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormul // workbook $objWriter->startElement('workbook'); - $objWriter->writeAttribute('xml:space', 'preserve'); $objWriter->writeAttribute('xmlns', Namespaces::MAIN); $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 9bb4f68f0e..9ffa49f526 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -68,7 +68,6 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, array $string // Worksheet $objWriter->startElement('worksheet'); - $objWriter->writeAttribute('xml:space', 'preserve'); $objWriter->writeAttribute('xmlns', Namespaces::MAIN); $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT); @@ -1436,13 +1435,16 @@ private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, Ri $objWriter->writeAttribute('t', $mappedType); if (!$cellValue instanceof RichText) { $objWriter->startElement('is'); - $objWriter->writeElement( - 't', - StringHelper::controlCharacterPHP2OOXML( - $cellValue - ) + $objWriter->startElement('t'); + $textToWrite = StringHelper::controlCharacterPHP2OOXML( + $cellValue ); - $objWriter->endElement(); + if ($textToWrite !== trim($textToWrite)) { + $objWriter->writeAttribute('xml:space', 'preserve'); + } + $objWriter->writeRawData($textToWrite); + $objWriter->endElement(); // t + $objWriter->endElement(); // is } else { $objWriter->startElement('is'); $this->getParentWriter() diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4542Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4542Test.php new file mode 100644 index 0000000000..7e942a33e2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4542Test.php @@ -0,0 +1,109 @@ +getActiveSheet(); + $string = ' Ye&ar '; + $trimString = trim($string); + $sheet->getCell('A1')->setValue($string); + $sheet->getCell('A2')->setValueExplicit($string, DataType::TYPE_INLINE); + $sheet->getCell('B1')->setValue($trimString); + $sheet->getCell('B2')->setValueExplicit($trimString, DataType::TYPE_INLINE); + $writer = new XlsxWriter($spreadsheet); + + $writer->createStyleDictionaries(); + $writerStyle = new XlsxWriter\Style($writer); + $data = $writerStyle->writeStyles($spreadsheet); + self::assertStringContainsString( + 'writeWorkbook($spreadsheet); + self::assertStringContainsString( + 'createStringTable(); + $writerStringTable = new XlsxWriter\StringTable($writer); + $data = $writerStringTable->writeStringTable($stringTable); + self::assertStringContainsString( + ' Ye&ar ', + $data + ); + self::assertStringContainsString( + 'Ye&ar', + $data + ); + + $writerWorksheet = new XlsxWriter\Worksheet($writer); + $data = $writerWorksheet->writeWorksheet($sheet, []); + self::assertStringContainsString( + ' Ye&ar ', + $data + ); + self::assertStringContainsString( + 'Ye&ar', + $data + ); + + $spreadsheet->disconnectWorksheets(); + } + + public function testTable(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray( + [ + ['MyCol', 'Colonne2', 'Colonne3'], + [10, 20], + [2], + [3], + [4], + ], + null, + 'B1', + true + ); + $table = new Table('B1:D5', 'Tableau1'); + $sheet->addTable($table); + + $writer = new XlsxWriter($spreadsheet); + $writerTable = new XlsxWriter\Table($writer); + $data = $writerTable->writeTable($table, 1); + + self::assertStringContainsString( + 'disconnectWorksheets(); + } +} From c32605828b6bfeb390582902cd005afe9776edbb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 3 Aug 2025 08:44:08 -0700 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb767db4eb..f94778da96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing yet. +- Xlsx Writer eliminate xml:space from non-text nodes. [Issue #4542](https://github.com/PHPOffice/PhpSpreadsheet/issues/4542) [PR #4556](https://github.com/PHPOffice/PhpSpreadsheet/pull/4556) ## 2025-07-23 - 4.5.0