From eb450c489ed72234c06a4f1075e7e11be25a7a8e Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Thu, 5 Dec 2024 20:10:42 -0800
Subject: [PATCH 1/7] Ods Writer Horizontal Alignment
Fix #4261. Ods does nothing with the `indent` property of Alignment. The fix is easy, but not as easy as it should be. Excel treats indent as an int in some unspecified unit. Ods, on the other hand, specifies it as a float with unit (inches, ems, etc.). Since MS does not reveal what an indent unit is, I resorted to some experimentation. It appears that 1 unit is equal to 0.1043 inches. That is not a guaranteed relationship - the conversion might be non-linear, or it might be affected by external factors like font size. I think multiplying by 0.1043 is adequate for now, and unquestionably better than what we're currently doing (ignoring the property). If it breaks down at some point, we'll look at it again. BTW, Shared\Drawing uses some conversion values of 9525, which, reciprocating and sliding the decimal point along yields a value of 0.10498..., which is close but not close enough for me to use.
The other property used in conjunction with indent is `text-align`, and here we have not been doing the right thing. If that property has the default value (`General`), it is treated as `start` (i.e. `left` on LTR docs and presumably `right` on RTL). This will align numbers as if they were text, which does no harm (they're still usable as numbers), but is not how LibreOffice handles them by default. Ods writer is changed to omit `text-align` when it is set to `General`, and let LibreOffice choose the appropriate alignment.
These changes are for Writer only. Ods Reader support for styles remains severely lacking.
---
src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 20 ++++--
.../Writer/Ods/IndentTest.php | 62 +++++++++++++++++++
tests/data/Writer/Ods/content-arrays.xml | 48 +++++++++++++-
tests/data/Writer/Ods/content-empty.xml | 1 -
.../Writer/Ods/content-hidden-worksheet.xml | 1 -
tests/data/Writer/Ods/content-with-data.xml | 11 ----
6 files changed, 124 insertions(+), 19 deletions(-)
create mode 100644 tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php
diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
index 7573230689..1cc68faba1 100644
--- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
+++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
@@ -20,6 +20,7 @@ class Style
public const COLUMN_STYLE_PREFIX = 'co';
public const ROW_STYLE_PREFIX = 'ro';
public const TABLE_STYLE_PREFIX = 'ta';
+ public const INDENT_TO_INCHES = 0.1043; // undocumented, used trial and error
private XMLWriter $writer;
@@ -28,12 +29,13 @@ public function __construct(XMLWriter $writer)
$this->writer = $writer;
}
- private function mapHorizontalAlignment(string $horizontalAlignment): string
+ private function mapHorizontalAlignment(?string $horizontalAlignment): string
{
return match ($horizontalAlignment) {
Alignment::HORIZONTAL_CENTER, Alignment::HORIZONTAL_CENTER_CONTINUOUS, Alignment::HORIZONTAL_DISTRIBUTED => 'center',
Alignment::HORIZONTAL_RIGHT => 'end',
Alignment::HORIZONTAL_FILL, Alignment::HORIZONTAL_JUSTIFY => 'justify',
+ Alignment::HORIZONTAL_GENERAL, '', null => '',
default => 'start',
};
}
@@ -145,8 +147,10 @@ private function writeCellProperties(CellStyle $style): void
{
// Align
$hAlign = $style->getAlignment()->getHorizontal();
+ $hAlign = $this->mapHorizontalAlignment($hAlign);
$vAlign = $style->getAlignment()->getVertical();
$wrap = $style->getAlignment()->getWrapText();
+ $indent = $style->getAlignment()->getIndent();
$this->writer->startElement('style:table-cell-properties');
if (!empty($vAlign) || $wrap) {
@@ -168,10 +172,16 @@ private function writeCellProperties(CellStyle $style): void
$this->writer->endElement();
- if (!empty($hAlign)) {
- $hAlign = $this->mapHorizontalAlignment($hAlign);
- $this->writer->startElement('style:paragraph-properties');
- $this->writer->writeAttribute('fo:text-align', $hAlign);
+ if ($hAlign !== '' || !empty($indent)) {
+ $this->writer
+ ->startElement('style:paragraph-properties');
+ if ($hAlign !== '') {
+ $this->writer->writeAttribute('fo:text-align', $hAlign);
+ }
+ if (!empty($indent)) {
+ $indentString = sprintf('%.4f', $indent * self::INDENT_TO_INCHES) . 'in';
+ $this->writer->writeAttribute('fo:margin-left', $indentString);
+ }
$this->writer->endElement();
}
}
diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php
new file mode 100644
index 0000000000..7b6a62baa2
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php
@@ -0,0 +1,62 @@
+compatibilityMode = Functions::getCompatibilityMode();
+ Functions::setCompatibilityMode(
+ Functions::COMPATIBILITY_OPENOFFICE
+ );
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ Functions::setCompatibilityMode($this->compatibilityMode);
+ }
+
+ public function testWriteSpreadsheet(): void
+ {
+ $spreadsheet = new Spreadsheet();
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->setCellValue('A1', 'aa');
+ $sheet->setCellValue('B1', 'bb');
+ $sheet->setCellValue('A2', 'cc');
+ $sheet->setCellValue('B2', 'dd');
+ $sheet->getStyle('A1')->getAlignment()->setIndent(2);
+ $writer = new Ods($spreadsheet);
+ $content = new Content($writer);
+ $xml = $content->write();
+ self::assertStringContainsString(
+ ''
+ . ''
+ . ''
+ . '',
+ $xml
+ );
+ self::assertStringContainsString(
+ ''
+ . ''
+ . '' // fo:margin-left is what we're looking for
+ . ''
+ . '',
+ $xml
+ );
+ self::assertSame(3, substr_count($xml, 'table:style-name="ce0"'));
+ self::assertSame(1, substr_count($xml, 'table:style-name="ce1"'));
+ $spreadsheet->disconnectWorksheets();
+ }
+}
diff --git a/tests/data/Writer/Ods/content-arrays.xml b/tests/data/Writer/Ods/content-arrays.xml
index a33b7dbfca..a363d199d9 100644
--- a/tests/data/Writer/Ods/content-arrays.xml
+++ b/tests/data/Writer/Ods/content-arrays.xml
@@ -1,2 +1,48 @@
-11133
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1
+
+
+1
+
+
+
+
+
+1
+
+
+3
+
+
+
+
+
+3
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml
index 84f4c23977..ee82953d1a 100644
--- a/tests/data/Writer/Ods/content-empty.xml
+++ b/tests/data/Writer/Ods/content-empty.xml
@@ -8,7 +8,6 @@
-
diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml
index 88a53257a1..89985861c1 100644
--- a/tests/data/Writer/Ods/content-hidden-worksheet.xml
+++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml
@@ -11,7 +11,6 @@
-
diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml
index db7d75a747..5b2f0677e0 100644
--- a/tests/data/Writer/Ods/content-with-data.xml
+++ b/tests/data/Writer/Ods/content-with-data.xml
@@ -11,57 +11,46 @@
-
-
-
-
-
-
-
-
-
-
-
From 8654e805f4e1aebf0aff6feb2df68472dc9bba62 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Thu, 5 Dec 2024 20:14:12 -0800
Subject: [PATCH 2/7] Ods Writer Eliminate Padding at End of Row
Ods Writer currently will write something like `` at the end of each row. This is not necessary. In eliminating that, I have made the code a bit more efficient and (hopefully) more readable.
---
src/PhpSpreadsheet/Writer/Ods/Content.php | 59 +++++++------------
.../Ods/RepeatEmptyCellsAndRowsTest.php | 48 +++++++++++++++
tests/data/Writer/Ods/content-arrays.xml | 45 +++++++++++++-
tests/data/Writer/Ods/content-empty.xml | 4 --
.../Writer/Ods/content-hidden-worksheet.xml | 2 -
tests/data/Writer/Ods/content-with-data.xml | 6 --
6 files changed, 113 insertions(+), 51 deletions(-)
create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php
diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php
index 91ac5d1d21..3b9cc82458 100644
--- a/src/PhpSpreadsheet/Writer/Ods/Content.php
+++ b/src/PhpSpreadsheet/Writer/Ods/Content.php
@@ -20,9 +20,6 @@
*/
class Content extends WriterPart
{
- const NUMBER_COLS_REPEATED_MAX = 1024;
- const NUMBER_ROWS_REPEATED_MAX = 1048576;
-
private Formula $formulaConvertor;
/**
@@ -142,7 +139,6 @@ private function writeSheets(XMLWriter $objWriter): void
sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric())
);
$objWriter->writeAttribute('table:default-cell-style-name', 'ce0');
-// $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
$objWriter->endElement();
}
$this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex);
@@ -155,34 +151,33 @@ private function writeSheets(XMLWriter $objWriter): void
*/
private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void
{
- $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
- $span_row = 0;
+ $spanRow = 0;
$rows = $sheet->getRowIterator();
foreach ($rows as $row) {
- $cellIterator = $row->getCellIterator();
- --$numberRowsRepeated;
- if ($cellIterator->valid()) {
- $objWriter->startElement('table:table-row');
- if ($span_row) {
- if ($span_row > 1) {
- $objWriter->writeAttribute('table:number-rows-repeated', (string) $span_row);
- }
- $objWriter->startElement('table:table-cell');
- $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX);
+ $cellIterator = $row->getCellIterator(iterateOnlyExistingCells: true);
+ $cellIterator->rewind();
+ $rowStyleExists = $sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0;
+ if ($cellIterator->valid() || $rowStyleExists) {
+ if ($spanRow) {
+ $objWriter->startElement('table:table-row');
+ $objWriter->writeAttribute(
+ 'table:number-rows-repeated',
+ (string) $spanRow
+ );
$objWriter->endElement();
- $span_row = 0;
- } else {
- if ($sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) {
- $objWriter->writeAttribute(
- 'table:style-name',
- sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
- );
- }
- $this->writeCells($objWriter, $cellIterator);
+ $spanRow = 0;
+ }
+ $objWriter->startElement('table:table-row');
+ if ($rowStyleExists) {
+ $objWriter->writeAttribute(
+ 'table:style-name',
+ sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
+ );
}
+ $this->writeCells($objWriter, $cellIterator);
$objWriter->endElement();
} else {
- ++$span_row;
+ ++$spanRow;
}
}
}
@@ -192,7 +187,6 @@ private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetInd
*/
private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void
{
- $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
$prevColumn = -1;
foreach ($cells as $cell) {
/** @var Cell $cell */
@@ -293,17 +287,6 @@ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void
$objWriter->endElement();
$prevColumn = $column;
}
-
- $numberColsRepeated = $numberColsRepeated - $prevColumn - 1;
- if ($numberColsRepeated > 0) {
- if ($numberColsRepeated > 1) {
- $objWriter->startElement('table:table-cell');
- $objWriter->writeAttribute('table:number-columns-repeated', (string) $numberColsRepeated);
- $objWriter->endElement();
- } else {
- $objWriter->writeElement('table:table-cell');
- }
- }
}
/**
diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php
new file mode 100644
index 0000000000..b64f8fd1dc
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php
@@ -0,0 +1,48 @@
+getActiveSheet();
+ $oldSheet->setCellValue('C1', 'xx');
+ $oldSheet->setCellValue('G1', 'aa');
+ $oldSheet->setCellValue('BB1', 'bb');
+ $oldSheet->setCellValue('A6', 'aaa');
+ $oldSheet->setCellValue('B7', 'bbb');
+ $oldSheet->getRowDimension(10)->setRowHeight(12);
+ $oldSheet->setCellValue('A12', 'this is A12');
+ $style = $oldSheet->getStyle('B14:D14');
+ $style->getFont()->setBold(true);
+ $oldSheet->getCell('E15')->setValue('X');
+ $oldSheet->mergeCells('E15:G16');
+ $oldSheet->getCell('J15')->setValue('j15');
+ $oldSheet->getCell('J16')->setValue('j16');
+ $oldSheet->getCell('A19')->setValue('lastrow');
+ $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods');
+ $spreadsheetOld->disconnectWorksheets();
+
+ $sheet = $spreadsheet->getActiveSheet();
+ self::assertSame('xx', $sheet->getCell('C1')->getValue());
+ self::assertSame('aa', $sheet->getCell('G1')->getValue());
+ self::assertSame('bb', $sheet->getCell('BB1')->getValue());
+ self::assertSame('aaa', $sheet->getCell('A6')->getValue());
+ self::assertSame('bbb', $sheet->getCell('B7')->getValue());
+ self::assertSame('this is A12', $sheet->getCell('A12')->getValue());
+ // Read styles, including row height, not yet implemented for ODS
+ self::assertSame('j15', $sheet->getCell('J15')->getValue());
+ self::assertSame('j16', $sheet->getCell('J16')->getValue());
+ self::assertSame(['E15:G16' => 'E15:G16'], $sheet->getMergeCells());
+ self::assertSame('lastrow', $sheet->getCell('A19')->getValue());
+
+ $spreadsheet->disconnectWorksheets();
+ }
+}
diff --git a/tests/data/Writer/Ods/content-arrays.xml b/tests/data/Writer/Ods/content-arrays.xml
index a33b7dbfca..939dbea8ca 100644
--- a/tests/data/Writer/Ods/content-arrays.xml
+++ b/tests/data/Writer/Ods/content-arrays.xml
@@ -1,2 +1,45 @@
-11133
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1
+
+
+1
+
+
+
+
+1
+
+
+3
+
+
+
+
+3
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml
index 84f4c23977..f8efeb542a 100644
--- a/tests/data/Writer/Ods/content-empty.xml
+++ b/tests/data/Writer/Ods/content-empty.xml
@@ -17,10 +17,6 @@
-
-
-
-
diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml
index 88a53257a1..8f34a19632 100644
--- a/tests/data/Writer/Ods/content-hidden-worksheet.xml
+++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml
@@ -24,7 +24,6 @@
1
-
@@ -33,7 +32,6 @@
2
-
diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml
index db7d75a747..911566ae04 100644
--- a/tests/data/Writer/Ods/content-with-data.xml
+++ b/tests/data/Writer/Ods/content-with-data.xml
@@ -92,7 +92,6 @@
Lorem ipsum
-
@@ -107,10 +106,6 @@
42798.572060185
-
-
-
-
@@ -119,7 +114,6 @@
2
-
From e2ec705ee8c5bd7d7daeee3d66b8425266287bfc Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Sat, 7 Dec 2024 00:26:40 -0800
Subject: [PATCH 3/7] Ods Writer Master Page Name
PR #2850 and PR #2851 added support for Worksheet Visibility to ODS. They work well when LibreOffice is used to open the spreadsheet. However, when Excel tries to open it, it reports corruption. Strictly speaking, this is not a PhpSpreadsheet problem, but, if we can fix it, we should.
It took a while to figure out what's bothering Excel. I'm not sure that all of what follows is necessary, but it works. It appears that it wants the content.xml `style:automatic-styles` definition for the `table` (i.e. worksheet) to include a `style:master-page-name` attribute. That attribute requires a corresponding definition in the `office:master-styles` section in styles.xml, and that attribute likewise requires a definition in `office:automatic-styles`.
The new entries in styles.xml can be used to specify things like header and footer. However, the ways that these are specified is distinctly different from what Excel (and therefore PhpSpreadsheet) does. Implementing that will be a good future project. However, for now, they will remain unsupported for Ods.
---
src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 1 +
src/PhpSpreadsheet/Writer/Ods/Styles.php | 13 ++++-
tests/data/Writer/Ods/content-arrays.xml | 49 ++++++++++++++++++-
tests/data/Writer/Ods/content-empty.xml | 2 +-
.../Writer/Ods/content-hidden-worksheet.xml | 4 +-
tests/data/Writer/Ods/content-with-data.xml | 4 +-
6 files changed, 65 insertions(+), 8 deletions(-)
diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
index 7573230689..c1c9f32c11 100644
--- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
+++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
@@ -289,6 +289,7 @@ public function writeTableStyle(Worksheet $worksheet, int $sheetId): void
'style:name',
sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId)
);
+ $this->writer->writeAttribute('style:master-page-name', 'Default');
$this->writer->startElement('style:table-properties');
diff --git a/src/PhpSpreadsheet/Writer/Ods/Styles.php b/src/PhpSpreadsheet/Writer/Ods/Styles.php
index 448b1eff13..6bfdfb1b33 100644
--- a/src/PhpSpreadsheet/Writer/Ods/Styles.php
+++ b/src/PhpSpreadsheet/Writer/Ods/Styles.php
@@ -56,8 +56,17 @@ public function write(): string
$objWriter->writeElement('office:font-face-decls');
$objWriter->writeElement('office:styles');
- $objWriter->writeElement('office:automatic-styles');
- $objWriter->writeElement('office:master-styles');
+ $objWriter->startElement('office:automatic-styles');
+ $objWriter->startElement('style:page-layout');
+ $objWriter->writeAttribute('style:name', 'Mpm1');
+ $objWriter->endElement(); // style:page-layout
+ $objWriter->endElement(); // office:automatic-styles
+ $objWriter->startElement('office:master-styles');
+ $objWriter->startElement('style:master-page');
+ $objWriter->writeAttribute('style:name', 'Default');
+ $objWriter->writeAttribute('style:page-layout-name', 'Mpm1');
+ $objWriter->endElement(); //style:master-page
+ $objWriter->endElement(); //office:master-styles
$objWriter->endElement();
return $objWriter->getData();
diff --git a/tests/data/Writer/Ods/content-arrays.xml b/tests/data/Writer/Ods/content-arrays.xml
index a33b7dbfca..231036ee29 100644
--- a/tests/data/Writer/Ods/content-arrays.xml
+++ b/tests/data/Writer/Ods/content-arrays.xml
@@ -1,2 +1,49 @@
-11133
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1
+
+
+1
+
+
+
+
+
+1
+
+
+3
+
+
+
+
+
+3
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml
index 84f4c23977..cb14ce7e53 100644
--- a/tests/data/Writer/Ods/content-empty.xml
+++ b/tests/data/Writer/Ods/content-empty.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml
index 88a53257a1..1d89560505 100644
--- a/tests/data/Writer/Ods/content-hidden-worksheet.xml
+++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml
@@ -3,10 +3,10 @@
-
+
-
+
diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml
index db7d75a747..82209609f8 100644
--- a/tests/data/Writer/Ods/content-with-data.xml
+++ b/tests/data/Writer/Ods/content-with-data.xml
@@ -3,10 +3,10 @@
-
+
-
+
From 08c5ff071b73785c80a6780dd4696a7240d340c2 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Wed, 11 Dec 2024 04:37:05 -0800
Subject: [PATCH 4/7] Add forceFullCalc Option to Xlsx Writer
Fix #4269. In response to issue #456 PR #515, `forceFullCalc` was added to workbook.xml whenever `preCalculateFormulas` was set to false. It is not clear why this should have been needed; attempts to reproduce the error in the original 6.5-year-old issue are unable to reproduce it today. Nevertheless, it is, or was, there for a reason.
Today, the forceFullCalc option sets an option where formulas *might* not be recalculated when a cell used in the formula changes. I have not succeeded in finding a situation where it doesn't automatically recalculate, but it probably exists on complicated spreadsheets. To overcome this possibility, Excel offers a button which can be used to recalculate on demand. By itself, this might not be a terrible problem. However, it seems to come with the strange property that any spreadsheets opened at the same time as a forceFullCalc spreadsheet operate as if they too specified forceFullCalc. That *is* a problem, especially since users when closing a spreasheet affected in this way will be prompted to save it even when they haven't changed anything.
I am not willing to make a BC change at this time, although I might consider it in future (PR #4240). For now, I am adding a new property `forceFullCalc` with setter (no getter needed) to Xlsx Writer. That property can be `null` (default, in which case the Xml attribute is set as today), or `false` or `true` (in which case the Xml attribute will be set to the Writer attribute). I think that, when `preCalculateFormulas` is set to false, the calling application should give consideration to setting `forceFullCalc` to false as well. All other situations should just use the default.
---
docs/topics/reading-and-writing-to-file.md | 6 ++
src/PhpSpreadsheet/Writer/Xlsx.php | 20 +++++-
src/PhpSpreadsheet/Writer/Xlsx/Workbook.php | 13 ++--
.../Writer/Xlsx/Issue4269Test.php | 65 +++++++++++++++++++
4 files changed, 99 insertions(+), 5 deletions(-)
create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php
diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md
index 19fe8a8ed9..28bc3b173e 100644
--- a/docs/topics/reading-and-writing-to-file.md
+++ b/docs/topics/reading-and-writing-to-file.md
@@ -169,6 +169,12 @@ $writer->save("05featuredemo.xlsx");
**Note** Formulas will still be calculated in any column set to be autosized
even if pre-calculated is set to false
+**Note** Prior to release 3.7.0, the use of this feature will cause Excel to be used in a mode where opening a sheet saved in this manner *might* not automatically recalculate a cell's formula when a cell used it the formula changes. Furthermore, that behavior might be applied to all spreadsheets open at the time. To avoid this behavior, add the following statement after `setPreCalculateFormulas` above:
+```php
+$writer->setForceFullCalc(false);
+```
+In a future release, the property's default may change to `false` and that statement may no longer be required.
+
#### Office 2003 compatibility pack
Because of a bug in the Office2003 compatibility pack, there can be some
diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php
index a9d06db581..b74d810502 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx.php
@@ -140,6 +140,8 @@ class Xlsx extends BaseWriter
private bool $useDynamicArray = false;
+ private ?bool $forceFullCalc = null;
+
/**
* Create a new Xlsx Writer.
*/
@@ -342,7 +344,7 @@ public function save($filename, int $flags = 0): void
$zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet);
// Add workbook to ZIP file
- $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas);
+ $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas, $this->forceFullCalc);
$chartCount = 0;
// Add worksheets
@@ -747,4 +749,20 @@ private function determineUseDynamicArrays(): void
{
$this->useDynamicArray = $this->preCalculateFormulas && Calculation::getInstance($this->spreadSheet)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY && !$this->useCSEArrays;
}
+
+ /**
+ * If this is set when a spreadsheet is opened,
+ * values may not be automatically re-calculated,
+ * and a button will be available to force re-calculation.
+ * This may apply to all spreadsheets open at that time.
+ * If null, this will be set to the opposite of $preCalculateFormulas.
+ * It is likely that false is the desired setting, although
+ * cases have been reported where true is required (issue #456).
+ */
+ public function setForceFullCalc(?bool $forceFullCalc): self
+ {
+ $this->forceFullCalc = $forceFullCalc;
+
+ return $this;
+ }
}
diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
index 24a2eeb064..c907e100b7 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
@@ -15,10 +15,11 @@ class Workbook extends WriterPart
* Write workbook to XML format.
*
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
+ * @param ?bool $forceFullCalc If null, !$preCalculateFormulas
*
* @return string XML Output
*/
- public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false): string
+ public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false, ?bool $forceFullCalc = null): string
{
// Create XML writer
if ($this->getParentWriter()->getUseDiskCaching()) {
@@ -57,7 +58,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormul
(new DefinedNamesWriter($objWriter, $spreadsheet))->write();
// calcPr
- $this->writeCalcPr($objWriter, $preCalculateFormulas);
+ $this->writeCalcPr($objWriter, $preCalculateFormulas, $forceFullCalc);
$objWriter->endElement();
@@ -148,7 +149,7 @@ private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spre
*
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
*/
- private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = true): void
+ private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas, ?bool $forceFullCalc): void
{
$objWriter->startElement('calcPr');
@@ -160,7 +161,11 @@ private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas =
// fullCalcOnLoad isn't needed if we will calculate before writing
$objWriter->writeAttribute('calcCompleted', ($preCalculateFormulas) ? '1' : '0');
$objWriter->writeAttribute('fullCalcOnLoad', ($preCalculateFormulas) ? '0' : '1');
- $objWriter->writeAttribute('forceFullCalc', ($preCalculateFormulas) ? '0' : '1');
+ if ($forceFullCalc === null) {
+ $objWriter->writeAttribute('forceFullCalc', $preCalculateFormulas ? '0' : '1');
+ } else {
+ $objWriter->writeAttribute('forceFullCalc', $forceFullCalc ? '1' : '0');
+ }
$objWriter->endElement();
}
diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php
new file mode 100644
index 0000000000..ffe9e30ae0
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php
@@ -0,0 +1,65 @@
+outputFile !== '') {
+ unlink($this->outputFile);
+ $this->outputFile = '';
+ }
+ }
+
+ #[DataProvider('validationProvider')]
+ public function testWriteArrayFormulaTextJoin(
+ bool $preCalculateFormulas,
+ ?bool $forceFullCalc,
+ string $expected
+ ): void {
+ $spreadsheet = new Spreadsheet();
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->setCellValue('A1', '=A2*2');
+ $sheet->setCellValue('A2', 0);
+
+ $writer = new XlsxWriter($spreadsheet);
+ $writer->setPreCalculateFormulas($preCalculateFormulas);
+ if ($forceFullCalc !== null) {
+ $writer->setForceFullCalc($forceFullCalc);
+ }
+ $this->outputFile = File::temporaryFilename();
+ $writer->save($this->outputFile);
+ $spreadsheet->disconnectWorksheets();
+
+ $file = 'zip://';
+ $file .= $this->outputFile;
+ $file .= '#xl/workbook.xml';
+ $data = file_get_contents($file);
+ if ($data === false) {
+ self::fail('Unable to read file');
+ } else {
+ self::assertStringContainsString($expected, $data);
+ }
+ }
+
+ public static function validationProvider(): array
+ {
+ return [
+ 'normal case' => [true, null, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="0"'],
+ 'issue 456' => [false, null, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="1"'],
+ 'better choice for no precalc' => [false, false, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="0"'],
+ 'unlikely use case' => [true, true, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="1"'],
+ ];
+ }
+}
From d76310d57df727aa723929da0bc51455c5a29ed9 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Sat, 14 Dec 2024 12:18:09 -0800
Subject: [PATCH 5/7] Ignore Coverage for SimpleCache1, ZipStream2
PhpSpreadsheet will use either SimpleCache1/3 (but not both). Likewise for ZipStream2/3. However, only SimpleCache3/Zipstream3 are used while processing code coverage. Indicate this with appropriate annotations.
No executable code is changed.
---
src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php | 5 +++++
src/PhpSpreadsheet/Writer/ZipStream2.php | 6 ++++++
2 files changed, 11 insertions(+)
diff --git a/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php
index b8918c9153..58463ebea0 100644
--- a/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php
+++ b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php
@@ -9,6 +9,11 @@
*
* Alternative implementation should leverage off-memory, non-volatile storage
* to reduce overall memory usage.
+ *
+ * Either SimpleCache1 or SimpleCache3, but not both, may be used.
+ * For code coverage testing, it will always be SimpleCache3.
+ *
+ * @codeCoverageIgnore
*/
class SimpleCache1 implements CacheInterface
{
diff --git a/src/PhpSpreadsheet/Writer/ZipStream2.php b/src/PhpSpreadsheet/Writer/ZipStream2.php
index ed21a7e399..e03e1d0cf9 100644
--- a/src/PhpSpreadsheet/Writer/ZipStream2.php
+++ b/src/PhpSpreadsheet/Writer/ZipStream2.php
@@ -5,6 +5,12 @@
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
+/**
+ * Either ZipStream2 or ZipStream3, but not both, may be used.
+ * For code coverage testing, it will always be ZipStream3.
+ *
+ * @codeCoverageIgnore
+ */
class ZipStream2
{
/**
From ed158f20ebffa5003c9a0783d47e5228f17bf642 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Sun, 15 Dec 2024 10:39:00 -0800
Subject: [PATCH 6/7] Update CHANGELOG.md
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f567e658c6..c06cdf5893 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed
- More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276)
+- Several fixed to ODS Writer. [Issue #4261](https://github.com/PHPOffice/PhpSpreadsheet/issues/4261) [PR #4263](https://github.com/PHPOffice/PhpSpreadsheet/pull/4263) [PR #4264](https://github.com/PHPOffice/PhpSpreadsheet/pull/4264) [PR #4266](https://github.com/PHPOffice/PhpSpreadsheet/pull/4266)
## 2024-12-08 - 3.6.0
From c744f570c1503e2f0f50e7caac4722e4e7b3ed1a Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Sun, 22 Dec 2024 17:48:30 -0800
Subject: [PATCH 7/7] Update CHANGELOG.md
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59875e9772..2cb5435cae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed
-- Nothing yet.
+- Add forceFullCalc option to Xlsx Writer. [Issue #4269](https://github.com/PHPOffice/PhpSpreadsheet/issues/4269) [PR #4271](https://github.com/PHPOffice/PhpSpreadsheet/pull/4271)
## 2024-12-08 - 3.6.0