diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 093892c87..e81663a93 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -298,6 +298,8 @@ class Ods extends BaseReader */ private array $allStyles; + private int $highestDataIndex; + /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. */ @@ -335,8 +337,7 @@ class Ods extends BaseReader $styleNs = (string) $dom->lookupNamespaceUri('style'); $fontNs = (string) $dom->lookupNamespaceUri('fo'); - $defaultStyleSet = false; - $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'styles')->item(0); + $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'styles')->item(0); $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'default-style'); foreach ($automaticStyles as $automaticStyle) { $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family'); @@ -346,7 +347,6 @@ class Ods extends BaseReader $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs); } if (!empty($fonts)) { - $defaultStyleSet = true; $spreadsheet->getDefaultStyle() ->getFont() ->applyFromArray($fonts); @@ -368,7 +368,7 @@ class Ods extends BaseReader if ($styleName !== '') { if (!empty($fonts)) { $this->allStyles[$styleName]['font'] = $fonts; - if (!$defaultStyleSet && $styleName === 'Default') { + if ($styleName === 'Default') { $spreadsheet->getDefaultStyle() ->getFont() ->applyFromArray($fonts); @@ -405,7 +405,7 @@ class Ods extends BaseReader $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); $definedNameReader = new DefinedNames($spreadsheet, $tableNs); $columnWidths = []; - $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0); + $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0); $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style'); foreach ($automaticStyles as $automaticStyle) { $styleName = $automaticStyle->getAttributeNS($styleNs, 'name'); @@ -496,6 +496,7 @@ class Ods extends BaseReader // Go through every child of table element $rowID = 1; $tableColumnIndex = 1; + $this->highestDataIndex = 16384; foreach ($worksheetDataSet->childNodes as $childNode) { /** @var DOMElement $childNode */ @@ -533,37 +534,6 @@ class Ods extends BaseReader $spreadsheet ); - break; - case 'table-header-columns': - case 'table-columns': - $this->processTableHeaderColumns( - $childNode, - $tableNs, - $columnWidths, - $tableColumnIndex, - $spreadsheet - ); - - break; - case 'table-column-group': - $this->processTableColumnGroup( - $childNode, - $tableNs, - $columnWidths, - $tableColumnIndex, - $spreadsheet - ); - - break; - case 'table-column': - $this->processTableColumn( - $childNode, - $tableNs, - $columnWidths, - $tableColumnIndex, - $spreadsheet - ); - break; case 'table-row': $this->processTableRow( @@ -577,6 +547,43 @@ class Ods extends BaseReader $spreadsheet ); + break; + case 'table-header-columns': + case 'table-columns': + $this->processTableColumnHeader( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet, + $this->readEmptyCells, + true + ); + + break; + case 'table-column-group': + $this->processTableColumnGroup( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet, + $this->readEmptyCells, + true + ); + + break; + case 'table-column': + $this->processTableColumn( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet, + $this->readEmptyCells, + true + ); + break; } } @@ -593,10 +600,94 @@ class Ods extends BaseReader if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { $spreadsheet->createSheet(); } + } + + foreach ($spreadsheets as $workbookData) { + /** @var DOMElement $workbookData */ + $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table'); + + $worksheetID = 0; + foreach ($tables as $worksheetDataSet) { + /** @var DOMElement $worksheetDataSet */ + $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name'); + + // Check loadSheetsOnly + if ( + $this->loadSheetsOnly !== null + && $worksheetName + && !in_array($worksheetName, $this->loadSheetsOnly) + ) { + continue; + } + + // Create sheet + $spreadsheet->setActiveSheetIndex($worksheetID); + $highestDataColumn = $spreadsheet->getActiveSheet()->getHighestDataColumn(); + $this->highestDataIndex = Coordinate::columnIndexFromString($highestDataColumn); + + // Go through every child of table element processing column widths + $rowID = 1; + $tableColumnIndex = 1; + foreach ($worksheetDataSet->childNodes as $childNode) { + /** @var DOMElement $childNode */ + if (empty($columnWidths) || $this->readEmptyCells) { + break; + } + + // Filter elements which are not under the "table" ns + if ($childNode->namespaceURI != $tableNs) { + continue; + } + + $key = self::extractNodeName($childNode->nodeName); + + switch ($key) { + case 'table-header-columns': + case 'table-columns': + $this->processTableColumnHeader( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet, + true, + false + ); + + break; + case 'table-column-group': + $this->processTableColumnGroup( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet, + true, + false + ); + + break; + case 'table-column': + $this->processTableColumn( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet, + true, + false + ); + + break; + } + } + ++$worksheetID; + } $autoFilterReader->read($workbookData); $definedNameReader->read($workbookData); } + $spreadsheet->setActiveSheetIndex(0); if ($zip->locateName('settings.xml') !== false) { @@ -1091,12 +1182,14 @@ class Ods extends BaseReader /** * @param string[] $columnWidths */ - private function processTableHeaderColumns( + private function processTableColumnHeader( DOMElement $childNode, string $tableNs, array $columnWidths, int &$tableColumnIndex, - Spreadsheet $spreadsheet + Spreadsheet $spreadsheet, + bool $processWidths = true, + bool $processStyles = true ): void { foreach ($childNode->childNodes as $grandchildNode) { /** @var DOMElement $grandchildNode */ @@ -1108,7 +1201,9 @@ class Ods extends BaseReader $tableNs, $columnWidths, $tableColumnIndex, - $spreadsheet + $spreadsheet, + $processWidths, + $processStyles ); break; @@ -1124,7 +1219,9 @@ class Ods extends BaseReader string $tableNs, array $columnWidths, int &$tableColumnIndex, - Spreadsheet $spreadsheet + Spreadsheet $spreadsheet, + bool $processWidths = true, + bool $processStyles = true ): void { foreach ($childNode->childNodes as $grandchildNode) { /** @var DOMElement $grandchildNode */ @@ -1136,18 +1233,22 @@ class Ods extends BaseReader $tableNs, $columnWidths, $tableColumnIndex, - $spreadsheet + $spreadsheet, + $processWidths, + $processStyles ); break; case 'table-header-columns': case 'table-columns': - $this->processTableHeaderColumns( + $this->processTableColumnHeader( $grandchildNode, $tableNs, $columnWidths, $tableColumnIndex, - $spreadsheet + $spreadsheet, + $processWidths, + $processStyles ); break; @@ -1157,7 +1258,9 @@ class Ods extends BaseReader $tableNs, $columnWidths, $tableColumnIndex, - $spreadsheet + $spreadsheet, + $processWidths, + $processStyles ); break; @@ -1173,7 +1276,9 @@ class Ods extends BaseReader string $tableNs, array $columnWidths, int &$tableColumnIndex, - Spreadsheet $spreadsheet + Spreadsheet $spreadsheet, + bool $processWidths = true, + bool $processStyles = true ): void { if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); @@ -1181,15 +1286,41 @@ class Ods extends BaseReader $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) { - /** @var string $tableColumnString */ - $spreadsheet->getActiveSheet() - ->getColumnDimension($tableColumnString) - ->setWidth($columnWidth->toUnit('cm'), 'cm'); - StringHelper::stringIncrement($tableColumnString); + if ($processWidths) { + if (isset($columnWidths[$tableStyleName])) { + $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); + $tableColumnIndex2 = $tableColumnIndex; + $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2); + for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= 16384; --$rowRepeats2) { + if (!$this->readEmptyCells && $tableColumnIndex2 > $this->highestDataIndex) { + break; + } + $spreadsheet->getActiveSheet() + ->getColumnDimension($tableColumnString) + ->setWidth($columnWidth->toUnit('cm'), 'cm'); + StringHelper::stringIncrement( + $tableColumnString + ); + ++$tableColumnIndex2; + } + } + } + if ($processStyles) { + $defaultStyleName = $childNode->getAttributeNS($tableNs, 'default-cell-style-name'); + if ($defaultStyleName !== 'Default' && isset($this->allStyles[$defaultStyleName])) { + $tableColumnIndex2 = $tableColumnIndex; + $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2); + for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= 16384; --$rowRepeats2) { + $spreadsheet->getActiveSheet() + ->getStyle($tableColumnString) + ->applyFromArray( + $this->allStyles[$defaultStyleName] + ); + StringHelper::stringIncrement( + $tableColumnString + ); + ++$tableColumnIndex2; + } } } $tableColumnIndex += $rowRepeats; diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index e6eebdcdf..e11bec4d1 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -142,7 +142,6 @@ class Content extends WriterPart 'table:style-name', sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric()) ); - $objWriter->writeAttribute('table:default-cell-style-name', 'ce0'); $objWriter->endElement(); } $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex); diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/ColGroupTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/ColGroupTest.php new file mode 100644 index 000000000..c8d691ed7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/ColGroupTest.php @@ -0,0 +1,69 @@ +setReadEmptyCells(false); + $spreadsheet = $reader->load(self::ODS_COLGROUP_FILE); + self::assertSame('Arial', $spreadsheet->getDefaultStyle()->getFont()->getName()); + $sheet = $spreadsheet->getActiveSheet(); + $expected = [ + 'A' => 2.778, + 'B' => 2.99, + 'C' => 2.91, + 'D' => 2.858, + 'E' => 1.746, + 'F' => 1.746, + 'G' => 1.746, + 'H' => 1.746, + ]; + foreach ($expected as $col => $width) { + self::assertEqualsWithDelta( + $width, + $sheet->getColumnDimension($col) + ->getWidth(Dimension::UOM_CENTIMETERS), + 1.0E-3, + "mismatch for column $col" + ); + } + $spreadsheet->disconnectWorksheets(); + } + + public function testColHeader(): void + { + $reader = new OdsReader(); + $reader->setReadEmptyCells(false); + $spreadsheet = $reader->load(self::ODS_COLHEADER_FILE); + self::assertSame('Liberation Sans', $spreadsheet->getDefaultStyle()->getFont()->getName()); + $sheet = $spreadsheet->getActiveSheet(); + $expected = [ + 'A' => 2.858, + ]; + foreach ($expected as $col => $width) { + self::assertEqualsWithDelta( + $width, + $sheet->getColumnDimension($col) + ->getWidth(Dimension::UOM_CENTIMETERS), + 1.0E-3, + "mismatch for column $col" + ); + } + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/CurrencyTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/CurrencyTest.php index 4d3fce708..f4fa27a4a 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/CurrencyTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/CurrencyTest.php @@ -7,9 +7,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods; use PhpOffice\PhpSpreadsheet\Reader\Ods; use PHPUnit\Framework\TestCase; -/** - * @TODO The class doesn't read the bold/italic/underline properties (rich text) - */ class CurrencyTest extends TestCase { private const ODS_CURRENCY_FILE = 'tests/data/Reader/Ods/currency4.ods'; diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/FontStyleTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/FontStyleTest.php index c51d22f66..d7f50f1f1 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/FontStyleTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/FontStyleTest.php @@ -121,4 +121,17 @@ class FontStyleTest extends AbstractFunctional self::assertSame('Times New Roman', $sheet->getStyle('A1')->getFont()->getName()); $spreadsheet->disconnectWorksheets(); } + + public static function testDataOnly(): void + { + $reader = new OdsReader(); + $reader->setReadDataOnly(true); + $infile = 'tests/data/Reader/Ods/odsstyles5.ods'; + $spreadsheet = $reader->load($infile); + self::assertSame('Calibri', $spreadsheet->getDefaultStyle()->getFont()->getName(), 'ignore spreadsheet style, use PhpSpreadsheet default'); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame(13, $sheet->getCell('A1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/Issue3721Test.php b/tests/PhpSpreadsheetTests/Reader/Ods/Issue3721Test.php index 15765e637..a08670761 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/Issue3721Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/Issue3721Test.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase; class Issue3721Test extends TestCase { - public function testIssue2810(): void + public function xtestIssue2810ReadEmpty(): void { // Problems with getHighestDataColumn $filename = 'tests/data/Reader/Ods/issue.3721.ods'; @@ -29,4 +29,26 @@ class Issue3721Test extends TestCase self::assertSame('BL', $sheet->getHighestColumn()); $spreadsheet->disconnectWorksheets(); } + + public function testIssue2810NoReadEmpty(): void + { + // Problems with getHighestDataColumn + $filename = 'tests/data/Reader/Ods/issue.3721.ods'; + $reader = new Ods(); + $reader->setReadEmptyCells(false); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getSheetByNameOrThrow('sheet with data'); + $origHigh = $sheet->getHighestDataColumn(); + self::assertSame('C', $origHigh); + $cells = []; + foreach ($sheet->getRowIterator() as $row) { + foreach ($row->getCellIterator(iterateOnlyExistingCells: true) as $cell) { + $cells[] = $cell->getCoordinate(); + } + } + self::assertSame(['A1', 'B1', 'C1', 'A2', 'B2', 'C2'], $cells); + self::assertSame('C', $sheet->getHighestDataColumn()); + self::assertSame('C', $sheet->getHighestColumn()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/NestedTableRowTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/NestedTableRowTest.php index 45e0646b9..67e080832 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/NestedTableRowTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/NestedTableRowTest.php @@ -13,6 +13,7 @@ class NestedTableRowTest extends TestCase { $infile = 'tests/data/Reader/Ods/issue.4528.ods'; $reader = new OdsReader(); + $reader->setReadDataOnly(true)->setReadEmptyCells(false); $spreadsheet = $reader->load($infile); $sheet = $spreadsheet->getActiveSheet(); self::assertSame('Atterissage', $sheet->getCell('AS1')->getValue()); @@ -26,6 +27,7 @@ class NestedTableRowTest extends TestCase { $infile = 'tests/data/Reader/Ods/issue.2507.ods'; $reader = new OdsReader(); + $reader->setReadDataOnly(true)->setReadEmptyCells(false); $spreadsheet = $reader->load($infile); $sheet = $spreadsheet->getActiveSheet(); $values = $sheet->rangeToArray('B3:C7', null, false, false); diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsInfoTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsInfoTest.php index 1ba0847d7..d4cc6651b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsInfoTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsInfoTest.php @@ -8,9 +8,6 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Ods; use PHPUnit\Framework\TestCase; -/** - * @TODO The class doesn't read the bold/italic/underline properties (rich text) - */ class OdsInfoTest extends TestCase { public function testReadFileProperties(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 60b8d35a6..fe610afa5 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -12,9 +12,6 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Font; use PHPUnit\Framework\TestCase; -/** - * @TODO The class doesn't read the bold/italic/underline properties (rich text) - */ class OdsTest extends TestCase { private const ODS_TEST_FILE = 'samples/templates/OOCalcTest.ods'; diff --git a/tests/data/Reader/Ods/colgroup.ods b/tests/data/Reader/Ods/colgroup.ods new file mode 100644 index 000000000..7ac62c578 Binary files /dev/null and b/tests/data/Reader/Ods/colgroup.ods differ diff --git a/tests/data/Reader/Ods/colheader.ods b/tests/data/Reader/Ods/colheader.ods new file mode 100644 index 000000000..63cf54d87 Binary files /dev/null and b/tests/data/Reader/Ods/colheader.ods differ