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 583b262c..7f0d4ab2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -316,10 +316,10 @@ class Chart extends WriterPart 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 21762f89..ed4f44c3 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 2b0c7016..2968a3fe 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -103,17 +103,21 @@ class Styles extends BaseParserClass 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 e3a6b206..caf85c04 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php +++ b/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php @@ -75,6 +75,16 @@ class ColorMap 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 88d666b2..ccdad067 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php @@ -29,4 +29,15 @@ class DefaultFillTest extends TestCase 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 ed4f44c3..7940a6a2 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 716a3787..9dcf8d91 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 89355400..89f50e3b 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 1a4d7ca3..d4163cf7 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -11,8 +11,9 @@ use DOMNode; 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\Date; 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 @@ class Ods extends BaseReader $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 @@ class Ods extends BaseReader ++$worksheetID; } - $this->readDefinedRanges($spreadsheet, $workbookData, $tableNs); - $this->readDefinedExpressions($spreadsheet, $workbookData, $tableNs); + $autoFilterReader->read($workbookData); + $definedNameReader->read($workbookData); } $spreadsheet->setActiveSheetIndex(0); @@ -771,26 +773,6 @@ class Ods extends BaseReader 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 @@ class Ods extends BaseReader 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 00000000..bdc8b3ff --- /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 00000000..82d41710 --- /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 00000000..79f5c027 --- /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 00000000..cf0450f1 --- /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 e4bd1793..a589e549 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -101,6 +101,7 @@ class Content extends WriterPart $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 00000000..47c7ee6a --- /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 00000000..4368ef76 --- /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 89f50e3b..af995b7b 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 d4163cf7..26151cdc 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -783,11 +783,13 @@ class Ods extends BaseReader // 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 82d41710..17e2d4d5 100644 --- a/src/PhpSpreadsheet/Reader/Ods/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/Ods/BaseReader.php @@ -62,8 +62,10 @@ abstract class BaseReader $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 79f5c027..6810a3c7 100644 --- a/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php +++ b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php @@ -44,6 +44,7 @@ class DefinedNames extends BaseReader $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 00000000..760421ce --- /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 00000000..1b2e30b2 --- /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 15bbf0d4..137f6469 100644 --- a/samples/Basic/16_Csv.php +++ b/samples/Basic/16_Csv.php @@ -38,7 +38,8 @@ $helper->write($spreadsheetFromCSV, __FILE__, ['Xlsx']); $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 ab3a9a07..8a3223b1 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php @@ -51,16 +51,20 @@ class Sum { $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 a9ea7f29..b85f0c90 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -26,4 +26,22 @@ class SumTest extends AllSetupTeardown { 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 00000000..9b7d16aa --- /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 a8219076..0c54613e 100644 --- a/tests/data/Calculation/MathTrig/SUM.php +++ b/tests/data/Calculation/MathTrig/SUM.php @@ -4,5 +4,9 @@ return [ [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 00000000..fd184ebd --- /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 9dcf8d91..05b8c117 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 d3cdf1b0..80ca46cb 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -270,7 +270,9 @@ class Gnumeric extends BaseReader $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 84844d3d..c3d5d8f4 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -390,7 +390,13 @@ class Worksheet extends BIFFwriter // 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 @@ class Worksheet extends BIFFwriter // 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 00000000..055ee1b9 --- /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 c3d5d8f4..894ce03a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2166,7 +2166,9 @@ class Worksheet extends BIFFwriter */ 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 055ee1b9..00000000 --- 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 00000000..7de39328 --- /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 00000000..7e1ca967 --- /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 af995b7b..0a8369ee 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 80ca46cb..d7358293 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -25,7 +25,19 @@ use XMLReader; 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 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader $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 @@ class Gnumeric extends BaseReader $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 @@ class Gnumeric extends BaseReader ]; 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; + 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; - } + break; } } $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); @@ -188,6 +195,7 @@ class Gnumeric extends BaseReader return $data; } + /** @var array */ private static $mappings = [ 'borderStyle' => [ '0' => Border::BORDER_NONE, @@ -266,7 +274,7 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader } } + /** + * @param mixed $value + */ + private static function testSimpleXml($value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement(''); + } + /** * Loads Spreadsheet from file. * @@ -306,12 +322,10 @@ class Gnumeric extends BaseReader $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 @@ class Gnumeric extends BaseReader $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 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader } } - private function processColumnLoop(int $c, int $maxCol, SimpleXMLElement $columnOverride, float $defaultWidth): int + private function setColumnWidth(int $whichColumn, float $defaultWidth): void { - $columnAttributes = $columnOverride->attributes(); + $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 = 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; - foreach ($sheet->Cols->ColInfo as $columnOverride) { - $c = $this->processColumnLoop($c, $maxCol, $columnOverride, $defaultWidth); + if ($columnAttributes !== null) { + $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; } - while ($c <= $maxCol) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); - ++$c; + $whichColumn = 0; + foreach ($sheet->Cols->ColInfo as $columnOverride) { + $whichColumn = $this->processColumnLoop($whichColumn, $maxCol, $columnOverride, $defaultWidth); + } + 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 @@ class Gnumeric extends BaseReader } } - 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 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader $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 0fe73005..accc2716 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 @@ class PageSetup 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 16d9c2e0..c466a859 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 @@ class Properties } } - 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); - $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 '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 '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; - case 'user-defined': - [, $attrName] = explode(':', $attributes['name']); - switch ($attrName) { - case 'publisher': - $docProps->setCompany($propertyValue); - - break; - case 'category': - $docProps->setCategory($propertyValue); - - break; - case 'manager': - $docProps->setManager($propertyValue); - - break; - } - - break; + break; + } } } } - public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML, array $namespacesMeta): void + private function userDefinedProperties(string $attrName, string $propertyValue): void { - if (isset($namespacesMeta['office'])) { - $officeXML = $xml->children($namespacesMeta['office']); + $docProps = $this->spreadsheet->getProperties(); + switch ($attrName) { + case 'publisher': + $docProps->setCompany($propertyValue); + + break; + case 'category': + $docProps->setCategory($propertyValue); + + break; + case 'manager': + $docProps->setManager($propertyValue); + + break; + } + } + + public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML): void + { + $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 e24178e5..9544fc3a 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php @@ -115,7 +115,8 @@ class GnumericLoadTest extends TestCase 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 7940a6a2..4599f877 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 05b8c117..c836a99a 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 d7358293..d66dbb88 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -482,6 +482,7 @@ class Gnumeric extends BaseReader $this->processColumnWidths($sheet, $maxCol); $this->processRowHeights($sheet, $maxRow); $this->processMergedCells($sheet); + $this->processAutofilter($sheet); ++$worksheetID; } @@ -514,10 +515,10 @@ class Gnumeric extends BaseReader } } - 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 @@ class Gnumeric extends BaseReader } } + 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 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader } } - 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 00000000..18dde473 --- /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 | 22 ++++--- tests/data/Calculation/LookupRef/INDEX.php | 63 +++++++++++++++---- 4 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0a8369ee..037ee997 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 00000000..b0739eb3 --- /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 @@ class Matrix 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 @@ class Matrix 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 157794ab..a699534e 100644 --- a/tests/data/Calculation/LookupRef/INDEX.php +++ b/tests/data/Calculation/LookupRef/INDEX.php @@ -6,7 +6,7 @@ return [ // Input [20 => ['R' => 1]], ], - [ + 'Negative Row' => [ '#VALUE!', // Expected // Input [ @@ -15,7 +15,7 @@ return [ ], -1, ], - [ + 'Row > matrix rows' => [ '#REF!', // Expected // Input [ @@ -24,7 +24,25 @@ return [ ], 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 @@ return [ ], 2, ], - [ + 'Return row 2 from larger matrix' => [ [21 => ['R' => 2, 'S' => 4]], // Expected // Input [ @@ -43,17 +61,17 @@ return [ 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 @@ return [ 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 @@ return [ 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 2968a3fe..80c32065 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -42,9 +42,12 @@ class Styles extends BaseParserClass 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 @@ class Styles extends BaseParserClass $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 ccdad067..dc61b953 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 b0e34e25..e5f4e952 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -102,14 +102,27 @@ class AddressHelper ?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 76a6aee8..dd9b2391 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 4599f877..0c1a549c 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 e5f4e952..91f85bed 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -102,26 +102,22 @@ class AddressHelper ?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 91f85bed..632c046f 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -102,20 +102,20 @@ class AddressHelper ?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 0b3917f2..58d2573e 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -13,6 +13,8 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; */ 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 d0324964..823d70c6 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 282cd528..4ef4efe7 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -284,7 +284,7 @@ class Xml extends BaseReader $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 00000000..e71992ed --- /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 06371c79..96c29780 100644 --- a/tests/data/Calculation/LookupRef/CHOOSE.php +++ b/tests/data/Calculation/LookupRef/CHOOSE.php @@ -25,4 +25,12 @@ return [ '#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 d2a8a446..61cb7e06 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -328,4 +328,28 @@ return [ 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 00000000..9a5e4c2e --- /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 d66dbb88..049e1da1 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; 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 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); } + (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol); + $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); - } - } - $this->processColumnWidths($sheet, $maxCol); $this->processRowHeights($sheet, $maxRow); $this->processMergedCells($sheet); @@ -493,28 +366,6 @@ class Gnumeric extends BaseReader 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 @@ class Gnumeric extends BaseReader } } - 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 @@ class Gnumeric extends BaseReader 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 00000000..9c725b20 --- /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 049e1da1..85bae6f8 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -12,14 +12,8 @@ use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; 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 @@ class Gnumeric extends BaseReader $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 9c725b20..02166eb0 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 @@ class Styles $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 @@ class Styles $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 @@ class Styles $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 @@ class Styles 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 02166eb0..270f0728 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -101,21 +101,19 @@ class Styles 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 @@ class Styles $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 @@ class Styles 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 270f0728..3ee57f41 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -106,7 +106,7 @@ class Styles } $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 @@ class Styles } } - 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 3ee57f41..301a8d9b 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -118,50 +118,10 @@ class Styles 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 @@ class Styles 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 301a8d9b..5e9b5d83 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -118,7 +118,7 @@ class Styles 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 @@ class Styles 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 | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 5e9b5d83..9961632f 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -244,22 +244,24 @@ class Styles $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']); + 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; + switch ($fontAttributes['Script']) { + case '1': + $styleArray['font']['superscript'] = true; - break; - case '-1': - $styleArray['font']['subscript'] = true; + break; + case '-1': + $styleArray['font']['subscript'] = true; - break; + 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 9961632f..9998448e 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -245,8 +245,8 @@ class Styles $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 a1b477bf..4bece774 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -219,7 +219,8 @@ class Xls extends BaseWriter $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 87d933e2..754a463b 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 6cd65021..314f21cd 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 3670f857..4921cc8f 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 3978eb6f..9b1d6730 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1230,6 +1230,7 @@ class Worksheet extends WriterPart $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 00000000..1f72a382 --- /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 7f40d5fd..b28b4a70 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é.