From d46b7b3e9de87adc212699e10a96d042ce9c9f50 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Thu, 29 Feb 2024 11:11:00 -0800
Subject: [PATCH] Default Style Alignment
Fix #3918, sort of. Xlsx cells use the default style when they omit the `s` tag, or when `s="0"` is specified. LibreOffice does not honor Alignment in the default Style unless the cell explicitly uses the second form, even though it honors other default Styles (e.g. bold font) when `s` is omitted. Gnumeric seems to have the same problem. A bug report has been filed with LibreOffice.
In the meantime, this PR adds to Xlsx Writer an optional boolean property `explicitStyle0` with setter and getter. Default is false, which will continue the current behavior by adding an `s` tag only when the cell uses a non-default style (this is how Excel itself behaves). When set to true, Xlsx Writer will explicity write `s="0"` for all cells using default style. This will allow users to create an Xlsx spreadsheet with default alignment of cells that will show up correctly when the spreadsheet is viewed with LibreOffice. Technically speaking, it is *probably* safe to always use `true`, except that the spreadsheet size will be a bit larger. However, my hope is that this is a temporary measure which can go away when the vendors have had a chance to fix their problems, hence the `false` default.
---
src/PhpSpreadsheet/Writer/Xlsx.php | 19 ++++
src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 9 +-
.../Writer/Xlsx/ExplicitStyle0Test.php | 106 ++++++++++++++++++
3 files changed, 133 insertions(+), 1 deletion(-)
create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/ExplicitStyle0Test.php
diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php
index 4a81ab861..26c185e1f 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx.php
@@ -134,6 +134,8 @@ class Xlsx extends BaseWriter
private Worksheet $writerPartWorksheet;
+ private bool $explicitStyle0 = false;
+
/**
* Create a new Xlsx Writer.
*/
@@ -699,4 +701,21 @@ class Xlsx extends BaseWriter
return $data;
}
+
+ public function getExplicitStyle0(): bool
+ {
+ return $this->explicitStyle0;
+ }
+
+ /**
+ * This may be useful if non-default Alignment is part of default style
+ * and you think you might want to open the spreadsheet
+ * with LibreOffice or Gnumeric.
+ */
+ public function setExplicitStyle0(bool $explicitStyle0): self
+ {
+ $this->explicitStyle0 = $explicitStyle0;
+
+ return $this;
+ }
}
diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
index 49d1ef67f..a14ff2812 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
@@ -28,6 +28,8 @@ class Worksheet extends WriterPart
private string $evalError = '';
+ private bool $explicitStyle0;
+
/**
* Write worksheet to XML format.
*
@@ -38,6 +40,7 @@ class Worksheet extends WriterPart
*/
public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, array $stringTable = [], bool $includeCharts = false): string
{
+ $this->explicitStyle0 = $this->getParentWriter()->getExplicitStyle0();
$this->numberStoredAsText = '';
$this->formula = '';
$this->twoDigitTextYear = '';
@@ -1441,7 +1444,11 @@ class Worksheet extends WriterPart
$objWriter->writeAttribute('r', $cellAddress);
// Sheet styles
- self::writeAttributeIf($objWriter, (bool) $xfi, 's', "$xfi");
+ if ($xfi) {
+ $objWriter->writeAttribute('s', "$xfi");
+ } elseif ($this->explicitStyle0) {
+ $objWriter->writeAttribute('s', '0');
+ }
// If cell value is supplied, write cell value
if ($writeValue) {
diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/ExplicitStyle0Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/ExplicitStyle0Test.php
new file mode 100644
index 000000000..57312dfba
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/ExplicitStyle0Test.php
@@ -0,0 +1,106 @@
+outputFile !== '') {
+ unlink($this->outputFile);
+ $this->outputFile = '';
+ }
+ }
+
+ public function testWithoutExplicitStyle0(): void
+ {
+ $spreadsheet = new Spreadsheet();
+ $defaultStyle = $spreadsheet->getDefaultStyle();
+ $defaultStyle->getFont()->setBold(true);
+ $defaultStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->getCell('A1')->setValue('bold');
+ $sheet->getCell('A2')->setValue('italic');
+ $sheet->getStyle('A2')->getFont()->setItalic(true);
+ $writer = new XlsxWriter($spreadsheet);
+ $this->outputFile = File::temporaryFilename();
+ $writer->save($this->outputFile);
+ $spreadsheet->disconnectWorksheets();
+
+ $reader = new XlsxReader();
+ $spreadsheet2 = $reader->load($this->outputFile);
+ $sheet2 = $spreadsheet2->getActiveSheet();
+ $styleA1 = $sheet2->getStyle('A1');
+ self::assertTrue($styleA1->getFont()->getBold());
+ self::assertFalse($styleA1->getFont()->getItalic());
+ self::assertSame(Alignment::HORIZONTAL_CENTER, $styleA1->getAlignment()->getHorizontal());
+ $styleA2 = $sheet2->getStyle('A2');
+ self::assertTrue($styleA1->getFont()->getBold());
+ self::assertTrue($styleA1->getFont()->getItalic());
+ self::assertSame(Alignment::HORIZONTAL_CENTER, $styleA1->getAlignment()->getHorizontal());
+ $spreadsheet2->disconnectWorksheets();
+
+ $file = 'zip://';
+ $file .= $this->outputFile;
+ $file .= '#xl/worksheets/sheet1.xml';
+ $data = file_get_contents($file);
+ if ($data === false) {
+ self::fail('Unable to read file');
+ } else {
+ self::assertStringContainsString('0', $data, 'no s attribute in c tag');
+ self::assertStringContainsString('1', $data);
+ }
+ }
+
+ public function testWithExplicitStyle0(): void
+ {
+ $spreadsheet = new Spreadsheet();
+ $defaultStyle = $spreadsheet->getDefaultStyle();
+ $defaultStyle->getFont()->setBold(true);
+ $defaultStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->getCell('A1')->setValue('bold');
+ $sheet->getCell('A2')->setValue('italic');
+ $sheet->getStyle('A2')->getFont()->setItalic(true);
+ $writer = new XlsxWriter($spreadsheet);
+ $writer->setExplicitStyle0(true);
+ $this->outputFile = File::temporaryFilename();
+ $writer->save($this->outputFile);
+ $spreadsheet->disconnectWorksheets();
+
+ $reader = new XlsxReader();
+ $spreadsheet2 = $reader->load($this->outputFile);
+ $sheet2 = $spreadsheet2->getActiveSheet();
+ $styleA1 = $sheet2->getStyle('A1');
+ self::assertTrue($styleA1->getFont()->getBold());
+ self::assertFalse($styleA1->getFont()->getItalic());
+ self::assertSame(Alignment::HORIZONTAL_CENTER, $styleA1->getAlignment()->getHorizontal());
+ $styleA2 = $sheet2->getStyle('A2');
+ self::assertTrue($styleA1->getFont()->getBold());
+ self::assertTrue($styleA1->getFont()->getItalic());
+ self::assertSame(Alignment::HORIZONTAL_CENTER, $styleA1->getAlignment()->getHorizontal());
+ $spreadsheet2->disconnectWorksheets();
+
+ $file = 'zip://';
+ $file .= $this->outputFile;
+ $file .= '#xl/worksheets/sheet1.xml';
+ $data = file_get_contents($file);
+ if ($data === false) {
+ self::fail('Unable to read file');
+ } else {
+ self::assertStringContainsString('0', $data, 'has s attribute in c tag');
+ self::assertStringContainsString('1', $data);
+ }
+ }
+}