Skip to content

Commit 2d9375f

Browse files
committed
Wrapped Cells and Default Row Height
Fix #4584. As discussed there, MS has implemented this situation in a way that I frankly do not understand. Marking a row with a wrapped cell to *not* use the default row height seems to affect every other populated row. Very odd. Nevertheless, adding a new `customFormat` boolean property to RowDimension seems to be a way to resolve this problem. Note that the Xml generated by Excel already outputs a `customFormat` attribute when a style is applied to a row. This PR just adds that property for when it is needed for rowHeight. It need not, and should not, be set by the application when a style is applied to the row; PhpSpreadsheet will take care of that on its own. Although the problem was raised for Xlsx format (I think), I investigated other relevant output formats as well. Html tends to do its own cell wrapping. I think the results after this change match the results before. Xls already handled the output side of this situation without difficulty. A change was needed on the input side, and is included as part of this PR. Ods did not handle default row height at all. As with other style properties, it is difficult to handle this on the input side, and this PR does nothing to improve that situation. However, we are, at least, able to provide some relief on the output side. Not complete relief - in order to apply the default size to unpopulated rows, we'd need to at least populate them with the `table:number-rows-repeated` attribute, and that has often been a source of problems, so I'm not willing to go there yet. This PR just ensures that all populated rows will have the correct height.
1 parent 889d26a commit 2d9375f

File tree

9 files changed

+308
-46
lines changed

9 files changed

+308
-46
lines changed

src/PhpSpreadsheet/Reader/Xls.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2647,7 +2647,14 @@ protected function readRow(): void
26472647
$useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
26482648

26492649
if (!$useDefaultHeight) {
2650-
$this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
2650+
if (
2651+
$this->phpSheet->getDefaultRowDimension()->getRowHeight() > 0
2652+
) {
2653+
$this->phpSheet->getRowDimension($r + 1)
2654+
->setCustomFormat(true, ($height === 255) ? -1 : ($height / 20));
2655+
} else {
2656+
$this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
2657+
}
26512658
}
26522659

26532660
// offset: 8; size: 2; not used

src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,33 @@ private function setColumnAttributes(string $columnAddress, array $columnAttribu
5050
* Set Worksheet row attributes by attributes array passed.
5151
*
5252
* @param int $rowNumber 1, 2, 3, ... 99, ...
53-
* @param array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float} $rowAttributes array of attributes (indexes are attribute name, values are value)
53+
* @param array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float, customFormat?: bool, ht?: float} $rowAttributes array of attributes (indexes are attribute name, values are value)
5454
* 'xfIndex', 'visible', 'collapsed', 'outlineLevel', 'rowHeight', ... ?
5555
*/
5656
private function setRowAttributes(int $rowNumber, array $rowAttributes): void
5757
{
5858
if (isset($rowAttributes['xfIndex'])) {
59-
$this->worksheet->getRowDimension($rowNumber)->setXfIndex($rowAttributes['xfIndex']);
59+
$this->worksheet->getRowDimension($rowNumber)
60+
->setXfIndex($rowAttributes['xfIndex']);
6061
}
6162
if (isset($rowAttributes['visible'])) {
62-
$this->worksheet->getRowDimension($rowNumber)->setVisible($rowAttributes['visible']);
63+
$this->worksheet->getRowDimension($rowNumber)
64+
->setVisible($rowAttributes['visible']);
6365
}
6466
if (isset($rowAttributes['collapsed'])) {
65-
$this->worksheet->getRowDimension($rowNumber)->setCollapsed($rowAttributes['collapsed']);
67+
$this->worksheet->getRowDimension($rowNumber)
68+
->setCollapsed($rowAttributes['collapsed']);
6669
}
6770
if (isset($rowAttributes['outlineLevel'])) {
68-
$this->worksheet->getRowDimension($rowNumber)->setOutlineLevel($rowAttributes['outlineLevel']);
71+
$this->worksheet->getRowDimension($rowNumber)
72+
->setOutlineLevel($rowAttributes['outlineLevel']);
6973
}
70-
if (isset($rowAttributes['rowHeight'])) {
71-
$this->worksheet->getRowDimension($rowNumber)->setRowHeight($rowAttributes['rowHeight']);
74+
if (isset($rowAttributes['customFormat'], $rowAttributes['rowHeight'])) {
75+
$this->worksheet->getRowDimension($rowNumber)
76+
->setCustomFormat($rowAttributes['customFormat'], $rowAttributes['rowHeight']);
77+
} elseif (isset($rowAttributes['rowHeight'])) {
78+
$this->worksheet->getRowDimension($rowNumber)
79+
->setRowHeight($rowAttributes['rowHeight']);
7280
}
7381
}
7482

@@ -203,20 +211,25 @@ private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDat
203211
$row = $rowx->attributes();
204212
if ($row !== null && (!$ignoreRowsWithNoCells || isset($rowx->c))) {
205213
$rowIndex = (int) $row['r'];
206-
if (isset($row['ht']) && !$readDataOnly) {
207-
$rowAttributes[$rowIndex]['rowHeight'] = (float) $row['ht'];
208-
}
209-
if (isset($row['hidden']) && self::boolean($row['hidden'])) {
210-
$rowAttributes[$rowIndex]['visible'] = false;
211-
}
212-
if (isset($row['collapsed']) && self::boolean($row['collapsed'])) {
213-
$rowAttributes[$rowIndex]['collapsed'] = true;
214-
}
215-
if (isset($row['outlineLevel']) && (int) $row['outlineLevel'] > 0) {
216-
$rowAttributes[$rowIndex]['outlineLevel'] = (int) $row['outlineLevel'];
217-
}
218-
if (isset($row['s']) && !$readDataOnly) {
219-
$rowAttributes[$rowIndex]['xfIndex'] = (int) $row['s'];
214+
if (!$readDataOnly) {
215+
if (isset($row['ht'])) {
216+
$rowAttributes[$rowIndex]['rowHeight'] = (float) $row['ht'];
217+
}
218+
if (isset($row['customFormat']) && self::boolean($row['customFormat'])) {
219+
$rowAttributes[$rowIndex]['customFormat'] = true;
220+
}
221+
if (isset($row['hidden']) && self::boolean($row['hidden'])) {
222+
$rowAttributes[$rowIndex]['visible'] = false;
223+
}
224+
if (isset($row['collapsed']) && self::boolean($row['collapsed'])) {
225+
$rowAttributes[$rowIndex]['collapsed'] = true;
226+
}
227+
if (isset($row['outlineLevel']) && (int) $row['outlineLevel'] > 0) {
228+
$rowAttributes[$rowIndex]['outlineLevel'] = (int) $row['outlineLevel'];
229+
}
230+
if (isset($row['s'])) {
231+
$rowAttributes[$rowIndex]['xfIndex'] = (int) $row['s'];
232+
}
220233
}
221234
if ($readFilterIsNotNull && empty($rowAttributes[$rowIndex])) {
222235
$rowAttributes[$rowIndex]['exists'] = true;

src/PhpSpreadsheet/Worksheet/RowDimension.php

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66

77
class RowDimension extends Dimension
88
{
9-
/**
10-
* Row index.
11-
*/
129
private ?int $rowIndex;
1310

1411
/**
@@ -23,9 +20,9 @@ class RowDimension extends Dimension
2320
*/
2421
private bool $zeroHeight = false;
2522

23+
private bool $customFormat = false;
24+
2625
/**
27-
* Create a new RowDimension.
28-
*
2926
* @param ?int $index Numeric row index
3027
*/
3128
public function __construct(?int $index = 0)
@@ -37,19 +34,11 @@ public function __construct(?int $index = 0)
3734
parent::__construct(null);
3835
}
3936

40-
/**
41-
* Get Row Index.
42-
*/
4337
public function getRowIndex(): ?int
4438
{
4539
return $this->rowIndex;
4640
}
4741

48-
/**
49-
* Set Row Index.
50-
*
51-
* @return $this
52-
*/
5342
public function setRowIndex(int $index): static
5443
{
5544
$this->rowIndex = $index;
@@ -76,35 +65,41 @@ public function getRowHeight(?string $unitOfMeasure = null): float
7665
* @param float $height in points. A value of -1 tells Excel to display this column in its default height.
7766
* By default, this will be the passed argument value; but this method also accepts an optional unit of measure
7867
* argument, and will convert the passed argument value to points from the specified UoM
79-
*
80-
* @return $this
8168
*/
8269
public function setRowHeight(float $height, ?string $unitOfMeasure = null): static
8370
{
8471
$this->height = ($unitOfMeasure === null || $height < 0)
8572
? $height
8673
: (new CssDimension("{$height}{$unitOfMeasure}"))->height();
74+
$this->customFormat = false;
8775

8876
return $this;
8977
}
9078

91-
/**
92-
* Get ZeroHeight.
93-
*/
9479
public function getZeroHeight(): bool
9580
{
9681
return $this->zeroHeight;
9782
}
9883

99-
/**
100-
* Set ZeroHeight.
101-
*
102-
* @return $this
103-
*/
10484
public function setZeroHeight(bool $zeroHeight): static
10585
{
10686
$this->zeroHeight = $zeroHeight;
10787

10888
return $this;
10989
}
90+
91+
public function getCustomFormat(): bool
92+
{
93+
return $this->customFormat;
94+
}
95+
96+
public function setCustomFormat(bool $customFormat, ?float $height = -1): self
97+
{
98+
$this->customFormat = $customFormat;
99+
if ($height !== null) {
100+
$this->height = $height;
101+
}
102+
103+
return $this;
104+
}
110105
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,21 @@ public function writeRowStyles(RowDimension $rowDimension, int $sheetId): void
302302
$this->writer->endElement(); // Close style:style
303303
}
304304

305+
public function writeDefaultRowStyle(RowDimension $rowDimension, int $sheetId): void
306+
{
307+
$this->writer->startElement('style:style');
308+
$this->writer->writeAttribute('style:family', 'table-row');
309+
$this->writer->writeAttribute(
310+
'style:name',
311+
sprintf('%s%d', self::ROW_STYLE_PREFIX, $sheetId)
312+
);
313+
314+
$this->writeRowProperties($rowDimension);
315+
316+
// End
317+
$this->writer->endElement(); // Close style:style
318+
}
319+
305320
public function writeTableStyle(Worksheet $worksheet, int $sheetId): void
306321
{
307322
$this->writer->startElement('style:style');

src/PhpSpreadsheet/Writer/Ods/Content.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetInd
174174
'table:style-name',
175175
sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
176176
);
177+
} elseif ($sheet->getDefaultRowDimension()->getRowHeight() > 0.0 && !$sheet->getRowDimension($row->getRowIndex())->getCustomFormat()) {
178+
$objWriter->writeAttribute(
179+
'table:style-name',
180+
sprintf('%s%d', Style::ROW_STYLE_PREFIX, $sheetIndex)
181+
);
177182
}
178183
$this->writeCells($objWriter, $cellIterator);
179184
$objWriter->endElement();
@@ -323,6 +328,10 @@ private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): voi
323328
}
324329
for ($i = 0; $i < $sheetCount; ++$i) {
325330
$worksheet = $spreadsheet->getSheet($i);
331+
$default = $worksheet->getDefaultRowDimension();
332+
if ($default->getRowHeight() > 0.0) {
333+
$styleWriter->writeDefaultRowStyle($default, $i);
334+
}
326335
foreach ($worksheet->getRowDimensions() as $rowDimension) {
327336
if ($rowDimension->getRowHeight() > 0.0) {
328337
$styleWriter->writeRowStyles($rowDimension, $i);

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,16 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w
13981398
}
13991399
}
14001400

1401+
$customHeightNeeded = false;
1402+
if ($worksheet->getDefaultRowDimension()->getRowHeight() >= 0) {
1403+
foreach ($worksheet->getRowDimensions() as $rowDimension) {
1404+
if ($rowDimension->getCustomFormat()) {
1405+
$customHeightNeeded = true;
1406+
1407+
break;
1408+
}
1409+
}
1410+
}
14011411
$currentRow = 0;
14021412
$emptyDimension = new RowDimension();
14031413
while ($currentRow++ < $highestRow) {
@@ -1411,6 +1421,7 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w
14111421

14121422
if ($writeCurrentRow) {
14131423
// Start a new row
1424+
$customFormatWritten = false;
14141425
$objWriter->startElement('row');
14151426
$objWriter->writeAttribute('r', "$currentRow");
14161427
$objWriter->writeAttribute('spans', '1:' . $colCount);
@@ -1419,6 +1430,12 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w
14191430
if ($rowDimension->getRowHeight() >= 0) {
14201431
$objWriter->writeAttribute('customHeight', '1');
14211432
$objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight()));
1433+
} elseif ($rowDimension->getCustomFormat()) {
1434+
$objWriter->writeAttribute('customFormat', '1');
1435+
$customFormatWritten = true;
1436+
$objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight()));
1437+
} elseif ($customHeightNeeded) {
1438+
$objWriter->writeAttribute('customHeight', '1');
14221439
}
14231440

14241441
// Row visibility
@@ -1439,7 +1456,9 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w
14391456
// Style
14401457
if ($rowDimension->getXfIndex() !== null) {
14411458
$objWriter->writeAttribute('s', (string) $rowDimension->getXfIndex());
1442-
$objWriter->writeAttribute('customFormat', '1');
1459+
if (!$customFormatWritten) {
1460+
$objWriter->writeAttribute('customFormat', '1');
1461+
}
14431462
}
14441463

14451464
// Write cells
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Ods;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheet\Writer\Ods as OdsWriter;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class Issue4584Test extends TestCase
12+
{
13+
public function testWriteRowDimensions(): void
14+
{
15+
$spreadsheet = new Spreadsheet();
16+
$sheet = $spreadsheet->getActiveSheet();
17+
$sheet->getDefaultRowDimension()->setRowHeight(20);
18+
$sheet->setCellValue('A1', 'hello there world 1');
19+
$sheet->getStyle('A1')->getAlignment()->setWrapText(true);
20+
$sheet->getRowDimension(1)->setCustomFormat(true);
21+
$sheet->setCellValue('A2', 'hello there world 2');
22+
$sheet->setCellValue('A4', 'hello there world 4');
23+
$writer = new OdsWriter($spreadsheet);
24+
$writerWorksheet = new OdsWriter\Content($writer);
25+
$data = $writerWorksheet->write();
26+
self::assertStringContainsString(
27+
'<style:style style:family="table-row" style:name="ro0"><style:table-row-properties style:row-height="0.706cm" style:use-optimal-row-height="false" fo:break-before="auto"/></style:style>',
28+
$data
29+
);
30+
self::assertStringContainsString(
31+
'<table:table-row><table:table-cell table:style-name="ce1" office:value-type="string"><text:p>hello there world 1</text:p></table:table-cell></table:table-row>',
32+
$data
33+
);
34+
self::assertStringContainsString(
35+
'<table:table-row table:style-name="ro0"><table:table-cell table:style-name="ce0" office:value-type="string"><text:p>hello there world 2</text:p></table:table-cell></table:table-row>',
36+
$data
37+
);
38+
self::assertStringContainsString(
39+
'<table:table-row table:number-rows-repeated="1"/><table:table-row table:style-name="ro0"><table:table-cell table:style-name="ce0" office:value-type="string"><text:p>hello there world 4</text:p></table:table-cell></table:table-row>',
40+
$data
41+
);
42+
$spreadsheet->disconnectWorksheets();
43+
}
44+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
9+
10+
class Issue4584Test extends AbstractFunctional
11+
{
12+
public function testWriteAndReadRowDimension(): void
13+
{
14+
$spreadsheet = new Spreadsheet();
15+
$sheet = $spreadsheet->getActiveSheet();
16+
$sheet->getDefaultRowDimension()->setRowHeight(20);
17+
$sheet->setCellValue('A1', 'hello there world 1');
18+
$sheet->getStyle('A1')->getAlignment()->setWrapText(true);
19+
$sheet->getRowDimension(1)->setCustomFormat(true);
20+
$sheet->setCellValue('A2', 'hello there world 2');
21+
$sheet->setCellValue('A4', 'hello there world 4');
22+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls');
23+
$spreadsheet->disconnectWorksheets();
24+
25+
$rsheet = $reloadedSpreadsheet->getActiveSheet();
26+
self::assertSame(20.0, $rsheet->getDefaultRowDimension()->getRowHeight());
27+
$row1 = $rsheet->getRowDimension(1);
28+
self::assertTrue($row1->getCustomFormat());
29+
self::assertSame(-1.0, $row1->getRowHeight());
30+
$row2 = $rsheet->getRowDimension(2);
31+
self::assertFalse($row2->getCustomFormat());
32+
self::assertSame(-1.0, $row2->getRowHeight());
33+
$row4 = $rsheet->getRowDimension(4);
34+
self::assertFalse($row4->getCustomFormat());
35+
self::assertSame(-1.0, $row4->getRowHeight());
36+
$reloadedSpreadsheet->disconnectWorksheets();
37+
}
38+
}

0 commit comments

Comments
 (0)