From 614f1c945c0708f6aceecc614ea3fa7852b1de3e Mon Sep 17 00:00:00 2001 From: Tiago Malheiro Date: Wed, 17 Mar 2021 14:13:18 +0000 Subject: [PATCH 01/28] Fix #1933. Swapped chart axis options --- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 583b262c35..7f0d4ab2aa 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -316,10 +316,10 @@ private function writePlotArea(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\W if ($chartType === DataSeries::TYPE_BUBBLECHART) { $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $yAxis); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); } - $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); } $objWriter->endElement(); From d555b5d312adf95d6c1cbed123fff850d8f1eef7 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 30 Apr 2021 20:05:45 +0200 Subject: [PATCH 02/28] Pattern Fill style should default to 'solid' if there is a pattern fill with colour but no style (#2050) * Pattern Fill style should default to 'solid' if there is a pattern fill style for a conditional; though may need to check if there are defined fg/bg colours as well; and only set a fill style if there are defined colurs --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 14 +++++++++----- src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php | 10 ++++++++++ .../Reader/Xlsx/DefaultFillTest.php | 11 +++++++++++ tests/data/Reader/XLSX/pr2050cf-fill.xlsx | Bin 0 -> 8267 bytes 5 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 tests/data/Reader/XLSX/pr2050cf-fill.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 21762f893b..ed4f44c3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Nothing. ### Fixed - +- Correct default fill style for conditional without a pattern defined [Issue #2035](https://github.com/PHPOffice/PhpSpreadsheet/issues/2035) [PR #2050](https://github.com/PHPOffice/PhpSpreadsheet/pull/2050) - Fixed issue where array key check for existince before accessing arrays in Xlsx.php. [PR #1970](https://github.com/PHPOffice/PhpSpreadsheet/pull/1970) - Fixed issue with quoted strings in number format mask rendered with toFormattedString() [Issue 1972#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) [PR #1978](https://github.com/PHPOffice/PhpSpreadsheet/pull/1978) - Fixed issue with percentage formats in number format mask rendered with toFormattedString() [Issue 1929#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #1928](https://github.com/PHPOffice/PhpSpreadsheet/pull/1928) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 2b0c7016f3..2968a3feaf 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -103,17 +103,21 @@ public static function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyl self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color) ); } elseif ($fillStyleXml->patternFill) { - $patternType = (string) $fillStyleXml->patternFill['patternType'] != '' - ? (string) $fillStyleXml->patternFill['patternType'] - : Fill::FILL_NONE; - - $fillStyle->setFillType($patternType); + $defaultFillStyle = Fill::FILL_NONE; if ($fillStyleXml->patternFill->fgColor) { $fillStyle->getStartColor()->setARGB(self::readColor($fillStyleXml->patternFill->fgColor, true)); + $defaultFillStyle = Fill::FILL_SOLID; } if ($fillStyleXml->patternFill->bgColor) { $fillStyle->getEndColor()->setARGB(self::readColor($fillStyleXml->patternFill->bgColor, true)); + $defaultFillStyle = Fill::FILL_SOLID; } + + $patternType = (string) $fillStyleXml->patternFill['patternType'] != '' + ? (string) $fillStyleXml->patternFill['patternType'] + : $defaultFillStyle; + + $fillStyle->setFillType($patternType); } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php b/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php index e3a6b2063b..caf85c0499 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php +++ b/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php @@ -75,6 +75,16 @@ public static function lookup(Color $color, int $defaultIndex = 0x00): int return self::$colorMap["#{$colorRgb}"]; } +// TODO Try and map RGB value to nearest colour within the define pallette +// $red = Color::getRed($colorRgb, false); +// $green = Color::getGreen($colorRgb, false); +// $blue = Color::getBlue($colorRgb, false); + +// $paletteSpace = 3; +// $newColor = ($red * $paletteSpace / 256) * ($paletteSpace * $paletteSpace) + +// ($green * $paletteSpace / 256) * $paletteSpace + +// ($blue * $paletteSpace / 256); + return $defaultIndex; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php index 88d666b212..ccdad06795 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php @@ -29,4 +29,15 @@ public function testDefaultFill(): void self::assertSame('none', $sheet->getCell('J16')->getStyle()->getFill()->getFillType()); self::assertSame('solid', $sheet->getCell('C2')->getStyle()->getFill()->getFillType()); } + + public function testDefaultConditionalFill(): void + { + // default fill pattern for a conditional style where the filltype is not defined + $filename = 'tests/data/Reader/XLSX/pr2050cf-fill.xlsx'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + + $style = $spreadsheet->getActiveSheet()->getConditionalStyles('A1')[0]->getStyle(); + self::assertSame('solid', $style->getFill()->getFillType()); + } } diff --git a/tests/data/Reader/XLSX/pr2050cf-fill.xlsx b/tests/data/Reader/XLSX/pr2050cf-fill.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e2183422d25a46b3513812937cb19a0c73c6a562 GIT binary patch literal 8267 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 E55p);2><{9 literal 0 HcmV?d00001 From 83e55cffcc162fe19fa67b15866328793b52fe7b Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 2 May 2021 22:00:48 +0200 Subject: [PATCH 03/28] First steps in the implementation of AutoFilters for ODS Reader and Writer (#2053) * First steps in the implementation of AutoFilters for ODS Reader and Writer, starting with reading a basic AutoFilter range (ignoring row visibility, filter types and active filters for the moment). And also some additional refactoring to extract the DefinedNames Reader into its own dedicated class as a part of overall code improvement... on the principle of "when working on a class, always try to leave the library codebase in a better state than you found it" * Provide a basic Ods Writer implementation for AutoFilters * AutoFilter Reader Test * AutoFilter Writer Test * Update Change Log --- CHANGELOG.md | 2 + docs/references/features-cross-reference.md | 4 +- phpstan-baseline.neon | 7 +- src/PhpSpreadsheet/Reader/Ods.php | 81 ++---------------- src/PhpSpreadsheet/Reader/Ods/AutoFilter.php | 45 ++++++++++ src/PhpSpreadsheet/Reader/Ods/BaseReader.php | 75 ++++++++++++++++ .../Reader/Ods/DefinedNames.php | 65 ++++++++++++++ src/PhpSpreadsheet/Writer/Ods/AutoFilters.php | 63 ++++++++++++++ src/PhpSpreadsheet/Writer/Ods/Content.php | 1 + .../Reader/Ods/AutoFilterTest.php | 31 +++++++ .../Writer/Ods/AutoFilterTest.php | 33 +++++++ tests/data/Reader/Ods/AutoFilter.ods | Bin 0 -> 10115 bytes 12 files changed, 325 insertions(+), 82 deletions(-) create mode 100644 src/PhpSpreadsheet/Reader/Ods/AutoFilter.php create mode 100644 src/PhpSpreadsheet/Reader/Ods/BaseReader.php create mode 100644 src/PhpSpreadsheet/Reader/Ods/DefinedNames.php create mode 100644 src/PhpSpreadsheet/Writer/Ods/AutoFilters.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/AutoFilterTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php create mode 100644 tests/data/Reader/Ods/AutoFilter.ods diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4f44c3f6..7940a6a29b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Implemented basic AutoFiltering for Ods Reader and Writer [PR #2053](https://github.com/PHPOffice/PhpSpreadsheet/pull/2053) - Improved support for Row and Column ranges in formulae [Issue #1755](https://github.com/PHPOffice/PhpSpreadsheet/issues/1755) [PR #2028](https://github.com/PHPOffice/PhpSpreadsheet/pull/2028) +- Implemented URLENCODE() Web Function - Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions. - Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908) diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 716a3787b9..9dcf8d919e 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -1313,13 +1313,13 @@ ● ● - + ● ● ● - + ● diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8935540000..89f50e3b76 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2812,12 +2812,7 @@ parameters: - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 7 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:convertToExcelAddressValue\\(\\) should return string but returns string\\|null\\.$#" - count: 1 + count: 3 path: src/PhpSpreadsheet/Reader/Ods.php - diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 1a4d7ca3c1..d4163cf7e9 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -11,8 +11,9 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; -use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; +use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter; +use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames; use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings; use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; @@ -22,7 +23,6 @@ use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; use XMLReader; use ZipArchive; @@ -304,8 +304,10 @@ public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) $pageSettings->readStyleCrossReferences($dom); - // Content + $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); + $definedNameReader = new DefinedNames($spreadsheet, $tableNs); + // Content $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body') ->item(0) ->getElementsByTagNameNS($officeNs, 'spreadsheet'); @@ -642,8 +644,8 @@ public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) ++$worksheetID; } - $this->readDefinedRanges($spreadsheet, $workbookData, $tableNs); - $this->readDefinedExpressions($spreadsheet, $workbookData, $tableNs); + $autoFilterReader->read($workbookData); + $definedNameReader->read($workbookData); } $spreadsheet->setActiveSheetIndex(0); @@ -771,26 +773,6 @@ private function parseRichText($is) return $value; } - private function convertToExcelAddressValue(string $openOfficeAddress): string - { - $excelAddress = $openOfficeAddress; - - // Cell range 3-d reference - // As we don't support 3-d ranges, we're just going to take a quick and dirty approach - // and assume that the second worksheet reference is the same as the first - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); - // Cell range reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress); - // Cell reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress); - // Cell range reference - $excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress); - // Simple cell reference - $excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress); - - return $excelAddress; - } - private function convertToExcelFormulaValue(string $openOfficeFormula): string { $temp = explode('"', $openOfficeFormula); @@ -816,53 +798,4 @@ private function convertToExcelFormulaValue(string $openOfficeFormula): string return $excelFormula; } - - /** - * Read any Named Ranges that are defined in this spreadsheet. - */ - private function readDefinedRanges(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void - { - $namedRanges = $workbookData->getElementsByTagNameNS($tableNs, 'named-range'); - foreach ($namedRanges as $definedNameElement) { - $definedName = $definedNameElement->getAttributeNS($tableNs, 'name'); - $baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address'); - $range = $definedNameElement->getAttributeNS($tableNs, 'cell-range-address'); - - $baseAddress = $this->convertToExcelAddressValue($baseAddress); - $range = $this->convertToExcelAddressValue($range); - - $this->addDefinedName($spreadsheet, $baseAddress, $definedName, $range); - } - } - - /** - * Read any Named Formulae that are defined in this spreadsheet. - */ - private function readDefinedExpressions(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void - { - $namedExpressions = $workbookData->getElementsByTagNameNS($tableNs, 'named-expression'); - foreach ($namedExpressions as $definedNameElement) { - $definedName = $definedNameElement->getAttributeNS($tableNs, 'name'); - $baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address'); - $expression = $definedNameElement->getAttributeNS($tableNs, 'expression'); - - $baseAddress = $this->convertToExcelAddressValue($baseAddress); - $expression = $this->convertToExcelFormulaValue($expression); - - $this->addDefinedName($spreadsheet, $baseAddress, $definedName, $expression); - } - } - - /** - * Assess scope and store the Defined Name. - */ - private function addDefinedName(Spreadsheet $spreadsheet, string $baseAddress, string $definedName, string $value): void - { - [$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true); - $worksheet = $spreadsheet->getSheetByName($sheetReference); - // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet - if ($worksheet !== null) { - $spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value)); - } - } } diff --git a/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php b/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php new file mode 100644 index 0000000000..bdc8b3ffc3 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php @@ -0,0 +1,45 @@ +readAutoFilters($workbookData); + } + + protected function readAutoFilters(DOMElement $workbookData): void + { + $databases = $workbookData->getElementsByTagNameNS($this->tableNs, 'database-ranges'); + + foreach ($databases as $autofilters) { + foreach ($autofilters->childNodes as $autofilter) { + $autofilterRange = $this->getAttributeValue($autofilter, 'target-range-address'); + if ($autofilterRange !== null) { + $baseAddress = $this->convertToExcelAddressValue($autofilterRange); + $this->spreadsheet->getActiveSheet()->setAutoFilter($baseAddress); + } + } + } + } + + protected function getAttributeValue(?DOMNode $node, string $attributeName): ?string + { + if ($node !== null && $node->attributes !== null) { + $attribute = $node->attributes->getNamedItemNS( + $this->tableNs, + $attributeName + ); + + if ($attribute !== null) { + return $attribute->nodeValue; + } + } + + return null; + } +} diff --git a/src/PhpSpreadsheet/Reader/Ods/BaseReader.php b/src/PhpSpreadsheet/Reader/Ods/BaseReader.php new file mode 100644 index 0000000000..82d4171020 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Ods/BaseReader.php @@ -0,0 +1,75 @@ +spreadsheet = $spreadsheet; + $this->tableNs = $tableNs; + } + + abstract public function read(DOMElement $workbookData): void; + + protected function convertToExcelAddressValue(string $openOfficeAddress): string + { + $excelAddress = $openOfficeAddress; + + // Cell range 3-d reference + // As we don't support 3-d ranges, we're just going to take a quick and dirty approach + // and assume that the second worksheet reference is the same as the first + $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); + // Cell range reference in another sheet + $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress ?? ''); + // Cell reference in another sheet + $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress ?? ''); + // Cell range reference + $excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress ?? ''); + // Simple cell reference + $excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress ?? ''); + + return $excelAddress ?? ''; + } + + protected function convertToExcelFormulaValue(string $openOfficeFormula): string + { + $temp = explode('"', $openOfficeFormula); + $tKey = false; + foreach ($temp as &$value) { + // @var string $value + // Only replace in alternate array entries (i.e. non-quoted blocks) + if ($tKey = !$tKey) { + // Cell range reference in another sheet + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); + // Cell reference in another sheet + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); + // Cell range reference + $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); + // Simple cell reference + $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + + $value = Calculation::translateSeparator(';', ',', $value ?? '', $inBraces); + } + } + + // Then rebuild the formula string + $excelFormula = implode('"', $temp); + + return $excelFormula; + } +} diff --git a/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php new file mode 100644 index 0000000000..79f5c02754 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php @@ -0,0 +1,65 @@ +readDefinedRanges($workbookData); + $this->readDefinedExpressions($workbookData); + } + + /** + * Read any Named Ranges that are defined in this spreadsheet. + */ + protected function readDefinedRanges(DOMElement $workbookData): void + { + $namedRanges = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-range'); + foreach ($namedRanges as $definedNameElement) { + $definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name'); + $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); + $range = $definedNameElement->getAttributeNS($this->tableNs, 'cell-range-address'); + + $baseAddress = $this->convertToExcelAddressValue($baseAddress); + $range = $this->convertToExcelAddressValue($range); + + $this->addDefinedName($baseAddress, $definedName, $range); + } + } + + /** + * Read any Named Formulae that are defined in this spreadsheet. + */ + protected function readDefinedExpressions(DOMElement $workbookData): void + { + $namedExpressions = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-expression'); + foreach ($namedExpressions as $definedNameElement) { + $definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name'); + $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); + $expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression'); + + $baseAddress = $this->convertToExcelAddressValue($baseAddress); + $expression = $this->convertToExcelFormulaValue($expression); + + $this->addDefinedName($baseAddress, $definedName, $expression); + } + } + + /** + * Assess scope and store the Defined Name. + */ + private function addDefinedName(string $baseAddress, string $definedName, string $value): void + { + [$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true); + $worksheet = $this->spreadsheet->getSheetByName($sheetReference); + // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet + if ($worksheet !== null) { + $this->spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value)); + } + } +} diff --git a/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php b/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php new file mode 100644 index 0000000000..cf0450f199 --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php @@ -0,0 +1,63 @@ +objWriter = $objWriter; + $this->spreadsheet = $spreadsheet; + } + + public function write(): void + { + $wrapperWritten = false; + $sheetCount = $this->spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $this->spreadsheet->getSheet($i); + $autofilter = $worksheet->getAutoFilter(); + if ($autofilter !== null && !empty($autofilter->getRange())) { + if ($wrapperWritten === false) { + $this->objWriter->startElement('table:database-ranges'); + $wrapperWritten = true; + } + $this->objWriter->startElement('table:database-range'); + $this->objWriter->writeAttribute('table:orientation', 'column'); + $this->objWriter->writeAttribute('table:display-filter-buttons', 'true'); + $this->objWriter->writeAttribute( + 'table:target-range-address', + $this->formatRange($worksheet, $autofilter) + ); + $this->objWriter->endElement(); + } + } + + if ($wrapperWritten === true) { + $this->objWriter->endElement(); + } + } + + protected function formatRange(Worksheet $worksheet, Autofilter $autofilter): string + { + $title = $worksheet->getTitle(); + $range = $autofilter->getRange(); + + return "'{$title}'.{$range}"; + } +} diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index e4bd17939a..a589e54923 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -101,6 +101,7 @@ public function write(): string $this->writeSheets($objWriter); + (new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write(); // Defined names (ranges and formulae) (new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write(); diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/AutoFilterTest.php new file mode 100644 index 0000000000..47c7ee6a09 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/AutoFilterTest.php @@ -0,0 +1,31 @@ +spreadsheet = $reader->load($filename); + } + + public function testAutoFilterRange(): void + { + $worksheet = $this->spreadsheet->getActiveSheet(); + + $autoFilterRange = $worksheet->getAutoFilter()->getRange(); + + self::assertSame('A1:C9', $autoFilterRange); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php new file mode 100644 index 0000000000..4368ef76e3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php @@ -0,0 +1,33 @@ +getActiveSheet(); + + $dataSet = [ + ['Year', 'Quarter', 'Sales'], + [2020, 'Q1', 100], + [2020, 'Q2', 120], + [2020, 'Q3', 140], + [2020, 'Q4', 160], + [2021, 'Q1', 180], + [2021, 'Q2', 75], + [2021, 'Q3', 0], + [2021, 'Q4', 0], + ]; + $worksheet->fromArray($dataSet, null, 'A1'); + $worksheet->getAutoFilter()->setRange('A1:C9'); + + $reloaded = $this->writeAndReload($spreadsheet, 'Ods'); + + self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange()); + } +} diff --git a/tests/data/Reader/Ods/AutoFilter.ods b/tests/data/Reader/Ods/AutoFilter.ods new file mode 100644 index 0000000000000000000000000000000000000000..a8f53b8105f466d30330e4533ff8ad113fba0675 GIT binary patch literal 10115 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 Date: Mon, 3 May 2021 08:39:42 +0200 Subject: [PATCH 04/28] Ods defined names unit tests (#2054) * Defined names/formulae in ODS are prefixed by $$ when used in a formula; so we need to strip this out to fully convert them to an Excel formula * Test for ODS Writer for DefinedNames --- phpstan-baseline.neon | 10 ----- src/PhpSpreadsheet/Reader/Ods.php | 8 ++-- src/PhpSpreadsheet/Reader/Ods/BaseReader.php | 4 +- .../Reader/Ods/DefinedNames.php | 1 + .../Reader/Ods/DefinedNamesTest.php | 35 ++++++++++++++++++ .../Writer/Ods/DefinedNamesTest.php | 33 +++++++++++++++++ tests/data/Reader/Ods/DefinedNames.ods | Bin 0 -> 3140 bytes 7 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Ods/DefinedNamesTest.php create mode 100644 tests/data/Reader/Ods/DefinedNames.ods diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 89f50e3b76..af995b7bcf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2810,16 +2810,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Ods.php - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 3 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#3 \\$formula of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:translateSeparator\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$officeNs has no typehint specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index d4163cf7e9..26151cdc23 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -783,11 +783,13 @@ private function convertToExcelFormulaValue(string $openOfficeFormula): string // Cell range reference in another sheet $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); // Cell reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value); + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); // Cell range reference - $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value); + $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); // Simple cell reference - $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value); + $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + // Convert references to defined names/formulae + $value = str_replace('$$', '', $value ?? ''); $value = Calculation::translateSeparator(';', ',', $value, $inBraces); } diff --git a/src/PhpSpreadsheet/Reader/Ods/BaseReader.php b/src/PhpSpreadsheet/Reader/Ods/BaseReader.php index 82d4171020..17e2d4d573 100644 --- a/src/PhpSpreadsheet/Reader/Ods/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/Ods/BaseReader.php @@ -62,8 +62,10 @@ protected function convertToExcelFormulaValue(string $openOfficeFormula): string $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); // Simple cell reference $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + // Convert references to defined names/formulae + $value = str_replace('$$', '', $value ?? ''); - $value = Calculation::translateSeparator(';', ',', $value ?? '', $inBraces); + $value = Calculation::translateSeparator(';', ',', $value, $inBraces); } } diff --git a/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php index 79f5c02754..6810a3c726 100644 --- a/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php +++ b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php @@ -44,6 +44,7 @@ protected function readDefinedExpressions(DOMElement $workbookData): void $expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression'); $baseAddress = $this->convertToExcelAddressValue($baseAddress); + $expression = substr($expression, strpos($expression, ':=') + 1); $expression = $this->convertToExcelFormulaValue($expression); $this->addDefinedName($baseAddress, $definedName, $expression); diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php new file mode 100644 index 0000000000..760421ceaf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php @@ -0,0 +1,35 @@ +spreadsheet = $reader->load($filename); + } + + public function testDefinedNames(): void + { + $worksheet = $this->spreadsheet->getActiveSheet(); + + $firstDefinedNameValue = $worksheet->getCell('First')->getValue(); + $secondDefinedNameValue = $worksheet->getCell('Second')->getValue(); + $calculatedFormulaValue = $worksheet->getCell('B2')->getCalculatedValue(); + + self::assertSame(3, $firstDefinedNameValue); + self::assertSame(4, $secondDefinedNameValue); + self::assertSame(12, $calculatedFormulaValue); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/DefinedNamesTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/DefinedNamesTest.php new file mode 100644 index 0000000000..1b2e30b28a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/DefinedNamesTest.php @@ -0,0 +1,33 @@ +getActiveSheet(); + + $dataSet = [ + [7, 'x', 5], + ['=', '=FORMULA'], + ]; + $worksheet->fromArray($dataSet, null, 'A1'); + + $spreadsheet->addDefinedName(new NamedRange('FIRST', $worksheet, '$A$1')); + $spreadsheet->addDefinedName(new NamedRange('SECOND', $worksheet, '$C$1')); + $spreadsheet->addDefinedName(new NamedFormula('FORMULA', $worksheet, '=FIRST*SECOND')); + + $reloaded = $this->writeAndReload($spreadsheet, 'Ods'); + + self::assertSame(7, $reloaded->getActiveSheet()->getCell('FIRST')->getValue()); + self::assertSame(5, $reloaded->getActiveSheet()->getCell('SECOND')->getValue()); + self::assertSame(35, $reloaded->getActiveSheet()->getCell('B2')->getCalculatedValue()); + } +} diff --git a/tests/data/Reader/Ods/DefinedNames.ods b/tests/data/Reader/Ods/DefinedNames.ods new file mode 100644 index 0000000000000000000000000000000000000000..4246435bad747543ec4d48887dde8f38cd3c6010 GIT binary patch literal 3140 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 MN~l8G3+n**7m@~ literal 0 HcmV?d00001 From 346bad1b1de6a47a1be19536f88947289ec7feca Mon Sep 17 00:00:00 2001 From: oleibman Date: Mon, 3 May 2021 09:31:01 -0700 Subject: [PATCH 05/28] Fix for Issue 2042 (SUM Partially Broken) (#2045) As issue #2042 documents, SUM behaves differently with invalid strings depending on whether they come from a cell or are used as literals in the formula. SUM is not alone in this regard; COUNTA is another function within this behavior, and the solution to this one is modeled on COUNTA. New tests are added for SUM, and the resulting tests are duplicated to confirm correct behavior for both cells and literals. Samples 16 (CSV), 17 (Html), and 21 (PDF) were adversely affected by this problem. 17 and 21 were immediately fixed, but 16 had another problem - Excel was not interpreting the UTF8 currency symbols correctly, even though the file was saved with a BOM. After some experimenting, it appears that the `sep=;` line generated by setExcelCompatibility(true) causes Excel to mis-handle the file. This seems like a bug - there is apparently no way to save a UTF-8 CSV with non-ASCII characters which specifies a non-standard separator which Excel will open correctly. I don't know if this is a recent change or if it is just the case that nobody noticed this problem till now. So, I changed Sample 16 to use setUseBom rather than setExcelCompatibility, which solved its problem. I then added new tests for setExcelCompatibility, with documentation of this problem. --- samples/Basic/16_Csv.php | 3 +- .../Calculation/MathTrig/Sum.php | 8 ++- .../Functions/MathTrig/SumTest.php | 18 +++++++ .../Writer/Csv/CsvExcelCompatibilityTest.php | 49 +++++++++++++++++++ tests/data/Calculation/MathTrig/SUM.php | 6 ++- .../data/Calculation/MathTrig/SUMLITERALS.php | 12 +++++ 6 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Csv/CsvExcelCompatibilityTest.php create mode 100644 tests/data/Calculation/MathTrig/SUMLITERALS.php diff --git a/samples/Basic/16_Csv.php b/samples/Basic/16_Csv.php index 15bbf0d456..137f6469c1 100644 --- a/samples/Basic/16_Csv.php +++ b/samples/Basic/16_Csv.php @@ -38,7 +38,8 @@ $filenameCSV = $helper->getFilename(__FILE__, 'csv'); /** @var \PhpOffice\PhpSpreadsheet\Writer\Csv $writerCSV */ $writerCSV = new CsvWriter($spreadsheetFromCSV); -$writerCSV->setExcelCompatibility(true); +//$writerCSV->setExcelCompatibility(true); +$writerCSV->setUseBom(true); // because of non-ASCII chars $callStartTime = microtime(true); $writerCSV->save($filenameCSV); diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php index ab3a9a07c1..8a3223b1c5 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php @@ -51,16 +51,20 @@ public static function funcSumNoStrings(...$args) { $returnValue = 0; // Loop through the arguments - foreach (Functions::flattenArray($args) as $arg) { + $aArgs = Functions::flattenArrayIndexed($args); + foreach ($aArgs as $k => $arg) { // Is it a numeric value? if (is_numeric($arg) || empty($arg)) { if (is_string($arg)) { $arg = (int) $arg; } $returnValue += $arg; + } elseif (is_bool($arg)) { + $returnValue += (int) $arg; } elseif (Functions::isError($arg)) { return $arg; - } else { + // ignore non-numerics from cell, but fail as literals (except null) + } elseif ($arg !== null && !Functions::isCellValue($k)) { return Functions::VALUE(); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php index a9ea7f2986..b85f0c9071 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -26,4 +26,22 @@ public function providerSUM(): array { return require 'tests/data/Calculation/MathTrig/SUM.php'; } + + /** + * @dataProvider providerSUMLiterals + * + * @param mixed $expectedResult + */ + public function testSUMLiterals($expectedResult, string $args): void + { + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue("=SUM($args)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerSUMLiterals(): array + { + return require 'tests/data/Calculation/MathTrig/SUMLITERALS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Csv/CsvExcelCompatibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Csv/CsvExcelCompatibilityTest.php new file mode 100644 index 0000000000..9b7d16aae2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Csv/CsvExcelCompatibilityTest.php @@ -0,0 +1,49 @@ +getActiveSheet(); + $sheet->setCellValue('A1', '1'); + $sheet->setCellValue('B1', '2'); + $sheet->setCellValue('C1', '3'); + $sheet->setCellValue('A2', '4'); + $sheet->setCellValue('B2', '5'); + $sheet->setCellValue('C2', '6'); + $writer = new CsvWriter($spreadsheet); + $writer->setExcelCompatibility(true); + self::assertSame('', $writer->getOutputEncoding()); + $filename = File::temporaryFilename(); + $writer->save($filename); + $reader = new CsvReader(); + $spreadsheet2 = $reader->load($filename); + $contents = file_get_contents($filename); + unlink($filename); + self::assertEquals(1, $spreadsheet2->getActiveSheet()->getCell('A1')->getValue()); + self::assertEquals(6, $spreadsheet2->getActiveSheet()->getCell('C2')->getValue()); + self::assertStringContainsString(CsvReader::UTF8_BOM, $contents); + self::assertStringContainsString("\r\n", $contents); + self::assertStringContainsString('sep=;', $contents); + self::assertStringContainsString('"1";"2";"3"', $contents); + self::assertStringContainsString('"4";"5";"6"', $contents); + } +} diff --git a/tests/data/Calculation/MathTrig/SUM.php b/tests/data/Calculation/MathTrig/SUM.php index a821907681..0c54613e2a 100644 --- a/tests/data/Calculation/MathTrig/SUM.php +++ b/tests/data/Calculation/MathTrig/SUM.php @@ -4,5 +4,9 @@ [50, 5, 15, 30], [52, 5, 15, 30, 2], [53.1, 5.7, 15, 30, 2.4], - ['#VALUE!', 5.7, 'X', 30, 2.4], // error here conflicts with SUMIF + [52.1, 5.7, '14', 30, 2.4], + [38.1, 5.7, 'X', 30, 2.4], // error if entered in formula, but not in cell + [38.1, 5.7, null, 30, 2.4], + [38.1, 5.7, false, 30, 2.4], + [39.1, 5.7, true, 30, 2.4], ]; diff --git a/tests/data/Calculation/MathTrig/SUMLITERALS.php b/tests/data/Calculation/MathTrig/SUMLITERALS.php new file mode 100644 index 0000000000..fd184ebd50 --- /dev/null +++ b/tests/data/Calculation/MathTrig/SUMLITERALS.php @@ -0,0 +1,12 @@ + Date: Mon, 3 May 2021 22:21:57 +0200 Subject: [PATCH 06/28] Fix row visibility in XLS Writer (#2058) * Fix reversed visibility in Xls Writer --- docs/references/features-cross-reference.md | 10 ++--- src/PhpSpreadsheet/Reader/Gnumeric.php | 4 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 10 ++++- .../Writer/Xls/RowVisibilityTest.php | 37 +++++++++++++++++++ 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 9dcf8d919e..05b8c117af 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -1220,13 +1220,13 @@ Merged Cells - - - - + ✔ ✔ - + ✔ + ✔ + N/A + N/A diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index d3cdf1b002..80ca46cb56 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -270,7 +270,9 @@ private function processComments(SimpleXMLElement $sheet): void $commentAttributes = $comment->attributes(); // Only comment objects are handled at the moment if ($commentAttributes->Text) { - $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound)->setAuthor((string) $commentAttributes->Author)->setText($this->parseRichText((string) $commentAttributes->Text)); + $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound) + ->setAuthor((string) $commentAttributes->Author) + ->setText($this->parseRichText((string) $commentAttributes->Text)); } } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 84844d3d80..c3d5d8f4c2 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -390,7 +390,13 @@ public function close(): void // Row dimensions foreach ($phpSheet->getRowDimensions() as $rowDimension) { $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs - $this->writeRow($rowDimension->getRowIndex() - 1, (int) $rowDimension->getRowHeight(), $xfIndex, $rowDimension->getVisible(), $rowDimension->getOutlineLevel()); + $this->writeRow( + $rowDimension->getRowIndex() - 1, + (int) $rowDimension->getRowHeight(), + $xfIndex, + !$rowDimension->getVisible(), + $rowDimension->getOutlineLevel() + ); } // Write Cells @@ -1181,7 +1187,7 @@ private function writeRow($row, $height, $xfIndex, $hidden = false, $level = 0): // collapsed. The zero height flag, 0x20, is used to collapse a row. $grbit |= $level; - if ($hidden) { + if ($hidden === true) { $grbit |= 0x0030; } if ($height !== null) { diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php new file mode 100644 index 0000000000..055ee1b981 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php @@ -0,0 +1,37 @@ +getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + $worksheet->setCellValue("A{$row}", $row); + $worksheet->getRowDimension($row)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getRowDimension($row)->getVisible()); + } + } + + public function dataProviderReoVisibility(): array + { + return [ + [ + [1 => true, 2 => false, 3 => false, 4 => true, 5 => true, 6 => false], + ], + ]; + } +} From 5873116488e07016500df9ece9c30975ab290a87 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 3 May 2021 23:46:40 +0200 Subject: [PATCH 07/28] Unit testing for row/column/worksheet visibility for Xls and Xlsx files (#2059) * Unit testing for row/column/worksheet visibility for Xls and Xlsx files * Include very hidden in worksheet visibility tests --- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 4 +- .../Writer/Xls/RowVisibilityTest.php | 37 ------- .../Writer/Xls/VisibilityTest.php | 99 +++++++++++++++++++ .../Writer/Xlsx/VisibilityTest.php | 99 +++++++++++++++++++ 4 files changed, 201 insertions(+), 38 deletions(-) delete mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index c3d5d8f4c2..894ce03acd 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2166,7 +2166,9 @@ private function writePassword(): void */ public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1): void { - $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage ? $this->processBitmapGd($bitmap) : $this->processBitmap($bitmap)); + $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage + ? $this->processBitmapGd($bitmap) + : $this->processBitmap($bitmap)); [$width, $height, $size, $data] = $bitmap_array; // Scale the frame of the image. diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php deleted file mode 100644 index 055ee1b981..0000000000 --- a/tests/PhpSpreadsheetTests/Writer/Xls/RowVisibilityTest.php +++ /dev/null @@ -1,37 +0,0 @@ -getActiveSheet(); - foreach ($visibleRows as $row => $visibility) { - $worksheet->setCellValue("A{$row}", $row); - $worksheet->getRowDimension($row)->setVisible($visibility); - } - - $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); - $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); - foreach ($visibleRows as $row => $visibility) { - self::assertSame($visibility, $reloadedWorksheet->getRowDimension($row)->getVisible()); - } - } - - public function dataProviderReoVisibility(): array - { - return [ - [ - [1 => true, 2 => false, 3 => false, 4 => true, 5 => true, 6 => false], - ], - ]; - } -} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php new file mode 100644 index 0000000000..7de3932820 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php @@ -0,0 +1,99 @@ +getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + $worksheet->setCellValue("A{$row}", $row); + $worksheet->getRowDimension($row)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getRowDimension($row)->getVisible()); + } + } + + public function dataProviderRowVisibility(): array + { + return [ + [ + [1 => true, 2 => false, 3 => false, 4 => true, 5 => true, 6 => false], + ], + ]; + } + + /** + * @dataProvider dataProviderColumnVisibility + */ + public function testColumnVisibility(array $visibleColumns): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + $worksheet->setCellValue("{$column}1", $column); + $worksheet->getColumnDimension($column)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getColumnDimension($column)->getVisible()); + } + } + + public function dataProviderColumnVisibility(): array + { + return [ + [ + ['A' => true, 'B' => false, 'C' => false, 'D' => true, 'E' => true, 'F' => false], + ], + ]; + } + + /** + * @dataProvider dataProviderSheetVisibility + */ + public function testSheetVisibility(array $visibleSheets): void + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + foreach ($visibleSheets as $sheetName => $visibility) { + $worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet, $sheetName)); + $worksheet->setCellValue('A1', $sheetName); + $worksheet->setSheetState($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + foreach ($visibleSheets as $sheetName => $visibility) { + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + self::assertSame($visibility, $reloadedWorksheet->getSheetState()); + } + } + + public function dataProviderSheetVisibility(): array + { + return [ + [ + [ + 'Worksheet 1' => Worksheet::SHEETSTATE_HIDDEN, + 'Worksheet 2' => Worksheet::SHEETSTATE_VERYHIDDEN, + 'Worksheet 3' => Worksheet::SHEETSTATE_VISIBLE, + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php new file mode 100644 index 0000000000..7e1ca967c0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php @@ -0,0 +1,99 @@ +getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + $worksheet->setCellValue("A{$row}", $row); + $worksheet->getRowDimension($row)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getRowDimension($row)->getVisible()); + } + } + + public function dataProviderRowVisibility(): array + { + return [ + [ + [1 => false, 2 => false, 3 => true, 4 => false, 5 => true, 6 => false], + ], + ]; + } + + /** + * @dataProvider dataProviderColumnVisibility + */ + public function testColumnVisibility(array $visibleColumns): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + $worksheet->setCellValue("{$column}1", $column); + $worksheet->getColumnDimension($column)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getColumnDimension($column)->getVisible()); + } + } + + public function dataProviderColumnVisibility(): array + { + return [ + [ + ['A' => false, 'B' => false, 'C' => true, 'D' => false, 'E' => true, 'F' => false], + ], + ]; + } + + /** + * @dataProvider dataProviderSheetVisibility + */ + public function testSheetVisibility(array $visibleSheets): void + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + foreach ($visibleSheets as $sheetName => $visibility) { + $worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet, $sheetName)); + $worksheet->setCellValue('A1', $sheetName); + $worksheet->setSheetState($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + foreach ($visibleSheets as $sheetName => $visibility) { + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + self::assertSame($visibility, $reloadedWorksheet->getSheetState()); + } + } + + public function dataProviderSheetVisibility(): array + { + return [ + [ + [ + 'Worksheet 1' => Worksheet::SHEETSTATE_HIDDEN, + 'Worksheet 2' => Worksheet::SHEETSTATE_VERYHIDDEN, + 'Worksheet 3' => Worksheet::SHEETSTATE_VISIBLE, + ], + ], + ]; + } +} From 4be93667228ce81c4f7083d7443490d9316bf2c2 Mon Sep 17 00:00:00 2001 From: oleibman Date: Tue, 4 May 2021 12:41:11 -0700 Subject: [PATCH 08/28] Gnumeric Better Namespace Handling (#2022) * Gnumeric Better Namespace Handling There have been a number of issues concerning the handling of legitimate but unexpected namespace prefixes in Xlsx spreadsheets created by software other than Excel and PhpSpreadsheet/PhpExcel.I have studied them, but, till now, have not had a good idea on how to act on them. A recent comment https://github.com/PHPOffice/PhpSpreadsheet/issues/860#issuecomment-824926224 in issue #860 by @IMSoP has triggered an idea about how to proceed. Although the issues exclusively concern Xlsx format, I am starting out by dealing with Gnumeric. It is simpler and smaller than Xlsx, and, more important, already has a test for an unexpected prefix, since, at some point, it changed its generic prefix from gmr to gnm. I added support and a test for that some time ago, but almost certainly not in the best possible manner. The code as changed for this PR seems simpler and less kludgey, both for that exceptional case as well as for normal handling. My hope is that this change can be a template for similar Reader changes for Xml, Ods, and, especially, Xlsx. All grandfathered Phpstan issues with Gnumeric are fixed and eliminated from baseline as part of this change. * Namespace Handling using XMLReader Adopt a suggestion from @IMSoP affecting listWorkSheetInfo, which uses XMLReader rather than SimpleXML for its processing. * Update GnumericLoadTest.php PR #2024 was pushed last night, causing a Phpstan problem with this member. * Update Gnumeric.php Suggestions from Mark Baker - strict equality test, more descriptive variable names. --- phpstan-baseline.neon | 65 ------ src/PhpSpreadsheet/Reader/Gnumeric.php | 212 +++++++++++------- .../Reader/Gnumeric/PageSetup.php | 11 +- .../Reader/Gnumeric/Properties.php | 99 ++++---- .../Reader/Gnumeric/GnumericLoadTest.php | 3 +- 5 files changed, 187 insertions(+), 203 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index af995b7bcf..0a8369eed0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2545,71 +2545,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:\\$referenceHelper has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Parameter \\#1 \\$fp of function fread expects resource, resource\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Parameter \\#1 \\$fp of function fclose expects resource, resource\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:\\$mappings has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Offset 'No' does not exist on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Offset 'Unit' does not exist on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Offset 'DefaultSizePts' does not exist on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseBorderAttributes\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseBorderAttributes\\(\\) has parameter \\$borderAttributes with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseRichText\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseRichText\\(\\) has parameter \\$is with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseGnumericColour\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseGnumericColour\\(\\) has parameter \\$gnmColour with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Html\\:\\:\\$rowspan has no typehint specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 80ca46cb56..d735829319 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -25,7 +25,19 @@ class Gnumeric extends BaseReader { - private const UOM_CONVERSION_POINTS_TO_CENTIMETERS = 0.03527777778; + const NAMESPACE_GNM = 'http://www.gnumeric.org/v10.dtd'; // gmr in old sheets + + const NAMESPACE_XSI = 'http://www.w3.org/2001/XMLSchema-instance'; + + const NAMESPACE_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'; + + const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink'; + + const NAMESPACE_DC = 'http://purl.org/dc/elements/1.1/'; + + const NAMESPACE_META = 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'; + + const NAMESPACE_OOO = 'http://openoffice.org/2004/office'; /** * Shared Expressions. @@ -41,16 +53,9 @@ class Gnumeric extends BaseReader */ private $spreadsheet; + /** @var ReferenceHelper */ private $referenceHelper; - /** - * Namespace shared across all functions. - * It is 'gnm', except for really old sheets which use 'gmr'. - * - * @var string - */ - private $gnm = 'gnm'; - /** * Create a new Gnumeric. */ @@ -77,16 +82,20 @@ public function canRead($pFilename) if (function_exists('gzread')) { // Read signature data (first 3 bytes) $fh = fopen($pFilename, 'rb'); - $data = fread($fh, 2); - fclose($fh); + if ($fh !== false) { + $data = fread($fh, 2); + fclose($fh); + } } - return $data == chr(0x1F) . chr(0x8B); + return $data === chr(0x1F) . chr(0x8B); } - private static function matchXml(string $name, string $field): bool + private static function matchXml(XMLReader $xml, string $expectedLocalName): bool { - return 1 === preg_match("/^(gnm|gmr):$field$/", $name); + return $xml->namespaceURI === self::NAMESPACE_GNM + && $xml->localName === $expectedLocalName + && $xml->nodeType === XMLReader::ELEMENT; } /** @@ -106,10 +115,10 @@ public function listWorksheetNames($pFilename) $worksheetNames = []; while ($xml->read()) { - if (self::matchXml($xml->name, 'SheetName') && $xml->nodeType == XMLReader::ELEMENT) { + if (self::matchXml($xml, 'SheetName')) { $xml->read(); // Move onto the value node $worksheetNames[] = (string) $xml->value; - } elseif (self::matchXml($xml->name, 'Sheets')) { + } elseif (self::matchXml($xml, 'Sheets')) { // break out of the loop once we've got our sheet names rather than parse the entire file break; } @@ -135,7 +144,7 @@ public function listWorksheetInfo($pFilename) $worksheetInfo = []; while ($xml->read()) { - if (self::matchXml($xml->name, 'Sheet') && $xml->nodeType == XMLReader::ELEMENT) { + if (self::matchXml($xml, 'Sheet')) { $tmpInfo = [ 'worksheetName' => '', 'lastColumnLetter' => 'A', @@ -145,20 +154,18 @@ public function listWorksheetInfo($pFilename) ]; while ($xml->read()) { - if ($xml->nodeType == XMLReader::ELEMENT) { - if (self::matchXml($xml->name, 'Name')) { - $xml->read(); // Move onto the value node - $tmpInfo['worksheetName'] = (string) $xml->value; - } elseif (self::matchXml($xml->name, 'MaxCol')) { - $xml->read(); // Move onto the value node - $tmpInfo['lastColumnIndex'] = (int) $xml->value; - $tmpInfo['totalColumns'] = (int) $xml->value + 1; - } elseif (self::matchXml($xml->name, 'MaxRow')) { - $xml->read(); // Move onto the value node - $tmpInfo['totalRows'] = (int) $xml->value + 1; - - break; - } + if (self::matchXml($xml, 'Name')) { + $xml->read(); // Move onto the value node + $tmpInfo['worksheetName'] = (string) $xml->value; + } elseif (self::matchXml($xml, 'MaxCol')) { + $xml->read(); // Move onto the value node + $tmpInfo['lastColumnIndex'] = (int) $xml->value; + $tmpInfo['totalColumns'] = (int) $xml->value + 1; + } elseif (self::matchXml($xml, 'MaxRow')) { + $xml->read(); // Move onto the value node + $tmpInfo['totalRows'] = (int) $xml->value + 1; + + break; } } $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); @@ -188,6 +195,7 @@ private function gzfileGetContents($filename) return $data; } + /** @var array */ private static $mappings = [ 'borderStyle' => [ '0' => Border::BORDER_NONE, @@ -266,7 +274,7 @@ public static function gnumericMappings(): array private function processComments(SimpleXMLElement $sheet): void { if ((!$this->readDataOnly) && (isset($sheet->Objects))) { - foreach ($sheet->Objects->children($this->gnm, true) as $key => $comment) { + foreach ($sheet->Objects->children(self::NAMESPACE_GNM) as $key => $comment) { $commentAttributes = $comment->attributes(); // Only comment objects are handled at the moment if ($commentAttributes->Text) { @@ -278,6 +286,14 @@ private function processComments(SimpleXMLElement $sheet): void } } + /** + * @param mixed $value + */ + private static function testSimpleXml($value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement(''); + } + /** * Loads Spreadsheet from file. * @@ -306,12 +322,10 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S $gFileData = $this->gzfileGetContents($pFilename); $xml2 = simplexml_load_string($this->securityScanner->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); - $xml = ($xml2 !== false) ? $xml2 : new SimpleXMLElement(''); - $namespacesMeta = $xml->getNamespaces(true); - $this->gnm = array_key_exists('gmr', $namespacesMeta) ? 'gmr' : 'gnm'; + $xml = self::testSimpleXml($xml2); - $gnmXML = $xml->children($namespacesMeta[$this->gnm]); - (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML, $namespacesMeta); + $gnmXML = $xml->children(self::NAMESPACE_GNM); + (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML); $worksheetID = 0; foreach ($gnmXML->Sheets->Sheet as $sheet) { @@ -331,7 +345,7 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); if (!$this->readDataOnly) { - (new PageSetup($this->spreadsheet, $this->gnm)) + (new PageSetup($this->spreadsheet)) ->printInformation($sheet) ->sheetMargins($sheet); } @@ -384,7 +398,7 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S if (array_key_exists($vtype, self::$mappings['dataType'])) { $type = self::$mappings['dataType'][$vtype]; } - if ($vtype == '20') { // Boolean + if ($vtype === '20') { // Boolean $cell = $cell == 'TRUE'; } } @@ -512,84 +526,122 @@ private function processMergedCells(SimpleXMLElement $sheet): void } } - private function processColumnLoop(int $c, int $maxCol, SimpleXMLElement $columnOverride, float $defaultWidth): int + private function setColumnWidth(int $whichColumn, float $defaultWidth): void + { + $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + if ($columnDimension !== null) { + $columnDimension->setWidth($defaultWidth); + } + } + + private function setColumnInvisible(int $whichColumn): void + { + $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + if ($columnDimension !== null) { + $columnDimension->setVisible(false); + } + } + + private function processColumnLoop(int $whichColumn, int $maxCol, SimpleXMLElement $columnOverride, float $defaultWidth): int { - $columnAttributes = $columnOverride->attributes(); + $columnAttributes = self::testSimpleXml($columnOverride->attributes()); $column = $columnAttributes['No']; $columnWidth = ((float) $columnAttributes['Unit']) / 5.4; $hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1'); $columnCount = (int) ($columnAttributes['Count'] ?? 1); - while ($c < $column) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); - ++$c; + while ($whichColumn < $column) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; } - while (($c < ($column + $columnCount)) && ($c <= $maxCol)) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($columnWidth); + while (($whichColumn < ($column + $columnCount)) && ($whichColumn <= $maxCol)) { + $this->setColumnWidth($whichColumn, $columnWidth); if ($hidden) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setVisible(false); + $this->setColumnInvisible($whichColumn); } - ++$c; + ++$whichColumn; } - return $c; + return $whichColumn; } private function processColumnWidths(SimpleXMLElement $sheet, int $maxCol): void { if ((!$this->readDataOnly) && (isset($sheet->Cols))) { // Column Widths + $defaultWidth = 0; $columnAttributes = $sheet->Cols->attributes(); - $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; - $c = 0; + if ($columnAttributes !== null) { + $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; + } + $whichColumn = 0; foreach ($sheet->Cols->ColInfo as $columnOverride) { - $c = $this->processColumnLoop($c, $maxCol, $columnOverride, $defaultWidth); + $whichColumn = $this->processColumnLoop($whichColumn, $maxCol, $columnOverride, $defaultWidth); } - while ($c <= $maxCol) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); - ++$c; + while ($whichColumn <= $maxCol) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; } } } - private function processRowLoop(int $r, int $maxRow, SimpleXMLElement $rowOverride, float $defaultHeight): int + private function setRowHeight(int $whichRow, float $defaultHeight): void { - $rowAttributes = $rowOverride->attributes(); + $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow); + if ($rowDimension !== null) { + $rowDimension->setRowHeight($defaultHeight); + } + } + + private function setRowInvisible(int $whichRow): void + { + $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow); + if ($rowDimension !== null) { + $rowDimension->setVisible(false); + } + } + + private function processRowLoop(int $whichRow, int $maxRow, SimpleXMLElement $rowOverride, float $defaultHeight): int + { + $rowAttributes = self::testSimpleXml($rowOverride->attributes()); $row = $rowAttributes['No']; $rowHeight = (float) $rowAttributes['Unit']; $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1'); $rowCount = (int) ($rowAttributes['Count'] ?? 1); - while ($r < $row) { - ++$r; - $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); + while ($whichRow < $row) { + ++$whichRow; + $this->setRowHeight($whichRow, $defaultHeight); } - while (($r < ($row + $rowCount)) && ($r < $maxRow)) { - ++$r; - $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($rowHeight); + while (($whichRow < ($row + $rowCount)) && ($whichRow < $maxRow)) { + ++$whichRow; + $this->setRowHeight($whichRow, $rowHeight); if ($hidden) { - $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setVisible(false); + $this->setRowInvisible($whichRow); } } - return $r; + return $whichRow; } private function processRowHeights(SimpleXMLElement $sheet, int $maxRow): void { if ((!$this->readDataOnly) && (isset($sheet->Rows))) { // Row Heights + $defaultHeight = 0; $rowAttributes = $sheet->Rows->attributes(); - $defaultHeight = (float) $rowAttributes['DefaultSizePts']; - $r = 0; + if ($rowAttributes !== null) { + $defaultHeight = (float) $rowAttributes['DefaultSizePts']; + } + $whichRow = 0; foreach ($sheet->Rows->RowInfo as $rowOverride) { - $r = $this->processRowLoop($r, $maxRow, $rowOverride, $defaultHeight); + $whichRow = $this->processRowLoop($whichRow, $maxRow, $rowOverride, $defaultHeight); } // never executed, I can't figure out any circumstances // under which it would be executed, and, even if // such exist, I'm not convinced this is needed. - //while ($r < $maxRow) { - // ++$r; - // $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); + //while ($whichRow < $maxRow) { + // ++$whichRow; + // $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow)->setRowHeight($defaultHeight); //} } } @@ -641,19 +693,21 @@ private static function addStyle2(array &$styleArray, string $key1, string $key, } } - private static function parseBorderAttributes($borderAttributes) + private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array { $styleArray = []; - if (isset($borderAttributes['Color'])) { - $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); - } + if ($borderAttributes !== null) { + if (isset($borderAttributes['Color'])) { + $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); + } - self::addStyle($styleArray, 'borderStyle', $borderAttributes['Style']); + self::addStyle($styleArray, 'borderStyle', $borderAttributes['Style']); + } return $styleArray; } - private function parseRichText($is) + private function parseRichText(string $is): RichText { $value = new RichText(); $value->createText($is); @@ -661,7 +715,7 @@ private function parseRichText($is) return $value; } - private static function parseGnumericColour($gnmColour) + private static function parseGnumericColour(string $gnmColour): string { [$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour); $gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2); @@ -679,7 +733,7 @@ private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes $shade = (string) $styleAttributes['Shade']; if (($RGB != '000000') || ($shade != '0')) { $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); - if ($shade == '1') { + if ($shade === '1') { $styleArray['fill']['startColor']['rgb'] = $RGB; $styleArray['fill']['endColor']['rgb'] = $RGB2; } else { diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php b/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php index 0fe7300593..accc27160f 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric; +use PhpOffice\PhpSpreadsheet\Reader\Gnumeric; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\PageMargins; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup as WorksheetPageSetup; @@ -14,15 +15,9 @@ class PageSetup */ private $spreadsheet; - /** - * @var string - */ - private $gnm; - - public function __construct(Spreadsheet $spreadsheet, string $gnm) + public function __construct(Spreadsheet $spreadsheet) { $this->spreadsheet = $spreadsheet; - $this->gnm = $gnm; } public function printInformation(SimpleXMLElement $sheet): self @@ -68,7 +63,7 @@ public function sheetMargins(SimpleXMLElement $sheet): self private function buildMarginSet(SimpleXMLElement $sheet, array $marginSet): array { - foreach ($sheet->PrintInformation->Margins->children($this->gnm, true) as $key => $margin) { + foreach ($sheet->PrintInformation->Margins->children(Gnumeric::NAMESPACE_GNM) as $key => $margin) { $marginAttributes = $margin->attributes(); $marginSize = ($marginAttributes['Points']) ?? 72; // Default is 72pt // Convert value in points to inches diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php b/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php index 16d9c2e082..c466a859e8 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric; +use PhpOffice\PhpSpreadsheet\Reader\Gnumeric; use PhpOffice\PhpSpreadsheet\Spreadsheet; use SimpleXMLElement; @@ -91,74 +92,72 @@ private function docPropertiesDC(SimpleXMLElement $officePropertyDC): void } } - private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta, array $namespacesMeta): void + private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta): void { $docProps = $this->spreadsheet->getProperties(); foreach ($officePropertyMeta as $propertyName => $propertyValue) { - if ($propertyValue === null) { - continue; + if ($propertyValue !== null) { + $attributes = $propertyValue->attributes(Gnumeric::NAMESPACE_META); + $propertyValue = trim((string) $propertyValue); + switch ($propertyName) { + case 'keyword': + $docProps->setKeywords($propertyValue); + + break; + case 'initial-creator': + $docProps->setCreator($propertyValue); + $docProps->setLastModifiedBy($propertyValue); + + break; + case 'creation-date': + $creationDate = strtotime($propertyValue); + $creationDate = $creationDate === false ? time() : $creationDate; + $docProps->setCreated($creationDate); + $docProps->setModified($creationDate); + + break; + case 'user-defined': + [, $attrName] = explode(':', $attributes['name']); + $this->userDefinedProperties($attrName, $propertyValue); + + break; + } } + } + } - $attributes = $propertyValue->attributes($namespacesMeta['meta']); - $propertyValue = trim((string) $propertyValue); - switch ($propertyName) { - case 'keyword': - $docProps->setKeywords($propertyValue); - - break; - case 'initial-creator': - $docProps->setCreator($propertyValue); - $docProps->setLastModifiedBy($propertyValue); - - break; - case 'creation-date': - $creationDate = strtotime($propertyValue); - $creationDate = $creationDate === false ? time() : $creationDate; - $docProps->setCreated($creationDate); - $docProps->setModified($creationDate); - - break; - case 'user-defined': - [, $attrName] = explode(':', $attributes['name']); - switch ($attrName) { - case 'publisher': - $docProps->setCompany($propertyValue); - - break; - case 'category': - $docProps->setCategory($propertyValue); + private function userDefinedProperties(string $attrName, string $propertyValue): void + { + $docProps = $this->spreadsheet->getProperties(); + switch ($attrName) { + case 'publisher': + $docProps->setCompany($propertyValue); - break; - case 'manager': - $docProps->setManager($propertyValue); + break; + case 'category': + $docProps->setCategory($propertyValue); - break; - } + break; + case 'manager': + $docProps->setManager($propertyValue); - break; - } + break; } } - public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML, array $namespacesMeta): void + public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML): void { - if (isset($namespacesMeta['office'])) { - $officeXML = $xml->children($namespacesMeta['office']); + $officeXML = $xml->children(Gnumeric::NAMESPACE_OFFICE); + if (!empty($officeXML)) { $officeDocXML = $officeXML->{'document-meta'}; $officeDocMetaXML = $officeDocXML->meta; foreach ($officeDocMetaXML as $officePropertyData) { - $officePropertyDC = []; - if (isset($namespacesMeta['dc'])) { - $officePropertyDC = $officePropertyData->children($namespacesMeta['dc']); - } + $officePropertyDC = $officePropertyData->children(Gnumeric::NAMESPACE_DC); $this->docPropertiesDC($officePropertyDC); - $officePropertyMeta = []; - if (isset($namespacesMeta['meta'])) { - $officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']); - } - $this->docPropertiesMeta($officePropertyMeta, $namespacesMeta); + $officePropertyMeta = $officePropertyData->children(Gnumeric::NAMESPACE_META); + $this->docPropertiesMeta($officePropertyMeta); } } elseif (isset($gnmXML->Summary)) { $this->docPropertiesOld($gnmXML); diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php index e24178e558..9544fc3a00 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php @@ -115,7 +115,8 @@ public function testLoad(): void self::assertEquals(Font::UNDERLINE_DOUBLE, $sheet->getCell('A24')->getStyle()->getFont()->getUnderline()); self::assertTrue($sheet->getCell('B23')->getStyle()->getFont()->getSubScript()); self::assertTrue($sheet->getCell('B24')->getStyle()->getFont()->getSuperScript()); - self::assertFalse($sheet->getRowDimension(30)->getVisible()); + $rowDimension = $sheet->getRowDimension(30); + self::assertFalse($rowDimension->getVisible()); } public function testLoadFilter(): void From 5ee4fbf090cb0aee04b172ff617da94d4455f7df Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 4 May 2021 22:32:12 +0200 Subject: [PATCH 09/28] Implement basic autofilter ranges with Gnumeric Reader (#2057) * Load basic autofilter ranges with Gnumeric Reader * Handle null values passed to row height/column with/merged cells/autofilters --- CHANGELOG.md | 1 + docs/references/features-cross-reference.md | 2 +- src/PhpSpreadsheet/Reader/Gnumeric.php | 31 +++++++++++++----- .../Reader/Gnumeric/AutoFilterTest.php | 31 ++++++++++++++++++ .../Reader/Gnumeric/Autofilter_Basic.gnumeric | Bin 0 -> 3070 bytes 5 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/AutoFilterTest.php create mode 100644 tests/data/Reader/Gnumeric/Autofilter_Basic.gnumeric diff --git a/CHANGELOG.md b/CHANGELOG.md index 7940a6a29b..4599f8772a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Implemented basic AutoFiltering for Ods Reader and Writer [PR #2053](https://github.com/PHPOffice/PhpSpreadsheet/pull/2053) +- Implemented basic AutoFiltering for Gnumeric Reader [PR #2055](https://github.com/PHPOffice/PhpSpreadsheet/pull/2055) - Improved support for Row and Column ranges in formulae [Issue #1755](https://github.com/PHPOffice/PhpSpreadsheet/issues/1755) [PR #2028](https://github.com/PHPOffice/PhpSpreadsheet/pull/2028) - Implemented URLENCODE() Web Function - Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions. diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 05b8c117af..c836a99a52 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -1314,7 +1314,7 @@ ● ● - + ● ● diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index d735829319..d66dbb88b6 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -482,6 +482,7 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S $this->processColumnWidths($sheet, $maxCol); $this->processRowHeights($sheet, $maxRow); $this->processMergedCells($sheet); + $this->processAutofilter($sheet); ++$worksheetID; } @@ -514,10 +515,10 @@ private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, str } } - private function processMergedCells(SimpleXMLElement $sheet): void + private function processMergedCells(?SimpleXMLElement $sheet): void { // Handle Merged Cells in this worksheet - if (isset($sheet->MergedRegions)) { + if ($sheet !== null && isset($sheet->MergedRegions)) { foreach ($sheet->MergedRegions->Merge as $mergeCells) { if (strpos($mergeCells, ':') !== false) { $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); @@ -526,6 +527,20 @@ private function processMergedCells(SimpleXMLElement $sheet): void } } + private function processAutofilter(?SimpleXMLElement $sheet): void + { + if ($sheet !== null && isset($sheet->Filters)) { + foreach ($sheet->Filters->Filter as $autofilter) { + if ($autofilter !== null) { + $attributes = $autofilter->attributes(); + if (isset($attributes['Area'])) { + $this->spreadsheet->getActiveSheet()->setAutoFilter((string) $attributes['Area']); + } + } + } + } + } + private function setColumnWidth(int $whichColumn, float $defaultWidth): void { $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); @@ -564,9 +579,9 @@ private function processColumnLoop(int $whichColumn, int $maxCol, SimpleXMLEleme return $whichColumn; } - private function processColumnWidths(SimpleXMLElement $sheet, int $maxCol): void + private function processColumnWidths(?SimpleXMLElement $sheet, int $maxCol): void { - if ((!$this->readDataOnly) && (isset($sheet->Cols))) { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Cols))) { // Column Widths $defaultWidth = 0; $columnAttributes = $sheet->Cols->attributes(); @@ -622,9 +637,9 @@ private function processRowLoop(int $whichRow, int $maxRow, SimpleXMLElement $ro return $whichRow; } - private function processRowHeights(SimpleXMLElement $sheet, int $maxRow): void + private function processRowHeights(?SimpleXMLElement $sheet, int $maxRow): void { - if ((!$this->readDataOnly) && (isset($sheet->Rows))) { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Rows))) { // Row Heights $defaultHeight = 0; $rowAttributes = $sheet->Rows->attributes(); @@ -646,10 +661,10 @@ private function processRowHeights(SimpleXMLElement $sheet, int $maxRow): void } } - private function processDefinedNames(SimpleXMLElement $gnmXML): void + private function processDefinedNames(?SimpleXMLElement $gnmXML): void { // Loop through definedNames (global named ranges) - if (isset($gnmXML->Names)) { + if ($gnmXML !== null && isset($gnmXML->Names)) { foreach ($gnmXML->Names->Name as $definedName) { $name = (string) $definedName->name; $value = (string) $definedName->value; diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/AutoFilterTest.php new file mode 100644 index 0000000000..18dde473a3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/AutoFilterTest.php @@ -0,0 +1,31 @@ +spreadsheet = $reader->load($filename); + } + + public function testAutoFilterRange(): void + { + $worksheet = $this->spreadsheet->getActiveSheet(); + + $autoFilterRange = $worksheet->getAutoFilter()->getRange(); + + self::assertSame('A1:D57', $autoFilterRange); + } +} diff --git a/tests/data/Reader/Gnumeric/Autofilter_Basic.gnumeric b/tests/data/Reader/Gnumeric/Autofilter_Basic.gnumeric new file mode 100644 index 0000000000000000000000000000000000000000..0009f438d088ae7f587a4379a1dcb2cc6de2fc54 GIT binary patch literal 3070 zcmVWiwFP!000001MQsObDKIA$KU%`u-=({+QbHglXx57aT_P`Y<{inrrW-F z2q?C0F!F-jB=@i1BY_FV5jjMixqZ0JOb3J{j=qwPK007N{`RzttRFN<8IO-!_I{^j z(Kz5Ci|5C!FVoY#!`5&841>sN7=fT+tpTTliwVktN|GAAANl<~3n(-?fEg2uy(JZXJ}0wF$E|e|_qd<3R9TSr zM9||a8gGj9%DH>WLF98fV)4Ukv)Vp7I%=z{o2+3_$LVdxL{-vKhSfBg{ z3UjEGT*?{x&>c?4vG+ROUdNr52o?azJ`tRq!dzJ%;Kq~mn+`@B@P zbq^LZ!Tf0NWq|mJpfQY*C}RHlF)v-smQUi!Xe$!7=4-Jsl`MocEnfT4n1Wx2$ptfD z82fh-9SLbcdsx%gMn1KH_MH~A$c z-BMm@If`KkS^AYavW`sVz}h=D2)?pzI24uh9j5F}LhrwT<2r6Fm)VTk%6`X%;L9c@ zB6=@g@^|*%E|JKo(X7ZqP06B+WAAt0EOXBR#js^Iv!Nnl=3KxZQ%gLrz^krk5`Yi) z=Rh1U$C(>$SIYg6SHS``IHWtNW>~zHVGa&Kf0h2A@r>QicpN>?Qt=$oRG)dtW7s++ z;A8c9!o-KZ)LvFAdd)3>!eJ`_2n>SjbE(TSSfu8KCDJS2j7L|~+2HeZbUVyk zIf|h=9;D^c!}0ifJedt9vy;)}=3?+|c=A$5W|GdyWb;4`vFhZIiDNpK`~o}uM8JZh zmQHmtmTRL5KZGjXRfl=%&Wu%cS?7a@&Ew4Q^TEaF>}qy4yc&)N7qFI=uH9zWK3z|z z*Ox8pYvQk_^hv1uO%~9Qhbcd0+4-1@oS@W2=xcQ(2_(*PT=3w5hHAkE41i>n3MfH0 zi!tC|0DGfT_}44`1CQbh7zpv!x-H9rfMBNr%xJ5r#Q=g?lDQ%qZu*Hw+1*FNk64C( zU%EaVmAbEa$J#F)D}k`o zxfu?6e|7P07k7U3E+(3}Tj}Dz=I<}sxupx+1Sz*})(O4$*O8d8Kj@9ZFMFT3rDb5V zrQ6A96X2)qd=nbcKksiso+0r86{{^hqF`1XTFYM58R**cc+dC&}xdP!G2W= z0yQ`tyVIb8SER5T6yH@r&MF$hgz-hAmbg1DIXJ>J#4%OE(JSFJC~mHVqgTSkG{iMk z!qqF`HYjecgsWBJu#0I3F;#--l^_j@n=3)|N_d!tcy&r(Cc0FerpE&qwN*rjFI6^6LXsX1anV&fPX-XXGl|cE4BU6ug zWacN1v`T!U(VVT94eHp{z&%7E(d&HK1gdHP6XPRnJzU~tuwSQ3r2~qnY;(A*p-YU1 zu=RM^KnFcp5(m`^W3;qu*b?I(Y&~2y&_U0Y1VvMJEm)#<3pHffsJxL5da5L-)xxgD zN>n>)xDw?b^kCUQ2R&1|sI|g2hDy7JDpCGHkChE{&=V!W>0peMb`4Xa{DU4U8|a|t zNe?$KYJsv*4e#|2HAwmICgpb}=;D^lT9Ry3!+Tvr4NIaV&=X`A*8pRLv}+g=P^cgb-Y`KT1;$I!+ZUK9ux60=qZulp3@jpqGK3$bo7*nmqAa8 zxXrXgu&ew4pRLQ#{R9tmo9=@~z~_6$Rjo}M1D zDtLN!B)B#hv!iDiiS+dBh*rV$%}39e9X-Q9q^D;`v8YgeT5fZQnr7qs=hvR;Xm`Wu%>*KeN19tSAJ%D$|6CN>Tg=JR7-wbN0#ZGcn8&A z$W{twx8%_eD0az18f8{@OyN?|NxC_3!cuvfP_*?e#zW MA2gjpLg7*X076OytN;K2 literal 0 HcmV?d00001 From 115e39ae0cc2b2bafe559635456375ae6db81b6b Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 7 May 2021 11:20:38 +0200 Subject: [PATCH 10/28] Issue 2066, highlighting more validation needed for LookupRef Functions (#2069) * Issue 2066, highlighting more validation needed for LookupRef Functions * Additional test cases --- phpstan-baseline.neon | 5 -- .../LookupRef/LookupRefValidations.php | 39 ++++++++++++ .../Calculation/LookupRef/Matrix.php | 24 ++++--- tests/data/Calculation/LookupRef/INDEX.php | 63 +++++++++++++++---- 4 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0a8369eed0..037ee99749 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -975,11 +975,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php - - - message: "#^Parameter \\#3 \\$rowNum of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Matrix\\:\\:extractRowValue\\(\\) expects int, float\\|int\\<0, max\\>\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Matrix\\:\\:extractRowValue\\(\\) has no return typehint specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php new file mode 100644 index 0000000000..b0739eb300 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php @@ -0,0 +1,39 @@ +getMessage(); } if (!is_array($matrix) || ($rowNum > count($matrix))) { @@ -69,12 +75,12 @@ public static function index($matrix, $rowNum = 0, $columnNum = 0) return Functions::REF(); } - if ($columnNum == 0) { + if ($columnNum === 0) { return self::extractRowValue($matrix, $rowKeys, $rowNum); } $columnNum = $columnKeys[--$columnNum]; - if ($rowNum == 0) { + if ($rowNum === 0) { return array_map( function ($value) { return [$value]; @@ -89,7 +95,7 @@ function ($value) { private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum) { - if ($rowNum == 0) { + if ($rowNum === 0) { return $matrix; } diff --git a/tests/data/Calculation/LookupRef/INDEX.php b/tests/data/Calculation/LookupRef/INDEX.php index 157794ab18..a699534e58 100644 --- a/tests/data/Calculation/LookupRef/INDEX.php +++ b/tests/data/Calculation/LookupRef/INDEX.php @@ -6,7 +6,7 @@ // Input [20 => ['R' => 1]], ], - [ + 'Negative Row' => [ '#VALUE!', // Expected // Input [ @@ -15,7 +15,7 @@ ], -1, ], - [ + 'Row > matrix rows' => [ '#REF!', // Expected // Input [ @@ -24,7 +24,25 @@ ], 10, ], - [ + 'Row is not a number' => [ + '#VALUE!', // Expected + // Input + [ + 20 => ['R' => 1], + 21 => ['R' => 2], + ], + 'NaN', + ], + 'Row is Error' => [ + '#N/A', // Expected + // Input + [ + 20 => ['R' => 1], + 21 => ['R' => 2], + ], + '#N/A', + ], + 'Return row 2' => [ [21 => ['R' => 2]], // Expected // Input [ @@ -33,7 +51,7 @@ ], 2, ], - [ + 'Return row 2 from larger matrix' => [ [21 => ['R' => 2, 'S' => 4]], // Expected // Input [ @@ -43,17 +61,17 @@ 2, 0, ], - [ + 'Negative Column' => [ '#VALUE!', // Expected // Input [ '20' => ['R' => 1, 'S' => 3], '21' => ['R' => 2, 'S' => 4], ], - 2, + 0, -1, ], - [ + 'Column > matrix columns' => [ '#REF!', // Expected // Input [ @@ -63,15 +81,25 @@ 2, 10, ], - [ - '#REF!', // Expected + 'Column is not a number' => [ + '#VALUE!', // Expected // Input [ - '20' => ['R' => 1, 'S' => 3], - '21' => ['R' => 2, 'S' => 4], + 20 => ['R' => 1], + 21 => ['R' => 2], ], - 10, - 2, + 1, + 'NaN', + ], + 'Column is Error' => [ + '#N/A', // Expected + // Input + [ + 20 => ['R' => 1], + 21 => ['R' => 2], + ], + 1, + '#N/A', ], [ 4, // Expected @@ -115,6 +143,15 @@ 2, 1, ], + [ + [1 => ['Bananas', 'Pears']], + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 0, + ], [ 3, [ From 72a36a5bb8d0ba6a807a7ff48f354ada1806dbe9 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 7 May 2021 12:53:59 +0200 Subject: [PATCH 11/28] Resolve issue with conditional font size set to zero in PHP8 (#2073) * Let's see if the tests now pass against PHP8; output file looks to be good * Font can't be both superscript and subscript at the same time, so we use if/else rather than if/if --- src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 12 +++++----- .../Reader/Xlsx/DefaultFillTest.php | 2 +- .../Reader/Xlsx/DefaultFontTest.php | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFontTest.php diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 2968a3feaf..80c320655b 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -42,9 +42,12 @@ public function setStyleBaseData(?Theme $theme = null, $styles = [], $cellStyles public static function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void { - $fontStyle->setName((string) $fontStyleXml->name['val']); - $fontStyle->setSize((float) $fontStyleXml->sz['val']); - + if (isset($fontStyleXml->name, $fontStyleXml->name['val'])) { + $fontStyle->setName((string) $fontStyleXml->name['val']); + } + if (isset($fontStyleXml->sz, $fontStyleXml->sz['val'])) { + $fontStyle->setSize((float) $fontStyleXml->sz['val']); + } if (isset($fontStyleXml->b)) { $fontStyle->setBold(!isset($fontStyleXml->b['val']) || self::boolean((string) $fontStyleXml->b['val'])); } @@ -68,8 +71,7 @@ public static function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyl $verticalAlign = strtolower((string) $fontStyleXml->vertAlign['val']); if ($verticalAlign === 'superscript') { $fontStyle->setSuperscript(true); - } - if ($verticalAlign === 'subscript') { + } elseif ($verticalAlign === 'subscript') { $fontStyle->setSubscript(true); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php index ccdad06795..dc61b95382 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php @@ -1,6 +1,6 @@ load($filename); + + $style = $spreadsheet->getActiveSheet()->getConditionalStyles('A1')[0]->getStyle(); + self::assertSame('9C0006', $style->getFont()->getColor()->getRGB()); + self::assertNull($style->getFont()->getName()); + self::assertNull($style->getFont()->getSize()); + } +} From 76ac0089110e959e810e4611490d42c62580892c Mon Sep 17 00:00:00 2001 From: Nathan Dench Date: Tue, 4 May 2021 14:45:04 +1000 Subject: [PATCH 12/28] R1C1 conversion should handle absolute A1 references --- src/PhpSpreadsheet/Cell/AddressHelper.php | 19 ++++++++++++++++--- .../data/Cell/A1ConversionToR1C1Relative.php | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index b0e34e253c..e5f4e952f7 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -102,14 +102,27 @@ public static function convertToR1C1( ?int $currentRowNumber = null, ?int $currentColumnNumber = null ): string { - $validityCheck = preg_match('/^\$?([A-Z]{1,3})\$?(\d{1,7})$/i', $address, $cellReference); + $validityCheck = preg_match('/^(\$?[A-Z]{1,3})(\$?\d{1,7})$/i', $address, $cellReference); if ($validityCheck === 0) { throw new Exception('Invalid A1-format Cell Reference'); } - $columnId = Coordinate::columnIndexFromString($cellReference[1]); - $rowId = (int) $cellReference[2]; + if ($cellReference[1][0] === '$') { + $columnId = Coordinate::columnIndexFromString(substr($cellReference[1], 1)); + // Column must be absolute address + $currentColumnNumber = null; + } else { + $columnId = Coordinate::columnIndexFromString($cellReference[1]); + } + + if ($cellReference[2][0] === '$') { + $rowId = (int) substr($cellReference[2], 1); + // Row must be absolute address + $currentRowNumber = null; + } else { + $rowId = (int) $cellReference[2]; + } if ($currentRowNumber !== null) { if ($rowId === $currentRowNumber) { diff --git a/tests/data/Cell/A1ConversionToR1C1Relative.php b/tests/data/Cell/A1ConversionToR1C1Relative.php index 76a6aee879..dd9b23918f 100644 --- a/tests/data/Cell/A1ConversionToR1C1Relative.php +++ b/tests/data/Cell/A1ConversionToR1C1Relative.php @@ -2,18 +2,33 @@ return [ ['R[2]C[2]', 'O18', 16, 13], + ['R18C15', '$O$18', 16, 13], ['R[-2]C[2]', 'O14', 16, 13], + ['R[-2]C15', '$O14', 16, 13], ['R[2]C[-2]', 'K18', 16, 13], + ['R18C[-2]', 'K$18', 16, 13], ['R[-2]C[-2]', 'K14', 16, 13], ['RC[3]', 'P16', 16, 13], + ['R16C[3]', 'P$16', 16, 13], ['RC[-3]', 'J16', 16, 13], + ['RC10', '$J16', 16, 13], ['R[4]C', 'M20', 16, 13], + ['R[4]C13', '$M20', 16, 13], ['R[-4]C', 'M12', 16, 13], + ['R12C', 'M$12', 16, 13], ['RC', 'E5', 5, 5], + ['R5C5', '$E$5', 5, 5], ['R5C', 'E5', null, 5], + ['R5C5', '$E5', null, 5], + ['R5C', 'E$5', null, 5], ['RC5', 'E5', 5, null], + ['RC5', '$E5', 5, null], + ['R5C5', 'E$5', 5, null], ['R5C[2]', 'E5', null, 3], + ['R5C5', '$E5', null, 3], + ['R5C[2]', 'E$5', null, 3], ['R[2]C5', 'E5', 3, null], + ['R5C5', '$E$5', 3, null], ['R5C[-2]', 'E5', null, 7], ['R[-2]C5', 'E5', 7, null], ]; From a96109d89b2474c4c465c70a843ea04989715119 Mon Sep 17 00:00:00 2001 From: Nathan Dench Date: Tue, 4 May 2021 15:21:44 +1000 Subject: [PATCH 13/28] Update CHANGELOG.md with R1C1 conversion change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4599f8772a..0c1a549c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Nothing. ### Fixed +- Correctly handle absolute A1 references when converting to R1C1 format [PR #2060](https://github.com/PHPOffice/PhpSpreadsheet/pull/2060) - Correct default fill style for conditional without a pattern defined [Issue #2035](https://github.com/PHPOffice/PhpSpreadsheet/issues/2035) [PR #2050](https://github.com/PHPOffice/PhpSpreadsheet/pull/2050) - Fixed issue where array key check for existince before accessing arrays in Xlsx.php. [PR #1970](https://github.com/PHPOffice/PhpSpreadsheet/pull/1970) - Fixed issue with quoted strings in number format mask rendered with toFormattedString() [Issue 1972#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) [PR #1978](https://github.com/PHPOffice/PhpSpreadsheet/pull/1978) From df01db58ad09e0f4912b5e53537457a88d43eb7c Mon Sep 17 00:00:00 2001 From: Nathan Dench Date: Thu, 6 May 2021 09:57:22 +1000 Subject: [PATCH 14/28] Remove complexity from AddressHelper::convertToR1C1 --- src/PhpSpreadsheet/Cell/AddressHelper.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index e5f4e952f7..91f85bed6f 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -102,26 +102,22 @@ public static function convertToR1C1( ?int $currentRowNumber = null, ?int $currentColumnNumber = null ): string { - $validityCheck = preg_match('/^(\$?[A-Z]{1,3})(\$?\d{1,7})$/i', $address, $cellReference); + $validityCheck = preg_match('/^(\$?)([A-Z]{1,3})(\$?)(\d{1,7})$/i', $address, $cellReference); if ($validityCheck === 0) { throw new Exception('Invalid A1-format Cell Reference'); } - if ($cellReference[1][0] === '$') { - $columnId = Coordinate::columnIndexFromString(substr($cellReference[1], 1)); + $columnId = Coordinate::columnIndexFromString($cellReference[2]); + if ($cellReference[1] === '$') { // Column must be absolute address $currentColumnNumber = null; - } else { - $columnId = Coordinate::columnIndexFromString($cellReference[1]); } - if ($cellReference[2][0] === '$') { - $rowId = (int) substr($cellReference[2], 1); + $rowId = (int) $cellReference[4]; + if ($cellReference[3] === '$') { // Row must be absolute address $currentRowNumber = null; - } else { - $rowId = (int) $cellReference[2]; } if ($currentRowNumber !== null) { From f28eea7341b5569354e4433fd10eda83f3945755 Mon Sep 17 00:00:00 2001 From: Nathan Dench Date: Fri, 7 May 2021 13:00:43 +1000 Subject: [PATCH 15/28] Use named regex groups and constants for regex strings --- src/PhpSpreadsheet/Cell/AddressHelper.php | 10 +++++----- src/PhpSpreadsheet/Cell/Coordinate.php | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index 91f85bed6f..632c046fdd 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -102,20 +102,20 @@ public static function convertToR1C1( ?int $currentRowNumber = null, ?int $currentColumnNumber = null ): string { - $validityCheck = preg_match('/^(\$?)([A-Z]{1,3})(\$?)(\d{1,7})$/i', $address, $cellReference); + $validityCheck = preg_match(Coordinate::A1_COORDINATE_REGEX, $address, $cellReference); if ($validityCheck === 0) { throw new Exception('Invalid A1-format Cell Reference'); } - $columnId = Coordinate::columnIndexFromString($cellReference[2]); - if ($cellReference[1] === '$') { + $columnId = Coordinate::columnIndexFromString($cellReference['col_ref']); + if ($cellReference['absolute_col'] === '$') { // Column must be absolute address $currentColumnNumber = null; } - $rowId = (int) $cellReference[4]; - if ($cellReference[3] === '$') { + $rowId = (int) $cellReference['row_ref']; + if ($cellReference['absolute_row'] === '$') { // Row must be absolute address $currentRowNumber = null; } diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 0b3917f265..58d2573e66 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -13,6 +13,8 @@ */ abstract class Coordinate { + public const A1_COORDINATE_REGEX = '/^(?\$?)(?[A-Z]{1,3})(?\$?)(?\d{1,7})$/i'; + /** * Default range variable constant. * @@ -29,8 +31,8 @@ abstract class Coordinate */ public static function coordinateFromString($pCoordinateString) { - if (preg_match('/^([$]?[A-Z]{1,3})([$]?\\d{1,7})$/', $pCoordinateString, $matches)) { - return [$matches[1], $matches[2]]; + if (preg_match(self::A1_COORDINATE_REGEX, $pCoordinateString, $matches)) { + return [$matches['absolute_col'] . $matches['col_ref'], $matches['absolute_row'] . $matches['row_ref']]; } elseif (self::coordinateIsRange($pCoordinateString)) { throw new Exception('Cell coordinate string can not be a range of cells'); } elseif ($pCoordinateString == '') { From d2e6db71fa3605b56dd7e6162a6f3a101d9b71b1 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 7 May 2021 23:40:30 +0200 Subject: [PATCH 16/28] Lookup functions additional unit tests (#2074) * Additional unit tests for VLOOKUP() and HLOOKUP() * Additional unit tests for CHOOSE() * Unit tests for HYPERLINK() function * Fix CHOOSE() test for spillage --- .../Calculation/LookupRef/Hyperlink.php | 2 +- src/PhpSpreadsheet/Reader/Xml.php | 2 +- .../Functions/LookupRef/HyperlinkTest.php | 51 +++++++++++++++++++ tests/data/Calculation/LookupRef/CHOOSE.php | 8 +++ tests/data/Calculation/LookupRef/HLOOKUP.php | 24 +++++++++ .../data/Calculation/LookupRef/HYPERLINK.php | 26 ++++++++++ tests/data/Calculation/LookupRef/VLOOKUP.php | 31 +++++++++++ 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HyperlinkTest.php create mode 100644 tests/data/Calculation/LookupRef/HYPERLINK.php diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php b/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php index d0324964b1..823d70c60c 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php @@ -11,7 +11,7 @@ class Hyperlink * HYPERLINK. * * Excel Function: - * =HYPERLINK(linkURL,displayName) + * =HYPERLINK(linkURL, [displayName]) * * @param mixed $linkURL Expect string. Value to check, is also the value returned when no error * @param mixed $displayName Expect string. Value to return when testValue is an error condition diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 282cd52892..4ef4efe7b5 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -284,7 +284,7 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) $worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); if ( - (isset($this->loadSheetsOnly)) && (isset($worksheet_ss['Name'])) && + isset($this->loadSheetsOnly, $worksheet_ss['Name']) && (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly)) ) { continue; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HyperlinkTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HyperlinkTest.php new file mode 100644 index 0000000000..e71992ed91 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HyperlinkTest.php @@ -0,0 +1,51 @@ +getMockBuilder(Cell::class) + ->onlyMethods(['getHyperlink']) + ->disableOriginalConstructor() + ->getMock(); + $cell->method('getHyperlink') + ->willReturn($hyperlink); + + $result = LookupRef::HYPERLINK($linkUrl, $description, $cell); + if (!is_array($expectedResult)) { + self::assertSame($expectedResult, $result); + } else { + self::assertSame($expectedResult[1], $result); + self::assertSame($expectedResult[0], $hyperlink->getUrl()); + self::assertSame($expectedResult[1], $hyperlink->getTooltip()); + } + } + + public function providerHYPERLINK(): array + { + return require 'tests/data/Calculation/LookupRef/HYPERLINK.php'; + } + + public function testHYPERLINKwithoutCell(): void + { + $result = LookupRef::HYPERLINK('https://phpspreadsheet.readthedocs.io/en/latest/', 'Read the Docs'); + self::assertSame(Functions::REF(), $result); + } +} diff --git a/tests/data/Calculation/LookupRef/CHOOSE.php b/tests/data/Calculation/LookupRef/CHOOSE.php index 06371c79eb..96c2978037 100644 --- a/tests/data/Calculation/LookupRef/CHOOSE.php +++ b/tests/data/Calculation/LookupRef/CHOOSE.php @@ -25,4 +25,12 @@ '#VALUE!', 0, 'red', 'blue', 'green', 'brown', ], + [ + '#VALUE!', + 'NaN', 'red', 'blue', 'green', 'brown', + ], + [ + ['blue', 'purple'], + 3, ['red', 'orange'], ['yellow', 'green'], ['blue', 'purple'], + ], ]; diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index d2a8a44682..61cb7e0642 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -328,4 +328,28 @@ 2, false, ], + [ + 0.61, + 'Ed', + [ + [null, 'Ann', 'Cara', 'Colin', 'Ed', 'Frank'], + ['Math', 0.58, 0.90, 0.67, 0.76, 0.80], + ['French', 0.61, 0.71, 0.59, 0.59, 0.76], + ['Physics', 0.75, 0.45, 0.39, 0.52, 0.69], + ['Bioogy', 0.39, 0.55, 0.77, 0.61, 0.45], + ], + 5, + false, + ], + [ + 'Normal Weight', + 23.5, + [ + [null, 'Min', 0.0, 18.5, 25.0, 30.0], + ['BMI', 'Max', 18.4, 24.9, 29.9, null], + [null, 'Body Type', 'Underweight', 'Normal Weight', 'Overweight', 'Obese'], + ], + 3, + true, + ], ]; diff --git a/tests/data/Calculation/LookupRef/HYPERLINK.php b/tests/data/Calculation/LookupRef/HYPERLINK.php new file mode 100644 index 0000000000..9a5e4c2e42 --- /dev/null +++ b/tests/data/Calculation/LookupRef/HYPERLINK.php @@ -0,0 +1,26 @@ + Date: Sat, 8 May 2021 19:36:35 +0200 Subject: [PATCH 17/28] Refactor Gnumeric Style Reader into a separate dedicated class --- src/PhpSpreadsheet/Reader/Gnumeric.php | 253 ++--------------- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 256 ++++++++++++++++++ 2 files changed, 274 insertions(+), 235 deletions(-) create mode 100644 src/PhpSpreadsheet/Reader/Gnumeric/Styles.php diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index d66dbb88b6..049e1da143 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\PageSetup; use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Properties; +use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Styles; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -56,6 +57,20 @@ class Gnumeric extends BaseReader /** @var ReferenceHelper */ private $referenceHelper; + /** @var array */ + public static $mappings = [ + 'dataType' => [ + '10' => DataType::TYPE_NULL, + '20' => DataType::TYPE_BOOL, + '30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel + '40' => DataType::TYPE_NUMERIC, // Float + '50' => DataType::TYPE_ERROR, + '60' => DataType::TYPE_STRING, + //'70': // Cell Range + //'80': // Array + ], + ]; + /** * Create a new Gnumeric. */ @@ -195,80 +210,9 @@ private function gzfileGetContents($filename) return $data; } - /** @var array */ - private static $mappings = [ - 'borderStyle' => [ - '0' => Border::BORDER_NONE, - '1' => Border::BORDER_THIN, - '2' => Border::BORDER_MEDIUM, - '3' => Border::BORDER_SLANTDASHDOT, - '4' => Border::BORDER_DASHED, - '5' => Border::BORDER_THICK, - '6' => Border::BORDER_DOUBLE, - '7' => Border::BORDER_DOTTED, - '8' => Border::BORDER_MEDIUMDASHED, - '9' => Border::BORDER_DASHDOT, - '10' => Border::BORDER_MEDIUMDASHDOT, - '11' => Border::BORDER_DASHDOTDOT, - '12' => Border::BORDER_MEDIUMDASHDOTDOT, - '13' => Border::BORDER_MEDIUMDASHDOTDOT, - ], - 'dataType' => [ - '10' => DataType::TYPE_NULL, - '20' => DataType::TYPE_BOOL, - '30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel - '40' => DataType::TYPE_NUMERIC, // Float - '50' => DataType::TYPE_ERROR, - '60' => DataType::TYPE_STRING, - //'70': // Cell Range - //'80': // Array - ], - 'fillType' => [ - '1' => Fill::FILL_SOLID, - '2' => Fill::FILL_PATTERN_DARKGRAY, - '3' => Fill::FILL_PATTERN_MEDIUMGRAY, - '4' => Fill::FILL_PATTERN_LIGHTGRAY, - '5' => Fill::FILL_PATTERN_GRAY125, - '6' => Fill::FILL_PATTERN_GRAY0625, - '7' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe - '8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe - '9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe - '10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe - '11' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch - '12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch - '13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL, - '14' => Fill::FILL_PATTERN_LIGHTVERTICAL, - '15' => Fill::FILL_PATTERN_LIGHTUP, - '16' => Fill::FILL_PATTERN_LIGHTDOWN, - '17' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch - '18' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch - ], - 'horizontal' => [ - '1' => Alignment::HORIZONTAL_GENERAL, - '2' => Alignment::HORIZONTAL_LEFT, - '4' => Alignment::HORIZONTAL_RIGHT, - '8' => Alignment::HORIZONTAL_CENTER, - '16' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, - '32' => Alignment::HORIZONTAL_JUSTIFY, - '64' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, - ], - 'underline' => [ - '1' => Font::UNDERLINE_SINGLE, - '2' => Font::UNDERLINE_DOUBLE, - '3' => Font::UNDERLINE_SINGLEACCOUNTING, - '4' => Font::UNDERLINE_DOUBLEACCOUNTING, - ], - 'vertical' => [ - '1' => Alignment::VERTICAL_TOP, - '2' => Alignment::VERTICAL_BOTTOM, - '4' => Alignment::VERTICAL_CENTER, - '8' => Alignment::VERTICAL_JUSTIFY, - ], - ]; - public static function gnumericMappings(): array { - return self::$mappings; + return array_merge(self::$mappings, Styles::$mappings); } private function processComments(SimpleXMLElement $sheet): void @@ -405,80 +349,9 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); } - $this->processComments($sheet); - - foreach ($sheet->Styles->StyleRegion as $styleRegion) { - $styleAttributes = $styleRegion->attributes(); - if ( - ($styleAttributes['startRow'] <= $maxRow) && - ($styleAttributes['startCol'] <= $maxCol) - ) { - $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); - $startRow = $styleAttributes['startRow'] + 1; - - $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; - $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); - - $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); - $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; - - $styleAttributes = $styleRegion->Style->attributes(); - - $styleArray = []; - // We still set the number format mask for date/time values, even if readDataOnly is true - $formatCode = (string) $styleAttributes['Format']; - if (Date::isDateTimeFormatCode($formatCode)) { - $styleArray['numberFormat']['formatCode'] = $formatCode; - } - if (!$this->readDataOnly) { - // If readDataOnly is false, we set all formatting information - $styleArray['numberFormat']['formatCode'] = $formatCode; - - self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); - self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); - $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; - $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); - $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; - $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; - - $this->addColors($styleArray, $styleAttributes); - - $fontAttributes = $styleRegion->Style->Font->attributes(); - $styleArray['font']['name'] = (string) $styleRegion->Style->Font; - $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); - $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; - $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; - $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; - self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); - - switch ($fontAttributes['Script']) { - case '1': - $styleArray['font']['superscript'] = true; - - break; - case '-1': - $styleArray['font']['subscript'] = true; - - break; - } - - if (isset($styleRegion->Style->StyleBorder)) { - $srssb = $styleRegion->Style->StyleBorder; - $this->addBorderStyle($srssb, $styleArray, 'top'); - $this->addBorderStyle($srssb, $styleArray, 'bottom'); - $this->addBorderStyle($srssb, $styleArray, 'left'); - $this->addBorderStyle($srssb, $styleArray, 'right'); - $this->addBorderDiagonal($srssb, $styleArray); - } - if (isset($styleRegion->Style->HyperLink)) { - // TO DO - $hyperlink = $styleRegion->Style->HyperLink->attributes(); - } - } - $this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); - } - } + (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol); + $this->processComments($sheet); $this->processColumnWidths($sheet, $maxCol); $this->processRowHeights($sheet, $maxRow); $this->processMergedCells($sheet); @@ -493,28 +366,6 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S return $this->spreadsheet; } - private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void - { - if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; - } elseif (isset($srssb->Diagonal)) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; - } elseif (isset($srssb->{'Rev-Diagonal'})) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; - } - } - - private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void - { - $ucDirection = ucfirst($direction); - if (isset($srssb->$ucDirection)) { - $styleArray['borders'][$direction] = self::parseBorderAttributes($srssb->$ucDirection->attributes()); - } - } - private function processMergedCells(?SimpleXMLElement $sheet): void { // Handle Merged Cells in this worksheet @@ -683,45 +534,6 @@ private function processDefinedNames(?SimpleXMLElement $gnmXML): void } } - private function calcRotation(SimpleXMLElement $styleAttributes): int - { - $rotation = (int) $styleAttributes->Rotation; - if ($rotation >= 270 && $rotation <= 360) { - $rotation -= 360; - } - $rotation = (abs($rotation) > 90) ? 0 : $rotation; - - return $rotation; - } - - private static function addStyle(array &$styleArray, string $key, string $value): void - { - if (array_key_exists($value, self::$mappings[$key])) { - $styleArray[$key] = self::$mappings[$key][$value]; - } - } - - private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void - { - if (array_key_exists($value, self::$mappings[$key])) { - $styleArray[$key1][$key] = self::$mappings[$key][$value]; - } - } - - private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array - { - $styleArray = []; - if ($borderAttributes !== null) { - if (isset($borderAttributes['Color'])) { - $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); - } - - self::addStyle($styleArray, 'borderStyle', $borderAttributes['Style']); - } - - return $styleArray; - } - private function parseRichText(string $is): RichText { $value = new RichText(); @@ -729,33 +541,4 @@ private function parseRichText(string $is): RichText return $value; } - - private static function parseGnumericColour(string $gnmColour): string - { - [$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour); - $gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2); - $gnmG = substr(str_pad($gnmG, 4, '0', STR_PAD_RIGHT), 0, 2); - $gnmB = substr(str_pad($gnmB, 4, '0', STR_PAD_RIGHT), 0, 2); - - return $gnmR . $gnmG . $gnmB; - } - - private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void - { - $RGB = self::parseGnumericColour($styleAttributes['Fore']); - $styleArray['font']['color']['rgb'] = $RGB; - $RGB = self::parseGnumericColour($styleAttributes['Back']); - $shade = (string) $styleAttributes['Shade']; - if (($RGB != '000000') || ($shade != '0')) { - $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); - if ($shade === '1') { - $styleArray['fill']['startColor']['rgb'] = $RGB; - $styleArray['fill']['endColor']['rgb'] = $RGB2; - } else { - $styleArray['fill']['endColor']['rgb'] = $RGB; - $styleArray['fill']['startColor']['rgb'] = $RGB2; - } - self::addStyle2($styleArray, 'fill', 'fillType', $shade); - } - } } diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php new file mode 100644 index 0000000000..9c725b203b --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -0,0 +1,256 @@ + [ + '0' => Border::BORDER_NONE, + '1' => Border::BORDER_THIN, + '2' => Border::BORDER_MEDIUM, + '3' => Border::BORDER_SLANTDASHDOT, + '4' => Border::BORDER_DASHED, + '5' => Border::BORDER_THICK, + '6' => Border::BORDER_DOUBLE, + '7' => Border::BORDER_DOTTED, + '8' => Border::BORDER_MEDIUMDASHED, + '9' => Border::BORDER_DASHDOT, + '10' => Border::BORDER_MEDIUMDASHDOT, + '11' => Border::BORDER_DASHDOTDOT, + '12' => Border::BORDER_MEDIUMDASHDOTDOT, + '13' => Border::BORDER_MEDIUMDASHDOTDOT, + ], + 'fillType' => [ + '1' => Fill::FILL_SOLID, + '2' => Fill::FILL_PATTERN_DARKGRAY, + '3' => Fill::FILL_PATTERN_MEDIUMGRAY, + '4' => Fill::FILL_PATTERN_LIGHTGRAY, + '5' => Fill::FILL_PATTERN_GRAY125, + '6' => Fill::FILL_PATTERN_GRAY0625, + '7' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe + '8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe + '9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe + '10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe + '11' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch + '12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch + '13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL, + '14' => Fill::FILL_PATTERN_LIGHTVERTICAL, + '15' => Fill::FILL_PATTERN_LIGHTUP, + '16' => Fill::FILL_PATTERN_LIGHTDOWN, + '17' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch + '18' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch + ], + 'horizontal' => [ + '1' => Alignment::HORIZONTAL_GENERAL, + '2' => Alignment::HORIZONTAL_LEFT, + '4' => Alignment::HORIZONTAL_RIGHT, + '8' => Alignment::HORIZONTAL_CENTER, + '16' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + '32' => Alignment::HORIZONTAL_JUSTIFY, + '64' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + ], + 'underline' => [ + '1' => Font::UNDERLINE_SINGLE, + '2' => Font::UNDERLINE_DOUBLE, + '3' => Font::UNDERLINE_SINGLEACCOUNTING, + '4' => Font::UNDERLINE_DOUBLEACCOUNTING, + ], + 'vertical' => [ + '1' => Alignment::VERTICAL_TOP, + '2' => Alignment::VERTICAL_BOTTOM, + '4' => Alignment::VERTICAL_CENTER, + '8' => Alignment::VERTICAL_JUSTIFY, + ], + ]; + + public function __construct(Spreadsheet $spreadsheet, bool $readDataOnly) + { + $this->spreadsheet = $spreadsheet; + $this->readDataOnly = $readDataOnly; + } + + public function read(?SimpleXMLElement $sheet, $maxRow, $maxCol) + { + foreach ($sheet->Styles->StyleRegion as $styleRegion) { + $styleAttributes = $styleRegion->attributes(); + if (($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { + $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); + $startRow = $styleAttributes['startRow'] + 1; + + $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; + $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); + + $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); + $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; + + $styleAttributes = $styleRegion->Style->attributes(); + + $styleArray = []; + // We still set the number format mask for date/time values, even if readDataOnly is true + $formatCode = (string) $styleAttributes['Format']; + if (Date::isDateTimeFormatCode($formatCode)) { + $styleArray['numberFormat']['formatCode'] = $formatCode; + } + if (!$this->readDataOnly) { + // If readDataOnly is false, we set all formatting information + $styleArray['numberFormat']['formatCode'] = $formatCode; + + self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); + self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); + $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; + $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); + $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; + $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; + + $this->addColors($styleArray, $styleAttributes); + + $fontAttributes = $styleRegion->Style->Font->attributes(); + $styleArray['font']['name'] = (string) $styleRegion->Style->Font; + $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); + $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; + $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; + $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; + self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); + + switch ($fontAttributes['Script']) { + case '1': + $styleArray['font']['superscript'] = true; + + break; + case '-1': + $styleArray['font']['subscript'] = true; + + break; + } + + if (isset($styleRegion->Style->StyleBorder)) { + $srssb = $styleRegion->Style->StyleBorder; + $this->addBorderStyle($srssb, $styleArray, 'top'); + $this->addBorderStyle($srssb, $styleArray, 'bottom'); + $this->addBorderStyle($srssb, $styleArray, 'left'); + $this->addBorderStyle($srssb, $styleArray, 'right'); + $this->addBorderDiagonal($srssb, $styleArray); + } + if (isset($styleRegion->Style->HyperLink)) { + // TO DO + $hyperlink = $styleRegion->Style->HyperLink->attributes(); + } + } + $this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); + } + } + } + + private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void + { + if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; + } elseif (isset($srssb->Diagonal)) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; + } elseif (isset($srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; + } + } + + private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void + { + $ucDirection = ucfirst($direction); + if (isset($srssb->$ucDirection)) { + $styleArray['borders'][$direction] = self::parseBorderAttributes($srssb->$ucDirection->attributes()); + } + } + + private function calcRotation(SimpleXMLElement $styleAttributes): int + { + $rotation = (int) $styleAttributes->Rotation; + if ($rotation >= 270 && $rotation <= 360) { + $rotation -= 360; + } + $rotation = (abs($rotation) > 90) ? 0 : $rotation; + + return $rotation; + } + + private static function addStyle(array &$styleArray, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key] = self::$mappings[$key][$value]; + } + } + + private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key1][$key] = self::$mappings[$key][$value]; + } + } + + private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array + { + $styleArray = []; + if ($borderAttributes !== null) { + if (isset($borderAttributes['Color'])) { + $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); + } + + self::addStyle($styleArray, 'borderStyle', $borderAttributes['Style']); + } + + return $styleArray; + } + + private static function parseGnumericColour(string $gnmColour): string + { + [$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour); + $gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2); + $gnmG = substr(str_pad($gnmG, 4, '0', STR_PAD_RIGHT), 0, 2); + $gnmB = substr(str_pad($gnmB, 4, '0', STR_PAD_RIGHT), 0, 2); + + return $gnmR . $gnmG . $gnmB; + } + + private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void + { + $RGB = self::parseGnumericColour($styleAttributes['Fore']); + $styleArray['font']['color']['rgb'] = $RGB; + $RGB = self::parseGnumericColour($styleAttributes['Back']); + $shade = (string) $styleAttributes['Shade']; + if (($RGB != '000000') || ($shade != '0')) { + $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); + if ($shade === '1') { + $styleArray['fill']['startColor']['rgb'] = $RGB; + $styleArray['fill']['endColor']['rgb'] = $RGB2; + } else { + $styleArray['fill']['endColor']['rgb'] = $RGB; + $styleArray['fill']['startColor']['rgb'] = $RGB2; + } + self::addStyle2($styleArray, 'fill', 'fillType', $shade); + } + } +} From e71c2e46d00b23b33b7fafc3c0b2aa7bd17139b9 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 May 2021 19:57:08 +0200 Subject: [PATCH 18/28] Minor style tweaks --- src/PhpSpreadsheet/Reader/Gnumeric.php | 10 ++----- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 28 +++++++++++-------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 049e1da143..85bae6f8c8 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -12,14 +12,8 @@ use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Settings; -use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PhpOffice\PhpSpreadsheet\Style\Alignment; -use PhpOffice\PhpSpreadsheet\Style\Border; -use PhpOffice\PhpSpreadsheet\Style\Borders; -use PhpOffice\PhpSpreadsheet\Style\Fill; -use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; use XMLReader; @@ -349,7 +343,9 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); } - (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol); + if ($sheet->Styles !== null) { + (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol); + } $this->processComments($sheet); $this->processColumnWidths($sheet, $maxCol); diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 9c725b203b..02166eb027 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; @@ -92,10 +91,17 @@ public function __construct(Spreadsheet $spreadsheet, bool $readDataOnly) $this->readDataOnly = $readDataOnly; } - public function read(?SimpleXMLElement $sheet, $maxRow, $maxCol) + public function read(SimpleXMLElement $sheet, int $maxRow, int $maxCol): void { - foreach ($sheet->Styles->StyleRegion as $styleRegion) { - $styleAttributes = $styleRegion->attributes(); + if ($sheet->Styles->StyleRegion !== null) { + $this->readStyles($sheet->Styles->StyleRegion, $maxRow, $maxCol); + } + } + + private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $maxCol): void + { + foreach ($styleRegion as $style) { + $styleAttributes = $style->attributes(); if (($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); $startRow = $styleAttributes['startRow'] + 1; @@ -106,7 +112,7 @@ public function read(?SimpleXMLElement $sheet, $maxRow, $maxCol) $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; - $styleAttributes = $styleRegion->Style->attributes(); + $styleAttributes = $style->Style->attributes(); $styleArray = []; // We still set the number format mask for date/time values, even if readDataOnly is true @@ -127,8 +133,8 @@ public function read(?SimpleXMLElement $sheet, $maxRow, $maxCol) $this->addColors($styleArray, $styleAttributes); - $fontAttributes = $styleRegion->Style->Font->attributes(); - $styleArray['font']['name'] = (string) $styleRegion->Style->Font; + $fontAttributes = $style->Style->Font->attributes(); + $styleArray['font']['name'] = (string) $style->Style->Font; $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; @@ -146,17 +152,17 @@ public function read(?SimpleXMLElement $sheet, $maxRow, $maxCol) break; } - if (isset($styleRegion->Style->StyleBorder)) { - $srssb = $styleRegion->Style->StyleBorder; + if (isset($style->Style->StyleBorder)) { + $srssb = $style->Style->StyleBorder; $this->addBorderStyle($srssb, $styleArray, 'top'); $this->addBorderStyle($srssb, $styleArray, 'bottom'); $this->addBorderStyle($srssb, $styleArray, 'left'); $this->addBorderStyle($srssb, $styleArray, 'right'); $this->addBorderDiagonal($srssb, $styleArray); } - if (isset($styleRegion->Style->HyperLink)) { + if (isset($style->Style->HyperLink)) { // TO DO - $hyperlink = $styleRegion->Style->HyperLink->attributes(); + $hyperlink = $style->Style->HyperLink->attributes(); } } $this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); From 5d6b072fb0557344d110cee8502822cccd6f2422 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 May 2021 20:58:17 +0200 Subject: [PATCH 19/28] More Minor tweaks --- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 02166eb027..270f0728f9 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -101,21 +101,19 @@ public function read(SimpleXMLElement $sheet, int $maxRow, int $maxCol): void private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $maxCol): void { foreach ($styleRegion as $style) { + if ($style === null) { + continue; + } + $styleAttributes = $style->attributes(); if (($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { - $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); - $startRow = $styleAttributes['startRow'] + 1; - - $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; - $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); - - $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); - $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; + $cellRange = $this->readStyleRange($styleAttributes, $maxCol, $maxRow); $styleAttributes = $style->Style->attributes(); $styleArray = []; - // We still set the number format mask for date/time values, even if readDataOnly is true + // We still set the number format mask for date/time values, even if readDataOnly is true + // so that we can identify whether a float is a float or a date value $formatCode = (string) $styleAttributes['Format']; if (Date::isDateTimeFormatCode($formatCode)) { $styleArray['numberFormat']['formatCode'] = $formatCode; @@ -247,7 +245,7 @@ private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes $styleArray['font']['color']['rgb'] = $RGB; $RGB = self::parseGnumericColour($styleAttributes['Back']); $shade = (string) $styleAttributes['Shade']; - if (($RGB != '000000') || ($shade != '0')) { + if (($RGB !== '000000') || ($shade !== '0')) { $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); if ($shade === '1') { $styleArray['fill']['startColor']['rgb'] = $RGB; @@ -259,4 +257,18 @@ private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes self::addStyle2($styleArray, 'fill', 'fillType', $shade); } } + + private function readStyleRange(?SimpleXMLElement $styleAttributes, int $maxCol, int $maxRow): string + { + $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); + $startRow = $styleAttributes['startRow'] + 1; + + $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; + $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); + + $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); + $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; + + return $cellRange; + } } From 13ec1633337e657e80e1ca2063da90853e0cb1b5 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 May 2021 21:04:13 +0200 Subject: [PATCH 20/28] phpstan appeasement --- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 270f0728f9..3ee57f41c1 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -106,7 +106,7 @@ private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $max } $styleAttributes = $style->attributes(); - if (($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { + if ($styleAttributes !== null && ($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { $cellRange = $this->readStyleRange($styleAttributes, $maxCol, $maxRow); $styleAttributes = $style->Style->attributes(); @@ -258,7 +258,7 @@ private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes } } - private function readStyleRange(?SimpleXMLElement $styleAttributes, int $maxCol, int $maxRow): string + private function readStyleRange(SimpleXMLElement $styleAttributes, int $maxCol, int $maxRow): string { $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); $startRow = $styleAttributes['startRow'] + 1; From 60e6a59ff2b132d77750d0f65db4296243ae2851 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 May 2021 22:08:58 +0200 Subject: [PATCH 21/28] Additional refactoring --- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 3ee57f41c1..301a8d9bd1 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -118,50 +118,10 @@ private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $max if (Date::isDateTimeFormatCode($formatCode)) { $styleArray['numberFormat']['formatCode'] = $formatCode; } - if (!$this->readDataOnly) { + if ($this->readDataOnly === false) { // If readDataOnly is false, we set all formatting information $styleArray['numberFormat']['formatCode'] = $formatCode; - - self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); - self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); - $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; - $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); - $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; - $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; - - $this->addColors($styleArray, $styleAttributes); - - $fontAttributes = $style->Style->Font->attributes(); - $styleArray['font']['name'] = (string) $style->Style->Font; - $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); - $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; - $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; - $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; - self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); - - switch ($fontAttributes['Script']) { - case '1': - $styleArray['font']['superscript'] = true; - - break; - case '-1': - $styleArray['font']['subscript'] = true; - - break; - } - - if (isset($style->Style->StyleBorder)) { - $srssb = $style->Style->StyleBorder; - $this->addBorderStyle($srssb, $styleArray, 'top'); - $this->addBorderStyle($srssb, $styleArray, 'bottom'); - $this->addBorderStyle($srssb, $styleArray, 'left'); - $this->addBorderStyle($srssb, $styleArray, 'right'); - $this->addBorderDiagonal($srssb, $styleArray); - } - if (isset($style->Style->HyperLink)) { - // TO DO - $hyperlink = $style->Style->HyperLink->attributes(); - } + $styleArray = $this->readStyle($styleArray, $styleAttributes, $style); } $this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); } @@ -271,4 +231,50 @@ private function readStyleRange(SimpleXMLElement $styleAttributes, int $maxCol, return $cellRange; } + + private function readStyle(array $styleArray, ?SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array + { + self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); + self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); + $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; + $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); + $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; + $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; + + $this->addColors($styleArray, $styleAttributes); + + $fontAttributes = $style->Style->Font->attributes(); + $styleArray['font']['name'] = (string) $style->Style->Font; + $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); + $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; + $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; + $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; + self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); + + switch ($fontAttributes['Script']) { + case '1': + $styleArray['font']['superscript'] = true; + + break; + case '-1': + $styleArray['font']['subscript'] = true; + + break; + } + + if (isset($style->Style->StyleBorder)) { + $srssb = $style->Style->StyleBorder; + $this->addBorderStyle($srssb, $styleArray, 'top'); + $this->addBorderStyle($srssb, $styleArray, 'bottom'); + $this->addBorderStyle($srssb, $styleArray, 'left'); + $this->addBorderStyle($srssb, $styleArray, 'right'); + $this->addBorderDiagonal($srssb, $styleArray); + } + if (isset($style->Style->HyperLink)) { + // TO DO + $hyperlink = $style->Style->HyperLink->attributes(); + } + + return $styleArray; + } } From bb572f757f8a786a8e12774e412a517b786a5b32 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 May 2021 22:18:00 +0200 Subject: [PATCH 22/28] Should fix phpstan --- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 301a8d9bd1..5e9b5d839a 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -118,7 +118,7 @@ private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $max if (Date::isDateTimeFormatCode($formatCode)) { $styleArray['numberFormat']['formatCode'] = $formatCode; } - if ($this->readDataOnly === false) { + if ($this->readDataOnly === false && $styleAttributes !== null) { // If readDataOnly is false, we set all formatting information $styleArray['numberFormat']['formatCode'] = $formatCode; $styleArray = $this->readStyle($styleArray, $styleAttributes, $style); @@ -232,7 +232,7 @@ private function readStyleRange(SimpleXMLElement $styleAttributes, int $maxCol, return $cellRange; } - private function readStyle(array $styleArray, ?SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array + private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array { self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); From 9a5a630e3fd0f6c87444dbb5d188d63eb5194230 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 May 2021 22:23:43 +0200 Subject: [PATCH 23/28] Check against font attributes --- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 5e9b5d839a..9961632f78 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -244,22 +244,24 @@ private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, $this->addColors($styleArray, $styleAttributes); $fontAttributes = $style->Style->Font->attributes(); - $styleArray['font']['name'] = (string) $style->Style->Font; - $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); - $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; - $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; - $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; - self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); - - switch ($fontAttributes['Script']) { - case '1': - $styleArray['font']['superscript'] = true; - - break; - case '-1': - $styleArray['font']['subscript'] = true; - - break; + if ($fontAttributes !== null) { + $styleArray['font']['name'] = (string)$style->Style->Font; + $styleArray['font']['size'] = (int)($fontAttributes['Unit']); + $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; + $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; + $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; + self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); + + switch ($fontAttributes['Script']) { + case '1': + $styleArray['font']['superscript'] = true; + + break; + case '-1': + $styleArray['font']['subscript'] = true; + + break; + } } if (isset($style->Style->StyleBorder)) { From 2ddb23574eb5961741ab24759225754fc3203785 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 May 2021 22:39:22 +0200 Subject: [PATCH 24/28] PHPCS Fix --- src/PhpSpreadsheet/Reader/Gnumeric/Styles.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 9961632f78..9998448e2f 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -245,8 +245,8 @@ private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, $fontAttributes = $style->Style->Font->attributes(); if ($fontAttributes !== null) { - $styleArray['font']['name'] = (string)$style->Style->Font; - $styleArray['font']['size'] = (int)($fontAttributes['Unit']); + $styleArray['font']['name'] = (string) $style->Style->Font; + $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; From a0719d8dd4e1766c67085a3b24a6573d5d83c4e1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 9 May 2021 14:27:50 +0200 Subject: [PATCH 25/28] Use modification time from properties when saving Excel5 --- src/PhpSpreadsheet/Writer/Xls.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index a1b477bf90..4bece77452 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -219,7 +219,8 @@ public function save($pFilename): void $arrRootData[] = $OLE_DocumentSummaryInformation; } - $root = new Root(time(), time(), $arrRootData); + $time = $this->spreadsheet->getProperties()->getModified(); + $root = new Root($time, $time, $arrRootData); // save the OLE file $this->openFileHandle($pFilename); $root->save($this->fileHandle); From e5bfc3c8992c332df800b7e2d72f77b30e8a2d1f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 10 May 2021 22:44:07 +0200 Subject: [PATCH 26/28] Add phpcs version compatibility check to pipeline --- .github/workflows/main.yml | 31 +++++++++++++++ composer.json | 3 +- composer.lock | 79 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 87d933e289..754a463b04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,6 +124,37 @@ jobs: - name: Code style with PHP_CodeSniffer run: ./vendor/bin/phpcs -q --report=checkstyle | cs2pr + versions: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + extensions: ctype, dom, gd, iconv, fileinfo, libxml, mbstring, simplexml, xml, xmlreader, xmlwriter, zip, zlib + coverage: none + tools: cs2pr + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Code Version Compatibility check with PHP_CodeSniffer + run: ./vendor/bin/phpcs -q --report-width=200 --report=summary,full src/ --standard=PHPCompatibility --runtime-set testVersion 7.2- + phpstan: runs-on: ubuntu-latest steps: diff --git a/composer.json b/composer.json index 6cd650211b..314f21cd7a 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,6 @@ }, "require": { "php": "^7.2 || ^8.0", - "ext-simplexml": "*", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -61,6 +60,7 @@ "ext-iconv": "*", "ext-libxml": "*", "ext-mbstring": "*", + "ext-simplexml": "*", "ext-xml": "*", "ext-xmlreader": "*", "ext-xmlwriter": "*", @@ -75,6 +75,7 @@ "psr/simple-cache": "^1.0" }, "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0", "friendsofphp/php-cs-fixer": "^2.18", "jpgraph/jpgraph": "^4.0", diff --git a/composer.lock b/composer.lock index 3670f857dd..4921cc8fb0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3be2673a6367d296c616bf9c34b77953", + "content-hash": "9158fcde13425499acaf0da201637737", "packages": [ { "name": "ezyang/htmlpurifier", @@ -802,6 +802,77 @@ ], "time": "2021-03-25T17:01:18+00:00" }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "c960cf4629fab7155caca18c038ca7257b7595e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/c960cf4629fab7155caca18c038ca7257b7595e3", + "reference": "c960cf4629fab7155caca18c038ca7257b7595e3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "enlightn/security-checker": "^1.2", + "phpcompatibility/php-compatibility": "^9.0" + }, + "default-branch": true, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2021-03-14T13:49:41+00:00" + }, { "name": "doctrine/annotations", "version": "1.12.1", @@ -5065,12 +5136,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "dealerdirect/phpcodesniffer-composer-installer": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^7.2 || ^8.0", - "ext-simplexml": "*", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -5078,6 +5150,7 @@ "ext-iconv": "*", "ext-libxml": "*", "ext-mbstring": "*", + "ext-simplexml": "*", "ext-xml": "*", "ext-xmlreader": "*", "ext-xmlwriter": "*", From 9c43d5f1b7426dbd38274ef27aeb6d50b00acfdd Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Mon, 10 May 2021 21:35:34 -0700 Subject: [PATCH 27/28] Xlsx Writer Formula with Bool Result of False Fix for #2082. Xlsx Writer was writing a cell which is a formula which evaluates to boolean false as an empty XML tag. This is okay for Excel 365, but not for Excel 2016-. Change to write the tag as a value of 0 instead, which works for all Excel releases. Add test. --- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 1 + .../Writer/Xlsx/Issue2082Test.php | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2082Test.php diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 3978eb6f04..9b1d67307f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1230,6 +1230,7 @@ private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $objWriter->writeAttribute('t', 'str'); } elseif (is_bool($calculatedValue)) { $objWriter->writeAttribute('t', 'b'); + $calculatedValue = (int) $calculatedValue; } // array values are not yet supported //$attributes = $pCell->getFormulaAttributes(); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2082Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2082Test.php new file mode 100644 index 0000000000..1f72a3821d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2082Test.php @@ -0,0 +1,40 @@ +getActiveSheet(); + $worksheet->fromArray(['A', 'B', 'C', 'D']); + $worksheet->getCell('A2')->setValue('=A1<>"A"'); + $worksheet->getCell('A3')->setValue('=A1="A"'); + $worksheet->getCell('B2')->setValue('=LEFT(B1, 0)'); + $worksheet->getCell('B3')->setValue('=B2=""'); + + $writer = new Writer($spreadsheet); + $writer->save($outputFilename); + $zipfile = "zip://$outputFilename#xl/worksheets/sheet1.xml"; + $contents = file_get_contents($zipfile); + unlink($outputFilename); + if ($contents === false) { + self::fail('Unable to open file'); + } else { + self::assertStringContainsString('A1<>"A"0', $contents); + self::assertStringContainsString('A1="A"1', $contents); + self::assertStringContainsString('LEFT(B1, 0)', $contents); + self::assertStringContainsString('B2=""1', $contents); + } + } +} From d08653433c39feb5337b1c0182fee7475fd7c643 Mon Sep 17 00:00:00 2001 From: Tanguy De Taxis Date: Tue, 11 May 2021 10:09:55 +0200 Subject: [PATCH 28/28] fr locale - Add JOURS function (DAYS equivalent) --- src/PhpSpreadsheet/Calculation/locale/fr/functions | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PhpSpreadsheet/Calculation/locale/fr/functions b/src/PhpSpreadsheet/Calculation/locale/fr/functions index 7f40d5fdeb..b28b4a7034 100644 --- a/src/PhpSpreadsheet/Calculation/locale/fr/functions +++ b/src/PhpSpreadsheet/Calculation/locale/fr/functions @@ -47,6 +47,7 @@ DVARP = BDVARP ## Calcule la variance pour l’ensemble d’une population d DATE = DATE ## Renvoie le numéro de série d’une date précise. DATEVALUE = DATEVAL ## Convertit une date représentée sous forme de texte en numéro de série. DAY = JOUR ## Convertit un numéro de série en jour du mois. +DAYS = JOURS ## Calcule le nombre de jours qui séparent deux dates. DAYS360 = JOURS360 ## Calcule le nombre de jours qui séparent deux dates sur la base d’une année de 360 jours. EDATE = MOIS.DECALER ## Renvoie le numéro séquentiel de la date qui représente une date spécifiée (l’argument date_départ), corrigée en plus ou en moins du nombre de mois indiqué. EOMONTH = FIN.MOIS ## Renvoie le numéro séquentiel de la date du dernier jour du mois précédant ou suivant la date_départ du nombre de mois indiqué.