Skip to content

Commit c3c1aca

Browse files
authored
Merge branch 'master' into biffcover
2 parents 06a6549 + af07ad1 commit c3c1aca

File tree

16 files changed

+319
-82
lines changed

16 files changed

+319
-82
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2525

2626
### Fixed
2727

28-
- 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)
28+
- Add forceFullCalc option to Xlsx Writer. [Issue #4269](https://github.com/PHPOffice/PhpSpreadsheet/issues/4269) [PR #4271](https://github.com/PHPOffice/PhpSpreadsheet/pull/4271)
29+
- 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)
2930
- Coverage-related tweaks to Xls Reader. [PR #4277](https://github.com/PHPOffice/PhpSpreadsheet/pull/4277)
31+
- 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)
3032

3133
## 2024-12-08 - 3.6.0
3234

docs/topics/reading-and-writing-to-file.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ $writer->save("05featuredemo.xlsx");
169169
**Note** Formulas will still be calculated in any column set to be autosized
170170
even if pre-calculated is set to false
171171

172+
**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:
173+
```php
174+
$writer->setForceFullCalc(false);
175+
```
176+
In a future release, the property's default may change to `false` and that statement may no longer be required.
177+
172178
#### Office 2003 compatibility pack
173179

174180
Because of a bug in the Office2003 compatibility pack, there can be some

src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
*
1010
* Alternative implementation should leverage off-memory, non-volatile storage
1111
* to reduce overall memory usage.
12+
*
13+
* Either SimpleCache1 or SimpleCache3, but not both, may be used.
14+
* For code coverage testing, it will always be SimpleCache3.
15+
*
16+
* @codeCoverageIgnore
1217
*/
1318
class SimpleCache1 implements CacheInterface
1419
{

src/PhpSpreadsheet/Writer/Ods/Cell/Style.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Style
2020
public const COLUMN_STYLE_PREFIX = 'co';
2121
public const ROW_STYLE_PREFIX = 'ro';
2222
public const TABLE_STYLE_PREFIX = 'ta';
23+
public const INDENT_TO_INCHES = 0.1043; // undocumented, used trial and error
2324

2425
private XMLWriter $writer;
2526

@@ -28,12 +29,13 @@ public function __construct(XMLWriter $writer)
2829
$this->writer = $writer;
2930
}
3031

31-
private function mapHorizontalAlignment(string $horizontalAlignment): string
32+
private function mapHorizontalAlignment(?string $horizontalAlignment): string
3233
{
3334
return match ($horizontalAlignment) {
3435
Alignment::HORIZONTAL_CENTER, Alignment::HORIZONTAL_CENTER_CONTINUOUS, Alignment::HORIZONTAL_DISTRIBUTED => 'center',
3536
Alignment::HORIZONTAL_RIGHT => 'end',
3637
Alignment::HORIZONTAL_FILL, Alignment::HORIZONTAL_JUSTIFY => 'justify',
38+
Alignment::HORIZONTAL_GENERAL, '', null => '',
3739
default => 'start',
3840
};
3941
}
@@ -145,8 +147,10 @@ private function writeCellProperties(CellStyle $style): void
145147
{
146148
// Align
147149
$hAlign = $style->getAlignment()->getHorizontal();
150+
$hAlign = $this->mapHorizontalAlignment($hAlign);
148151
$vAlign = $style->getAlignment()->getVertical();
149152
$wrap = $style->getAlignment()->getWrapText();
153+
$indent = $style->getAlignment()->getIndent();
150154

151155
$this->writer->startElement('style:table-cell-properties');
152156
if (!empty($vAlign) || $wrap) {
@@ -168,10 +172,16 @@ private function writeCellProperties(CellStyle $style): void
168172

169173
$this->writer->endElement();
170174

171-
if (!empty($hAlign)) {
172-
$hAlign = $this->mapHorizontalAlignment($hAlign);
173-
$this->writer->startElement('style:paragraph-properties');
174-
$this->writer->writeAttribute('fo:text-align', $hAlign);
175+
if ($hAlign !== '' || !empty($indent)) {
176+
$this->writer
177+
->startElement('style:paragraph-properties');
178+
if ($hAlign !== '') {
179+
$this->writer->writeAttribute('fo:text-align', $hAlign);
180+
}
181+
if (!empty($indent)) {
182+
$indentString = sprintf('%.4f', $indent * self::INDENT_TO_INCHES) . 'in';
183+
$this->writer->writeAttribute('fo:margin-left', $indentString);
184+
}
175185
$this->writer->endElement();
176186
}
177187
}
@@ -289,6 +299,7 @@ public function writeTableStyle(Worksheet $worksheet, int $sheetId): void
289299
'style:name',
290300
sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId)
291301
);
302+
$this->writer->writeAttribute('style:master-page-name', 'Default');
292303

293304
$this->writer->startElement('style:table-properties');
294305

src/PhpSpreadsheet/Writer/Ods/Content.php

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@
2020
*/
2121
class Content extends WriterPart
2222
{
23-
const NUMBER_COLS_REPEATED_MAX = 1024;
24-
const NUMBER_ROWS_REPEATED_MAX = 1048576;
25-
2623
private Formula $formulaConvertor;
2724

2825
/**
@@ -142,7 +139,6 @@ private function writeSheets(XMLWriter $objWriter): void
142139
sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric())
143140
);
144141
$objWriter->writeAttribute('table:default-cell-style-name', 'ce0');
145-
// $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
146142
$objWriter->endElement();
147143
}
148144
$this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex);
@@ -155,34 +151,33 @@ private function writeSheets(XMLWriter $objWriter): void
155151
*/
156152
private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void
157153
{
158-
$numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
159-
$span_row = 0;
154+
$spanRow = 0;
160155
$rows = $sheet->getRowIterator();
161156
foreach ($rows as $row) {
162-
$cellIterator = $row->getCellIterator();
163-
--$numberRowsRepeated;
164-
if ($cellIterator->valid()) {
165-
$objWriter->startElement('table:table-row');
166-
if ($span_row) {
167-
if ($span_row > 1) {
168-
$objWriter->writeAttribute('table:number-rows-repeated', (string) $span_row);
169-
}
170-
$objWriter->startElement('table:table-cell');
171-
$objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX);
157+
$cellIterator = $row->getCellIterator(iterateOnlyExistingCells: true);
158+
$cellIterator->rewind();
159+
$rowStyleExists = $sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0;
160+
if ($cellIterator->valid() || $rowStyleExists) {
161+
if ($spanRow) {
162+
$objWriter->startElement('table:table-row');
163+
$objWriter->writeAttribute(
164+
'table:number-rows-repeated',
165+
(string) $spanRow
166+
);
172167
$objWriter->endElement();
173-
$span_row = 0;
174-
} else {
175-
if ($sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) {
176-
$objWriter->writeAttribute(
177-
'table:style-name',
178-
sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
179-
);
180-
}
181-
$this->writeCells($objWriter, $cellIterator);
168+
$spanRow = 0;
169+
}
170+
$objWriter->startElement('table:table-row');
171+
if ($rowStyleExists) {
172+
$objWriter->writeAttribute(
173+
'table:style-name',
174+
sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
175+
);
182176
}
177+
$this->writeCells($objWriter, $cellIterator);
183178
$objWriter->endElement();
184179
} else {
185-
++$span_row;
180+
++$spanRow;
186181
}
187182
}
188183
}
@@ -192,7 +187,6 @@ private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetInd
192187
*/
193188
private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void
194189
{
195-
$numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
196190
$prevColumn = -1;
197191
foreach ($cells as $cell) {
198192
/** @var Cell $cell */
@@ -293,17 +287,6 @@ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void
293287
$objWriter->endElement();
294288
$prevColumn = $column;
295289
}
296-
297-
$numberColsRepeated = $numberColsRepeated - $prevColumn - 1;
298-
if ($numberColsRepeated > 0) {
299-
if ($numberColsRepeated > 1) {
300-
$objWriter->startElement('table:table-cell');
301-
$objWriter->writeAttribute('table:number-columns-repeated', (string) $numberColsRepeated);
302-
$objWriter->endElement();
303-
} else {
304-
$objWriter->writeElement('table:table-cell');
305-
}
306-
}
307290
}
308291

309292
/**

src/PhpSpreadsheet/Writer/Ods/Styles.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,17 @@ public function write(): string
5656

5757
$objWriter->writeElement('office:font-face-decls');
5858
$objWriter->writeElement('office:styles');
59-
$objWriter->writeElement('office:automatic-styles');
60-
$objWriter->writeElement('office:master-styles');
59+
$objWriter->startElement('office:automatic-styles');
60+
$objWriter->startElement('style:page-layout');
61+
$objWriter->writeAttribute('style:name', 'Mpm1');
62+
$objWriter->endElement(); // style:page-layout
63+
$objWriter->endElement(); // office:automatic-styles
64+
$objWriter->startElement('office:master-styles');
65+
$objWriter->startElement('style:master-page');
66+
$objWriter->writeAttribute('style:name', 'Default');
67+
$objWriter->writeAttribute('style:page-layout-name', 'Mpm1');
68+
$objWriter->endElement(); //style:master-page
69+
$objWriter->endElement(); //office:master-styles
6170
$objWriter->endElement();
6271

6372
return $objWriter->getData();

src/PhpSpreadsheet/Writer/Xlsx.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ class Xlsx extends BaseWriter
140140

141141
private bool $useDynamicArray = false;
142142

143+
private ?bool $forceFullCalc = null;
144+
143145
/**
144146
* Create a new Xlsx Writer.
145147
*/
@@ -342,7 +344,7 @@ public function save($filename, int $flags = 0): void
342344
$zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet);
343345

344346
// Add workbook to ZIP file
345-
$zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas);
347+
$zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas, $this->forceFullCalc);
346348

347349
$chartCount = 0;
348350
// Add worksheets
@@ -747,4 +749,20 @@ private function determineUseDynamicArrays(): void
747749
{
748750
$this->useDynamicArray = $this->preCalculateFormulas && Calculation::getInstance($this->spreadSheet)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY && !$this->useCSEArrays;
749751
}
752+
753+
/**
754+
* If this is set when a spreadsheet is opened,
755+
* values may not be automatically re-calculated,
756+
* and a button will be available to force re-calculation.
757+
* This may apply to all spreadsheets open at that time.
758+
* If null, this will be set to the opposite of $preCalculateFormulas.
759+
* It is likely that false is the desired setting, although
760+
* cases have been reported where true is required (issue #456).
761+
*/
762+
public function setForceFullCalc(?bool $forceFullCalc): self
763+
{
764+
$this->forceFullCalc = $forceFullCalc;
765+
766+
return $this;
767+
}
750768
}

src/PhpSpreadsheet/Writer/Xlsx/Workbook.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ class Workbook extends WriterPart
1515
* Write workbook to XML format.
1616
*
1717
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
18+
* @param ?bool $forceFullCalc If null, !$preCalculateFormulas
1819
*
1920
* @return string XML Output
2021
*/
21-
public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false): string
22+
public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false, ?bool $forceFullCalc = null): string
2223
{
2324
// Create XML writer
2425
if ($this->getParentWriter()->getUseDiskCaching()) {
@@ -57,7 +58,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormul
5758
(new DefinedNamesWriter($objWriter, $spreadsheet))->write();
5859

5960
// calcPr
60-
$this->writeCalcPr($objWriter, $preCalculateFormulas);
61+
$this->writeCalcPr($objWriter, $preCalculateFormulas, $forceFullCalc);
6162

6263
$objWriter->endElement();
6364

@@ -148,7 +149,7 @@ private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spre
148149
*
149150
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
150151
*/
151-
private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = true): void
152+
private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas, ?bool $forceFullCalc): void
152153
{
153154
$objWriter->startElement('calcPr');
154155

@@ -160,7 +161,11 @@ private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas =
160161
// fullCalcOnLoad isn't needed if we will calculate before writing
161162
$objWriter->writeAttribute('calcCompleted', ($preCalculateFormulas) ? '1' : '0');
162163
$objWriter->writeAttribute('fullCalcOnLoad', ($preCalculateFormulas) ? '0' : '1');
163-
$objWriter->writeAttribute('forceFullCalc', ($preCalculateFormulas) ? '0' : '1');
164+
if ($forceFullCalc === null) {
165+
$objWriter->writeAttribute('forceFullCalc', $preCalculateFormulas ? '0' : '1');
166+
} else {
167+
$objWriter->writeAttribute('forceFullCalc', $forceFullCalc ? '1' : '0');
168+
}
164169

165170
$objWriter->endElement();
166171
}

src/PhpSpreadsheet/Writer/ZipStream2.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
use ZipStream\Option\Archive;
66
use ZipStream\ZipStream;
77

8+
/**
9+
* Either ZipStream2 or ZipStream3, but not both, may be used.
10+
* For code coverage testing, it will always be ZipStream3.
11+
*
12+
* @codeCoverageIgnore
13+
*/
814
class ZipStream2
915
{
1016
/**
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
9+
10+
class RepeatEmptyCellsAndRowsTest extends AbstractFunctional
11+
{
12+
public function testSaveAndLoadHyperlinks(): void
13+
{
14+
$spreadsheetOld = new Spreadsheet();
15+
$oldSheet = $spreadsheetOld->getActiveSheet();
16+
$oldSheet->setCellValue('C1', 'xx');
17+
$oldSheet->setCellValue('G1', 'aa');
18+
$oldSheet->setCellValue('BB1', 'bb');
19+
$oldSheet->setCellValue('A6', 'aaa');
20+
$oldSheet->setCellValue('B7', 'bbb');
21+
$oldSheet->getRowDimension(10)->setRowHeight(12);
22+
$oldSheet->setCellValue('A12', 'this is A12');
23+
$style = $oldSheet->getStyle('B14:D14');
24+
$style->getFont()->setBold(true);
25+
$oldSheet->getCell('E15')->setValue('X');
26+
$oldSheet->mergeCells('E15:G16');
27+
$oldSheet->getCell('J15')->setValue('j15');
28+
$oldSheet->getCell('J16')->setValue('j16');
29+
$oldSheet->getCell('A19')->setValue('lastrow');
30+
$spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods');
31+
$spreadsheetOld->disconnectWorksheets();
32+
33+
$sheet = $spreadsheet->getActiveSheet();
34+
self::assertSame('xx', $sheet->getCell('C1')->getValue());
35+
self::assertSame('aa', $sheet->getCell('G1')->getValue());
36+
self::assertSame('bb', $sheet->getCell('BB1')->getValue());
37+
self::assertSame('aaa', $sheet->getCell('A6')->getValue());
38+
self::assertSame('bbb', $sheet->getCell('B7')->getValue());
39+
self::assertSame('this is A12', $sheet->getCell('A12')->getValue());
40+
// Read styles, including row height, not yet implemented for ODS
41+
self::assertSame('j15', $sheet->getCell('J15')->getValue());
42+
self::assertSame('j16', $sheet->getCell('J16')->getValue());
43+
self::assertSame(['E15:G16' => 'E15:G16'], $sheet->getMergeCells());
44+
self::assertSame('lastrow', $sheet->getCell('A19')->getValue());
45+
46+
$spreadsheet->disconnectWorksheets();
47+
}
48+
}

0 commit comments

Comments
 (0)