diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index fa3db908ef..bfb52401aa 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -491,9 +491,12 @@ private function processDomElementImg(Worksheet $sheet, int &$row, string &$colu } } + private string $currentColumn = 'A'; + private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'table') { + $this->currentColumn = 'A'; $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); $column = $this->setTableStartColumn($column); if ($this->tableLevel > 1 && $row > 1) { @@ -513,7 +516,10 @@ private function processDomElementTable(Worksheet $sheet, int &$row, string &$co private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { - if ($child->nodeName === 'tr') { + if ($child->nodeName === 'col') { + $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray); + ++$this->currentColumn; + } elseif ($child->nodeName === 'tr') { $column = $this->getTableStartColumn(); $cellContent = ''; $this->processDomElement($child, $sheet, $row, $column, $cellContent); @@ -877,7 +883,9 @@ private function applyInlineStyle(Worksheet &$sheet, $row, $column, $attributeAr return; } - if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { + if ($row <= 0 || $column === '') { + $cellStyle = new Style(); + } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { $columnTo = $column; for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { ++$columnTo; @@ -1009,16 +1017,20 @@ private function applyInlineStyle(Worksheet &$sheet, $row, $column, $attributeAr break; case 'width': - $sheet->getColumnDimension($column)->setWidth( - (new CssDimension($styleValue ?? ''))->width() - ); + if ($column !== '') { + $sheet->getColumnDimension($column)->setWidth( + (new CssDimension($styleValue ?? ''))->width() + ); + } break; case 'height': - $sheet->getRowDimension($row)->setRowHeight( - (new CssDimension($styleValue ?? ''))->height() - ); + if ($row > 0) { + $sheet->getRowDimension($row)->setRowHeight( + (new CssDimension($styleValue ?? ''))->height() + ); + } break; diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 2508154007..9913f33252 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -8,6 +8,7 @@ use DOMNode; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension; use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter; use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames; use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator; @@ -295,11 +296,29 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) $tableNs = $dom->lookupNamespaceUri('table'); $textNs = $dom->lookupNamespaceUri('text'); $xlinkNs = $dom->lookupNamespaceUri('xlink'); + $styleNs = $dom->lookupNamespaceUri('style'); $pageSettings->readStyleCrossReferences($dom); $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); $definedNameReader = new DefinedNames($spreadsheet, $tableNs); + $columnWidths = []; + $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0); + $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style'); + foreach ($automaticStyles as $automaticStyle) { + $styleName = $automaticStyle->getAttributeNS($styleNs, 'name'); + $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family'); + if ($styleFamily === 'table-column') { + $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties'); + if ($tcprops !== null) { + $tcprop = $tcprops->item(0); + if ($tcprop !== null) { + $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width'); + $columnWidths[$styleName] = $columnWidth; + } + } + } + } // Content $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0); @@ -340,6 +359,7 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) // Go through every child of table element $rowID = 1; + $tableColumnIndex = 1; foreach ($worksheetDataSet->childNodes as $childNode) { /** @var DOMElement $childNode */ @@ -366,6 +386,26 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) // $rowData = $cellData; // break; // } + break; + case 'table-column': + if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $rowRepeats = 1; + } + $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); + if (isset($columnWidths[$tableStyleName])) { + $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); + $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex); + for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) { + $spreadsheet->getActiveSheet() + ->getColumnDimension($tableColumnString) + ->setWidth($columnWidth->toUnit('cm'), 'cm'); + ++$tableColumnString; + } + } + $tableColumnIndex += $rowRepeats; + break; case 'table-row': if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 06f86cd173..29221e9910 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -543,9 +543,18 @@ public function getDefaultRowDimension() */ public function getColumnDimensions() { + /** @var callable */ + $callable = [self::class, 'columnDimensionCompare']; + uasort($this->columnDimensions, $callable); + return $this->columnDimensions; } + private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int + { + return $a->getColumnNumeric() - $b->getColumnNumeric(); + } + /** * Get default column dimension. * diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index e931421afd..e0a729ab83 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -126,7 +126,16 @@ private function writeSheets(XMLWriter $objWriter): void $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1)); $objWriter->writeElement('office:forms'); + $lastColumn = 0; foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { + $thisColumn = $columnDimension->getColumnNumeric(); + $emptyColumns = $thisColumn - $lastColumn - 1; + if ($emptyColumns > 0) { + $objWriter->startElement('table:table-column'); + $objWriter->writeAttribute('table:number-columns-repeated', (string) $emptyColumns); + $objWriter->endElement(); + } + $lastColumn = $thisColumn; $objWriter->startElement('table:table-column'); $objWriter->writeAttribute( 'table:style-name', diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 47fc13d8ec..f7c769cd1b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -141,8 +141,8 @@ public function testReadValueAndComments(): void $firstSheet = $spreadsheet->getSheet(0); - self::assertEquals(29, $firstSheet->getHighestRow()); - self::assertEquals('N', $firstSheet->getHighestColumn()); + self::assertEquals(29, $firstSheet->getHighestDataRow()); + self::assertEquals('N', $firstSheet->getHighestDataColumn()); // Simple cell value self::assertEquals('Test String 1', $firstSheet->getCell('A1')->getValue()); diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnDimension2Test.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnDimension2Test.php new file mode 100644 index 0000000000..f5878eef56 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnDimension2Test.php @@ -0,0 +1,65 @@ +getActiveSheet(); + $expectedCm = 0.45; + foreach ($columns as $column) { + $sheet->getColumnDimension($column)->setWidth($expectedCm, 'cm'); + } + if ($type === 'Html') { + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $type, null, [self::class, 'inlineCss']); + } else { + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $type); + } + $spreadsheet->disconnectWorksheets(); + $sheet = $reloadedSpreadsheet->getActiveSheet(); + for ($column = 'A'; $column !== 'Z'; ++$column) { + if (in_array($column, $columns, true)) { + self::assertEqualsWithDelta($expectedCm, $sheet->getColumnDimension($column)->getWidth(Dimension::UOM_CENTIMETERS), 1E-3, "column $column"); + } elseif ($type === 'Xls' && $column <= 'T') { + // Xls is a bit weird. Columns through max used + // actually set a width in the spreadsheet file. + // Columns above max used obviously do not. + self::assertEqualsWithDelta(9.140625, $sheet->getColumnDimension($column)->getWidth(), 1E-3, "column $column"); + } elseif ($type === 'Html' && $column <= 'T') { + // Html is a lot like Xls. Columns through max used + // actually set a width in the spreadsheet file. + // Columns above max used obviously do not. + self::assertEqualsWithDelta(7.998, $sheet->getColumnDimension($column)->getWidth(), 1E-3, "column $column"); + } else { + self::assertSame(-1.0, $sheet->getColumnDimension($column)->getWidth(), "column $column"); + } + } + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public static function providerType(): array + { + return [ + ['Xlsx'], + ['Xls'], + ['Ods'], + ['Html'], + ]; + } + + public static function inlineCss(Html $writer): void + { + $writer->setUseInlineCss(true); + } +}