From 34dd0a929e5546ced2cadeb4745018be291fb9aa Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 15 May 2022 13:40:46 +0200 Subject: [PATCH 01/30] Read ChartSheets with the Xlsx Reader if includeCharts is enabled; although we're reading it as a normal worksheet that simply contains a chart at cell A1, xOffset 0, yOffset 0, with no data And Handle ChartSheets in the Xlsx Writer --- src/PhpSpreadsheet/Chart/Chart.php | 4 +- src/PhpSpreadsheet/Reader/Xlsx.php | 51 +++++++++++++++++-- src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php | 2 + src/PhpSpreadsheet/Writer/Xlsx/Drawing.php | 42 ++++++++++----- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index bed89464..5982548e 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -124,7 +124,7 @@ class Chart * * @var string */ - private $bottomRightCellRef = 'A1'; + private $bottomRightCellRef = ''; /** * Bottom-Right X-Offset. @@ -524,7 +524,7 @@ class Chart * * @return $this */ - public function setBottomRightPosition($cell, $xOffset = null, $yOffset = null) + public function setBottomRightPosition($cell = '', $xOffset = null, $yOffset = null) { $this->bottomRightCellRef = $cell; if ($xOffset !== null) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index a6e7fe03..2068e950 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -512,6 +512,12 @@ class Xlsx extends BaseReader case Namespaces::PURL_WORKSHEET: $worksheets[(string) $ele['Id']] = $ele['Target']; + break; + case Namespaces::CHARTSHEET: + if ($this->includeCharts === true) { + $worksheets[(string) $ele['Id']] = $ele['Target']; + } + break; // a vbaProject ? (: some macros) case Namespaces::VBA: @@ -690,6 +696,13 @@ class Xlsx extends BaseReader continue; } + $sheetReferenceId = (string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id'); + if (isset($worksheets[$sheetReferenceId]) === false) { + ++$countSkippedSheets; + $mapSheetId[$oldSheetId] = null; + + continue; + } // Map old sheet id in original workbook to new sheet id. // They will differ if loadSheetsOnly() is being used $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets; @@ -701,7 +714,8 @@ class Xlsx extends BaseReader // and we're simply bringing the worksheet name in line with the formula, not the // reverse $docSheet->setTitle((string) $eleSheetAttr['name'], false, false); - $fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id')]; + + $fileWorksheet = (string) $worksheets[$sheetReferenceId]; $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS); $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS); @@ -1195,6 +1209,7 @@ class Xlsx extends BaseReader $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); } } + if ($xmlSheet->drawing && !$this->readDataOnly) { $unparsedDrawings = []; $fileDrawing = null; @@ -1203,6 +1218,7 @@ class Xlsx extends BaseReader $fileDrawing = $drawings[$drawingRelId]; $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels'; $relsDrawing = $this->loadZipNoNamespace($drawingFilename, $xmlNamespaceBase); + $images = []; $hyperlinks = []; if ($relsDrawing && $relsDrawing->Relationship) { @@ -1223,6 +1239,7 @@ class Xlsx extends BaseReader } } } + $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, ''); $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING); @@ -1401,6 +1418,27 @@ class Xlsx extends BaseReader } } } + if ($xmlDrawingChildren->absoluteAnchor) { + foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) { + if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) { + $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; + /** @var SimpleXMLElement $chartRef */ + $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; + $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); + $width = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cx')[0]); + $height = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cy')[0]); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => 'A1', + 'fromOffsetX' => 0, + 'fromOffsetY' => 0, + 'width' => $width, + 'height' => $height, + 'worksheetTitle' => $docSheet->getTitle(), + ]; + } + } + } if (empty($relsDrawing) && $xmlDrawing->count() == 0) { // Save Drawing without rels and children as unparsed $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML(); @@ -1600,16 +1638,21 @@ class Xlsx extends BaseReader $chartEntryRef = ltrim((string) $contentType['PartName'], '/'); $chartElements = $this->loadZip($chartEntryRef); $objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml')); - if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; if (isset($chartDetails[$chartPositionRef])) { $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); - $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + // For oneCellAnchor or absoluteAnchor positioned charts, + // toCoordinate is not in the data. Does it need to be calculated? if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) { - // For oneCellAnchor positioned charts, toCoordinate is not in the data. Does it need to be calculated? + // twoCellAnchor + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); + } else { + // oneCellAnchor or absoluteAnchor (e.g. Chart sheet) + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index c0713ae4..eb768d09 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -50,6 +50,8 @@ class Namespaces const WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; + const CHARTSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet'; + const SCHEMA_MICROSOFT = 'http://schemas.microsoft.com/office/2006/relationships'; const EXTENSIBILITY = 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility'; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 816bb9d4..33eee1e0 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -89,22 +89,36 @@ class Drawing extends WriterPart $tl = $chart->getTopLeftPosition(); $tlColRow = Coordinate::indexesFromString($tl['cell']); $br = $chart->getBottomRightPosition(); - $brColRow = Coordinate::indexesFromString($br['cell']); - $objWriter->startElement('xdr:twoCellAnchor'); + $isTwoCellAnchor = $br['cell'] !== ''; + if ($isTwoCellAnchor) { + $brColRow = Coordinate::indexesFromString($br['cell']); - $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); - $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); - $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); - $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); - $objWriter->endElement(); - $objWriter->startElement('xdr:to'); - $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1)); - $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset'])); - $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); - $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); - $objWriter->endElement(); + $objWriter->startElement('xdr:twoCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:to'); + $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } else { + $objWriter->startElement('xdr:absoluteAnchor'); + $objWriter->startElement('xdr:pos'); + $objWriter->writeAttribute('x', '0'); + $objWriter->writeAttribute('y', '0'); + $objWriter->endElement(); + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset'])); + $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } $objWriter->startElement('xdr:graphicFrame'); $objWriter->writeAttribute('macro', ''); From 198878b34741853fe6d9f49dca5cb1a6046f7a4d Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 16 May 2022 11:25:28 +0200 Subject: [PATCH 02/30] Add unit tests for ChartSheet reading --- .../Reader/Xlsx/ChartSheetTest.php | 35 ++++++++++++++++++ tests/data/Reader/XLSX/ChartSheet.xlsx | Bin 0 -> 18017 bytes 2 files changed, 35 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php create mode 100644 tests/data/Reader/XLSX/ChartSheet.xlsx diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php new file mode 100644 index 00000000..0f1605ff --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php @@ -0,0 +1,35 @@ +setIncludeCharts(true); + $spreadsheet = $reader->load($filename); + + self::assertCount(2, $spreadsheet->getAllSheets()); + $chartSheet = $spreadsheet->getSheetByName('Chart1'); + self::assertInstanceOf(Worksheet::class, $chartSheet); + self::assertSame(1, $chartSheet->getChartCount()); + } + + public function testLoadChartSheetWithoutCharts(): void + { + $filename = 'tests/data/Reader/XLSX/ChartSheet.xlsx'; + $reader = new Xlsx(); + $reader->setIncludeCharts(false); + $spreadsheet = $reader->load($filename); + + self::assertCount(1, $spreadsheet->getAllSheets()); + $chartSheet = $spreadsheet->getSheetByName('Chart1'); + self::assertNull($chartSheet); + } +} diff --git a/tests/data/Reader/XLSX/ChartSheet.xlsx b/tests/data/Reader/XLSX/ChartSheet.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..861bdeeb3bceb619fcda438868e7d39a1c33cbfd GIT binary patch literal 18017 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%0DYP$nPZ;6WPvU9sG(z+8UuqY7l87FSf0JZR2 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 JHu(Rz`hQFE%}oFR literal 0 HcmV?d00001 From 94963f4b97350d11923989a242625864d884f5d1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 16 May 2022 15:49:02 +0200 Subject: [PATCH 03/30] Type and return tpe cleanups --- phpstan-baseline.neon | 70 ------------------------------ src/PhpSpreadsheet/Chart/Chart.php | 59 +++++++++++++++++-------- 2 files changed, 41 insertions(+), 88 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 74087f44..664306fd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1160,76 +1160,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Chart/Chart.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getBottomRightXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getBottomRightYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getTopLeftXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getTopLeftYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightCell\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightCell\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightXOffset\\(\\) has parameter \\$xOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightYOffset\\(\\) has parameter \\$yOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftXOffset\\(\\) has parameter \\$xOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftYOffset\\(\\) has parameter \\$yOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$legend \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\|null\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 5982548e..2efec5bb 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -400,15 +400,15 @@ class Chart /** * Set the Top Left position for the chart. * - * @param string $cell + * @param string $cellAddress * @param int $xOffset * @param int $yOffset * * @return $this */ - public function setTopLeftPosition($cell, $xOffset = null, $yOffset = null) + public function setTopLeftPosition($cellAddress, $xOffset = null, $yOffset = null) { - $this->topLeftCellRef = $cell; + $this->topLeftCellRef = $cellAddress; if ($xOffset !== null) { $this->setTopLeftXOffset($xOffset); } @@ -446,13 +446,13 @@ class Chart /** * Set the Top Left cell position for the chart. * - * @param string $cell + * @param string $cellAddress * * @return $this */ - public function setTopLeftCell($cell) + public function setTopLeftCell($cellAddress) { - $this->topLeftCellRef = $cell; + $this->topLeftCellRef = $cellAddress; return $this; } @@ -491,6 +491,11 @@ class Chart ]; } + /** + * @param int $xOffset + * + * @return $this + */ public function setTopLeftXOffset($xOffset) { $this->topLeftXOffset = $xOffset; @@ -498,11 +503,16 @@ class Chart return $this; } - public function getTopLeftXOffset() + public function getTopLeftXOffset(): int { return $this->topLeftXOffset; } + /** + * @param int $yOffset + * + * @return $this + */ public function setTopLeftYOffset($yOffset) { $this->topLeftYOffset = $yOffset; @@ -510,7 +520,7 @@ class Chart return $this; } - public function getTopLeftYOffset() + public function getTopLeftYOffset(): int { return $this->topLeftYOffset; } @@ -518,15 +528,15 @@ class Chart /** * Set the Bottom Right position of the chart. * - * @param string $cell + * @param string $cellAddress * @param int $xOffset * @param int $yOffset * * @return $this */ - public function setBottomRightPosition($cell = '', $xOffset = null, $yOffset = null) + public function setBottomRightPosition($cellAddress = '', $xOffset = null, $yOffset = null) { - $this->bottomRightCellRef = $cell; + $this->bottomRightCellRef = $cellAddress; if ($xOffset !== null) { $this->setBottomRightXOffset($xOffset); } @@ -551,19 +561,22 @@ class Chart ]; } - public function setBottomRightCell($cell) + /** + * Set the Bottom Right cell for the chart. + * + * @return $this + */ + public function setBottomRightCell(string $cellAddress = '') { - $this->bottomRightCellRef = $cell; + $this->bottomRightCellRef = $cellAddress; return $this; } /** * Get the cell address where the bottom right of the chart is fixed. - * - * @return string */ - public function getBottomRightCell() + public function getBottomRightCell(): string { return $this->bottomRightCellRef; } @@ -602,6 +615,11 @@ class Chart ]; } + /** + * @param int $xOffset + * + * @return $this + */ public function setBottomRightXOffset($xOffset) { $this->bottomRightXOffset = $xOffset; @@ -609,11 +627,16 @@ class Chart return $this; } - public function getBottomRightXOffset() + public function getBottomRightXOffset(): int { return $this->bottomRightXOffset; } + /** + * @param int $yOffset + * + * @return $this + */ public function setBottomRightYOffset($yOffset) { $this->bottomRightYOffset = $yOffset; @@ -621,7 +644,7 @@ class Chart return $this; } - public function getBottomRightYOffset() + public function getBottomRightYOffset(): int { return $this->bottomRightYOffset; } From a33ed026e9aa48e342be14e5dcaa31ab2415bc79 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 16 May 2022 16:35:35 +0200 Subject: [PATCH 04/30] Update Change Log --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60d7425..128e1d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Add point size option for scatter charts +- Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) ### Changed - Memory and speed improvements, particularly for the Cell Collection, and the Writers. - See [the Discussion](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance + See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions ### Deprecated From 79f5cf99f68637b4e0db70eb1307e62e570300b8 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 17 May 2022 12:56:21 +0200 Subject: [PATCH 05/30] Resolve Issue #2833, NULL value handling in PRODUCT() Excel function --- .../Calculation/MathTrig/Operations.php | 2 +- tests/data/Calculation/MathTrig/PRODUCT.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php index 7fd30233..44e13526 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -108,7 +108,7 @@ class Operations // Loop through arguments foreach (Functions::flattenArray($args) as $arg) { // Is it a numeric value? - if (is_numeric($arg)) { + if (is_numeric($arg) || $arg === null) { if ($returnValue === null) { $returnValue = $arg; } else { diff --git a/tests/data/Calculation/MathTrig/PRODUCT.php b/tests/data/Calculation/MathTrig/PRODUCT.php index 0e7c4080..ab500953 100644 --- a/tests/data/Calculation/MathTrig/PRODUCT.php +++ b/tests/data/Calculation/MathTrig/PRODUCT.php @@ -48,6 +48,18 @@ return [ -6.7800000000000002, -2, ], + [ + 0, + 12.5, + null, + 2.5, + ], + [ + 0, + 12.5, + 2.5, + null, + ], ['#VALUE!', 1, 'y', 3], [6, 1, '2', 3], ]; From 2b9b42d5c64e695921e54bd0352225b221605464 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 17 May 2022 13:03:52 +0200 Subject: [PATCH 06/30] Update Change Log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60d7425..53d6c312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) +- Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) ## 1.23.0 - 2022-04-24 From c66baa44ea262af11452fb0bef54bc812edf8202 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 17 May 2022 15:31:29 +0200 Subject: [PATCH 07/30] Update Change Log --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70986645..a53df5cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add point size option for scatter charts - Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) + Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. + ### Changed - Memory and speed improvements, particularly for the Cell Collection, and the Writers. From e02f25bf48eaf9eff4088dc37142326355a81057 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 17 May 2022 15:44:24 +0200 Subject: [PATCH 08/30] Update listWorksheetNames() and listWorksheetInfo() to accept ChartSheet information --- src/PhpSpreadsheet/Reader/Xlsx.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 2068e950..ecae5ad7 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -226,7 +226,10 @@ class Xlsx extends BaseReader $worksheets = []; foreach ($relsWorkbook->Relationship as $elex) { $ele = self::getAttributes($elex); - if ((string) $ele['Type'] === "$namespace/worksheet") { + if ( + ((string) $ele['Type'] === "$namespace/worksheet") || + ((string) $ele['Type'] === "$namespace/chartsheet") + ) { $worksheets[(string) $ele['Id']] = $ele['Target']; } } From b4ba57acaad981c717e028d710e3b96a5d355047 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 17 May 2022 15:57:23 +0200 Subject: [PATCH 09/30] Unit tests for listWorksheetNames() and listWorksheetInfo() with a ChartSheet --- .../Reader/Xlsx/WorksheetInfoNamesTest.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php index ed01db25..cc2269b2 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php @@ -77,4 +77,29 @@ class WorksheetInfoNamesTest extends TestCase self::assertEquals($expected, $actual); } + + public function testListWorksheetNamesChartSheet(): void + { + $filename = 'tests/data/Reader/XLSX/ChartSheet.xlsx'; + $reader = new Xlsx(); + $actual = $reader->listWorksheetNames($filename); + + $expected = ['Sheet1', 'Chart1']; + + self::assertEquals($expected, $actual); + } + + public function testListWorksheetInfoChartSheet(): void + { + $filename = 'tests/data/Reader/XLSX/ChartSheet.xlsx'; + $reader = new Xlsx(); + $actual = $reader->listWorksheetInfo($filename); + + $chartSheetInfo = $actual[1]; + + self::assertSame('Chart1', $chartSheetInfo['worksheetName']); + self::assertSame(-1, $chartSheetInfo['lastColumnIndex']); + self::assertSame(0, $chartSheetInfo['totalRows']); + self::assertSame(0, $chartSheetInfo['totalColumns']); + } } From db4dac3de9c67d20c0e866997a763fc5bac4ba21 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 17 May 2022 16:40:14 +0200 Subject: [PATCH 10/30] Filter null values in PRODUCT() function, as they don't affect the calculation in any way, and actually require additional code handling if they're present within the calculation itself --- .../Calculation/MathTrig/Operations.php | 26 +++++++++---------- tests/data/Calculation/MathTrig/PRODUCT.php | 16 ++++++++++-- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php index 44e13526..f26da389 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -102,29 +102,27 @@ class Operations */ public static function product(...$args) { + $args = array_filter( + Functions::flattenArray($args), + function ($value) { + return $value !== null; + } + ); + // Return value - $returnValue = null; + $returnValue = (count($args) === 0) ? 0.0 : 1.0; // Loop through arguments - foreach (Functions::flattenArray($args) as $arg) { + foreach ($args as $arg) { // Is it a numeric value? - if (is_numeric($arg) || $arg === null) { - if ($returnValue === null) { - $returnValue = $arg; - } else { - $returnValue *= $arg; - } + if (is_numeric($arg)) { + $returnValue *= $arg; } else { return ExcelError::VALUE(); } } - // Return - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return (float) $returnValue; } /** diff --git a/tests/data/Calculation/MathTrig/PRODUCT.php b/tests/data/Calculation/MathTrig/PRODUCT.php index ab500953..657c713c 100644 --- a/tests/data/Calculation/MathTrig/PRODUCT.php +++ b/tests/data/Calculation/MathTrig/PRODUCT.php @@ -49,17 +49,29 @@ return [ -2, ], [ - 0, + 31.25, 12.5, null, 2.5, ], [ - 0, + 31.25, 12.5, 2.5, null, ], + [ + 12.5, + 12.5, + null, + null, + ], + [ + 0.0, + null, + null, + null, + ], ['#VALUE!', 1, 'y', 3], [6, 1, '2', 3], ]; From 9aa17084686fb20a8428f64012dc1578b16a2604 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 17 May 2022 07:48:24 -0700 Subject: [PATCH 11/30] Some Fixes for Scatter Charts (#2828) * Some Fixes for Scatter Charts Chart issues have been pouring in recently. This is a partial response to issue #2762. It implements "no joins" for scatter charts, as well as having the reader and writer handle "point size", "line width", and "color" for markers. A new boolean property `scatterLines`, with setter and getter, is added to DataSeriesValues to handle joins (default is true which means scatter plot points *are* joined by lines). Some, but not yet all, default font properties for the chart title are handled (color and, surprisingly, font name present challenges). With these changes, sample 32readwriteScatterChart1.xlsx now looks closer to its source. There are still some differences (x-axis changes), but I think this change is already large enough. I can work on the other problems later. The code for reading charts has not yet been converted to be namespace aware. Having a tiny island of aware code in a sea of unaware makes no sense to me, so some of the new code is likewise unaware. I hope to be able to get to it eventually, but, among other considerations, it is difficult to generate suitable test cases. * Add Formal Tests Essentially the same as the corresponding Samples, but with formal assertions. * Clean Up Some Code in Reader/Xlsx/Chart Having added code to support default font attributes as well as element-specific font attributes for chart captions, there was duplicated code between the default and specific sections. I hope that this PR makes the code easier to follow. * Add Support for Font Name and Color to XLSX Chart Titles XML layout for these in new files differs from what program was expecting. Not sure if program expectations were wrong, or if this is a change to Excel since initial development. * Minor Improvement Handle theoretical case where Chart title has text but no font information. * Support Bezier Curve and Scaling of X-Axis on Scatter Plot For Bezier, need to specify `` tag in addition to already supplied `` For X-Axis, scatter needs to supply both X and y axis as `` rather than `` for X. --- phpstan-baseline.neon | 100 +------ .../templates/32readwriteScatterChart6.xlsx | Bin 0 -> 31632 bytes src/PhpSpreadsheet/Chart/DataSeriesValues.php | 41 ++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 227 +++++++++++---- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 27 +- .../Writer/Xlsx/StringTable.php | 62 +++-- .../Functional/AbstractFunctional.php | 5 +- .../Writer/Xlsx/Charts32ScatterTest.php | 261 ++++++++++++++++++ .../Writer/Xlsx/Charts32XmlTest.php | 89 ++++++ 9 files changed, 615 insertions(+), 197 deletions(-) create mode 100644 samples/templates/32readwriteScatterChart6.xlsx create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 664306fd..de63f72f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1215,26 +1215,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Chart/DataSeries.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:refresh\\(\\) has parameter \\$flatten with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$dataSource \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$dataTypeValues has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$fillColor \\(array\\\\|string\\) does not accept array\\\\|string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - message: "#^Parameter \\#1 \\$angle of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setShadowAngle\\(\\) expects int, int\\|null given\\.$#" count: 1 @@ -2560,56 +2540,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - message: "#^Cannot call method getFont\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\Run\\|null\\.$#" - count: 12 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 3 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has no return type specified\\.$#" count: 1 @@ -2635,11 +2565,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$marker with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" count: 1 @@ -2715,21 +2640,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has parameter \\$background with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has parameter \\$color with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Parameter \\#1 \\$position of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend constructor expects string, bool\\|float\\|int\\|string\\|null given\\.$#" count: 1 @@ -4997,7 +4907,7 @@ parameters: - message: "#^Cannot call method getBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5007,12 +4917,12 @@ parameters: - message: "#^Cannot call method getItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - message: "#^Cannot call method getName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5022,7 +4932,7 @@ parameters: - message: "#^Cannot call method getStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5037,7 +4947,7 @@ parameters: - message: "#^Cannot call method getUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - diff --git a/samples/templates/32readwriteScatterChart6.xlsx b/samples/templates/32readwriteScatterChart6.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ddfa3048317b9efbc52b3527a87512d28970141a GIT binary patch literal 31632 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 Zb^j;rm+L1ff~ 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 zckB^x+4eW!6K9O^$?setDataType($dataType); $this->dataSource = $dataSource; @@ -100,6 +104,9 @@ class DataSeriesValues $this->dataValues = $dataValues; $this->pointMarker = $marker; $this->fillColor = $fillColor; + if (is_numeric($pointSize)) { + $this->pointSize = (int) $pointSize; + } } /** @@ -126,7 +133,7 @@ class DataSeriesValues */ public function setDataType($dataType) { - if (!in_array($dataType, self::$dataTypeValues)) { + if (!in_array($dataType, self::DATA_TYPE_VALUES)) { throw new Exception('Invalid datatype for chart data series values'); } $this->dataType = $dataType; @@ -137,7 +144,7 @@ class DataSeriesValues /** * Get Series Data Source (formula). * - * @return string + * @return ?string */ public function getDataSource() { @@ -147,7 +154,7 @@ class DataSeriesValues /** * Set Series Data Source (formula). * - * @param string $dataSource + * @param ?string $dataSource * * @return $this */ @@ -239,7 +246,7 @@ class DataSeriesValues /** * Get fill color. * - * @return string|string[] HEX color or array with HEX colors + * @return null|string|string[] HEX color or array with HEX colors */ public function getFillColor() { @@ -249,7 +256,7 @@ class DataSeriesValues /** * Set fill color for series. * - * @param string|string[] $color HEX color or array with HEX colors + * @param null|string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ @@ -260,7 +267,7 @@ class DataSeriesValues $this->validateColor($colorValue); } } else { - $this->validateColor($color); + $this->validateColor("$color"); } $this->fillColor = $color; @@ -379,7 +386,7 @@ class DataSeriesValues return $this; } - public function refresh(Worksheet $worksheet, $flatten = true): void + public function refresh(Worksheet $worksheet, bool $flatten = true): void { if ($this->dataSource !== null) { $calcEngine = Calculation::getInstance($worksheet->getParent()); @@ -421,4 +428,16 @@ class DataSeriesValues $this->pointCount = count($this->dataValues); } } + + public function getScatterLines(): bool + { + return $this->scatterLines; + } + + public function setScatterLines(bool $scatterLines): self + { + $this->scatterLines = $scatterLines; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 680335b7..4e3cd02d 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -25,7 +25,7 @@ class Chart private static function getAttribute(SimpleXMLElement $component, $name, $format) { $attributes = $component->attributes(); - if (isset($attributes[$name])) { + if (@isset($attributes[$name])) { if ($format == 'string') { return (string) $attributes[$name]; } elseif ($format == 'integer') { @@ -42,15 +42,6 @@ class Chart return null; } - private static function readColor($color, $background = false) - { - if (isset($color['rgb'])) { - return (string) $color['rgb']; - } elseif (isset($color['indexed'])) { - return Color::indexedColor($color['indexed'] - 7, $background)->getARGB(); - } - } - /** * @param string $chartName * @@ -293,6 +284,10 @@ class Chart case 'ser': $marker = null; $seriesIndex = ''; + $srgbClr = null; + $lineWidth = null; + $pointSize = null; + $noFill = false; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -307,9 +302,25 @@ class Chart case 'tx': $seriesLabel[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + break; + case 'spPr': + $ln = $seriesDetail->children($namespacesChartMeta['a'])->ln; + $lineWidth = self::getAttribute($ln, 'w', 'string'); + if (is_countable($ln->noFill) && count($ln->noFill) === 1) { + $noFill = true; + } + break; case 'marker': $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); + $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string'); + $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; + if (count($seriesDetail->spPr) === 1) { + $ln = $seriesDetail->spPr->children($namespacesChartMeta['a']); + if (count($ln->solidFill) === 1) { + $srgbClr = self::getattribute($ln->solidFill->srgbClr, 'val', 'string'); + } + } break; case 'smooth': @@ -321,30 +332,52 @@ class Chart break; case 'val': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); break; case 'xVal': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); break; case 'yVal': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); break; } } + if ($noFill) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setScatterLines(false); + } + } + if (is_numeric($lineWidth)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setLineWidth((int) $lineWidth); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setLineWidth((int) $lineWidth); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setLineWidth((int) $lineWidth); + } + } } } return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); } - private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker = null) + private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, ?string $marker = null, ?string $srgbClr = null, ?string $pointSize = null) { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->strRef->strCache)) { $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); @@ -356,7 +389,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->numRef->numCache)) { $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); $seriesValues @@ -367,7 +400,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); @@ -379,7 +412,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); @@ -474,62 +507,138 @@ class Chart { $value = new RichText(); $objText = null; + $defaultFontSize = null; + $defaultBold = null; + $defaultItalic = null; + $defaultUnderscore = null; + $defaultStrikethrough = null; + $defaultBaseline = null; + $defaultFontName = null; + $defaultColor = null; + if (isset($titleDetailPart->pPr->defRPr)) { + /** @var ?int */ + $defaultFontSize = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer'); + /** @var ?bool */ + $defaultBold = self::getAttribute($titleDetailPart->pPr->defRPr, 'b', 'boolean'); + /** @var ?bool */ + $defaultItalic = self::getAttribute($titleDetailPart->pPr->defRPr, 'i', 'boolean'); + /** @var ?string */ + $defaultUnderscore = self::getAttribute($titleDetailPart->pPr->defRPr, 'u', 'string'); + /** @var ?string */ + $defaultStrikethrough = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string'); + /** @var ?int */ + $defaultBaseline = self::getAttribute($titleDetailPart->pPr->defRPr, 'baseline', 'integer'); + if (isset($titleDetailPart->pPr->defRPr->latin)) { + /** @var ?string */ + $defaultFontName = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->solidFill->srgbClr)) { + /** @var ?string */ + $defaultColor = self::getAttribute($titleDetailPart->pPr->defRPr->solidFill->srgbClr, 'val', 'string'); + } + } foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { if (isset($titleDetailElement->t)) { $objText = $value->createTextRun((string) $titleDetailElement->t); } + if ($objText === null || $objText->getFont() === null) { + continue; + } + $fontSize = null; + $bold = null; + $italic = null; + $underscore = null; + $strikethrough = null; + $baseline = null; + $fontName = null; + $fontColor = null; if (isset($titleDetailElement->rPr)) { + // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { - $objText->getFont()->setName((string) $titleDetailElement->rPr->rFont['val']); - } - - $fontSize = (self::getAttribute($titleDetailElement->rPr, 'sz', 'integer')); - if (is_int($fontSize)) { - $objText->getFont()->setSize(floor($fontSize / 100)); - } - - $fontColor = (self::getAttribute($titleDetailElement->rPr, 'color', 'string')); - if ($fontColor !== null) { - $objText->getFont()->setColor(new Color(self::readColor($fontColor))); + $fontName = (string) $titleDetailElement->rPr->rFont['val']; + } + if (isset($titleDetailElement->rPr->latin)) { + /** @var ?string */ + $fontName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); + } + /** @var ?int */ + $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); + + // not used now, not sure it ever was, grandfathering + /** @var ?string */ + $fontColor = self::getAttribute($titleDetailElement->rPr, 'color', 'string'); + if (isset($titleDetailElement->rPr->solidFill->srgbClr)) { + /** @var ?string */ + $fontColor = self::getAttribute($titleDetailElement->rPr->solidFill->srgbClr, 'val', 'string'); } + /** @var ?bool */ $bold = self::getAttribute($titleDetailElement->rPr, 'b', 'boolean'); - if ($bold !== null) { - $objText->getFont()->setBold($bold); - } + /** @var ?bool */ $italic = self::getAttribute($titleDetailElement->rPr, 'i', 'boolean'); - if ($italic !== null) { - $objText->getFont()->setItalic($italic); - } + /** @var ?int */ $baseline = self::getAttribute($titleDetailElement->rPr, 'baseline', 'integer'); - if ($baseline !== null) { - if ($baseline > 0) { - $objText->getFont()->setSuperscript(true); - } elseif ($baseline < 0) { - $objText->getFont()->setSubscript(true); - } - } - $underscore = (self::getAttribute($titleDetailElement->rPr, 'u', 'string')); - if ($underscore !== null) { - if ($underscore == 'sng') { - $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); - } elseif ($underscore == 'dbl') { - $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); - } else { - $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); - } - } + /** @var ?string */ + $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); - $strikethrough = (self::getAttribute($titleDetailElement->rPr, 's', 'string')); - if ($strikethrough !== null) { - if ($strikethrough == 'noStrike') { - $objText->getFont()->setStrikethrough(false); - } else { - $objText->getFont()->setStrikethrough(true); - } + /** @var ?string */ + $strikethrough = self::getAttribute($titleDetailElement->rPr, 's', 'string'); + } + + $fontName = $fontName ?? $defaultFontName; + if ($fontName !== null) { + $objText->getFont()->setName($fontName); + } + + $fontSize = $fontSize ?? $defaultFontSize; + if (is_int($fontSize)) { + $objText->getFont()->setSize(floor($fontSize / 100)); + } + + $fontColor = $fontColor ?? $defaultColor; + if ($fontColor !== null) { + $objText->getFont()->setColor(new Color($fontColor)); + } + + $bold = $bold ?? $defaultBold; + if ($bold !== null) { + $objText->getFont()->setBold($bold); + } + + $italic = $italic ?? $defaultItalic; + if ($italic !== null) { + $objText->getFont()->setItalic($italic); + } + + $baseline = $baseline ?? $defaultBaseline; + if ($baseline !== null) { + if ($baseline > 0) { + $objText->getFont()->setSuperscript(true); + } elseif ($baseline < 0) { + $objText->getFont()->setSubscript(true); + } + } + + $underscore = $underscore ?? $defaultUnderscore; + if ($underscore !== null) { + if ($underscore == 'sng') { + $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + } elseif ($underscore == 'dbl') { + $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } else { + $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); + } + } + + $strikethrough = $strikethrough ?? $defaultStrikethrough; + if ($strikethrough !== null) { + if ($strikethrough == 'noStrike') { + $objText->getFont()->setStrikethrough(false); + } else { + $objText->getFont()->setStrikethrough(true); } } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index ba7a6545..f18d9216 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -306,7 +306,7 @@ 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, $xAxis); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis, ($chartType === DataSeries::TYPE_SCATTERCHART) ? 'c:valAx' : 'c:catAx'); } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); @@ -367,9 +367,9 @@ class Chart extends WriterPart * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void + private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis, string $element = 'c:catAx'): void { - $objWriter->startElement('c:catAx'); + $objWriter->startElement($element); if ($id1 > 0) { $objWriter->startElement('c:axId'); @@ -1016,7 +1016,7 @@ class Chart extends WriterPart * @param bool $valIsMultiLevelSeries Is value set a multi-series set * @param string $plotGroupingType Type of grouping for multi-series values */ - private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void + private function writePlotGroup(?DataSeries $plotGroup, string $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void { if ($plotGroup === null) { return; @@ -1104,7 +1104,7 @@ class Chart extends WriterPart } // Formatting for the points - if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART)) { + if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()))) { $plotLineWidth = 12700; if ($plotSeriesValues) { $plotLineWidth = $plotSeriesValues->getLineWidth(); @@ -1113,7 +1113,7 @@ class Chart extends WriterPart $objWriter->startElement('c:spPr'); $objWriter->startElement('a:ln'); $objWriter->writeAttribute('w', $plotLineWidth); - if ($groupType == DataSeries::TYPE_STOCKCHART) { + if ($groupType == DataSeries::TYPE_STOCKCHART || $groupType === DataSeries::TYPE_SCATTERCHART) { $objWriter->startElement('a:noFill'); $objWriter->endElement(); } elseif ($plotLabel) { @@ -1142,6 +1142,16 @@ class Chart extends WriterPart $objWriter->startElement('c:size'); $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize()); $objWriter->endElement(); + $fillColor = $plotSeriesValues->getFillColor(); + if (is_string($fillColor) && $fillColor !== '') { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $fillColor); + $objWriter->endElement(); // srgbClr + $objWriter->endElement(); // solidFill + $objWriter->endElement(); // spPr + } } $objWriter->endElement(); @@ -1192,6 +1202,11 @@ class Chart extends WriterPart $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num'); $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') { + $objWriter->startElement('c:smooth'); + $objWriter->writeAttribute('val', '1'); + $objWriter->endElement(); + } } if ($groupType === DataSeries::TYPE_BUBBLECHART) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index a64e0d68..6808f33e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -215,36 +215,48 @@ class StringTable extends WriterPart foreach ($elements as $element) { // r $objWriter->startElement($prefix . 'r'); + if ($element->getFont() !== null) { + // rPr + $objWriter->startElement($prefix . 'rPr'); + $size = $element->getFont()->getSize(); + if (is_numeric($size)) { + $objWriter->writeAttribute('sz', (string) (int) ($size * 100)); + } - // rPr - $objWriter->startElement($prefix . 'rPr'); + // Bold + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); + // Italic + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); + // Underline + $underlineType = $element->getFont()->getUnderline(); + switch ($underlineType) { + case 'single': + $underlineType = 'sng'; - // Bold - $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); - // Italic - $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); - // Underline - $underlineType = $element->getFont()->getUnderline(); - switch ($underlineType) { - case 'single': - $underlineType = 'sng'; + break; + case 'double': + $underlineType = 'dbl'; - break; - case 'double': - $underlineType = 'dbl'; + break; + } + $objWriter->writeAttribute('u', $underlineType); + // Strikethrough + $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); - break; + // Color + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . 'srgbClr'); + $objWriter->writeAttribute('val', $element->getFont()->getColor()->getRGB()); + $objWriter->endElement(); // srgbClr + $objWriter->endElement(); // solidFill + + // fontName + $objWriter->startElement($prefix . 'latin'); + $objWriter->writeAttribute('typeface', $element->getFont()->getName()); + $objWriter->endElement(); + + $objWriter->endElement(); } - $objWriter->writeAttribute('u', $underlineType); - // Strikethrough - $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); - - // rFont - $objWriter->startElement($prefix . 'latin'); - $objWriter->writeAttribute('typeface', $element->getFont()->getName()); - $objWriter->endElement(); - - $objWriter->endElement(); // t $objWriter->startElement($prefix . 't'); diff --git a/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php b/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php index da821532..60351d71 100644 --- a/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php +++ b/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php @@ -19,10 +19,13 @@ abstract class AbstractFunctional extends TestCase * * @return Spreadsheet */ - protected function writeAndReload(Spreadsheet $spreadsheet, $format, ?callable $readerCustomizer = null) + protected function writeAndReload(Spreadsheet $spreadsheet, $format, ?callable $readerCustomizer = null, ?callable $writerCustomizer = null) { $filename = File::temporaryFilename(); $writer = IOFactory::createWriter($spreadsheet, $format); + if ($writerCustomizer) { + $writerCustomizer($writer); + } $writer->save($filename); $reader = IOFactory::createReader($format); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php new file mode 100644 index 00000000..dc7fbc0b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php @@ -0,0 +1,261 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testScatter1(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - No Join and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter6(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart6.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - Rich Text Title No Join and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(3, $elements); + + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $run = $elements[1]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Courier New', $font->getName()); + self::assertEquals(10, $font->getSize()); + self::assertFalse($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('single', $font->getUnderline()); + self::assertSame('00B0F0', $font->getColor()->getRGB()); + + $run = $elements[2]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter3(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart3.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - Join Straight Lines and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php new file mode 100644 index 00000000..75a3946c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php @@ -0,0 +1,89 @@ +outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + /** + * @dataProvider providerScatterCharts + */ + public function testBezierCount(int $expectedCount, string $inputFile): void + { + $file = self::DIRECTORY . $inputFile; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart2.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(1, substr_count($data, '')); + self::assertSame($expectedCount, substr_count($data, '')); + } + } + + public function providerScatterCharts(): array + { + return [ + 'no line' => [0, '32readwriteScatterChart1.xlsx'], + 'smooth line (Bezier)' => [3, '32readwriteScatterChart2.xlsx'], + 'straight line' => [0, '32readwriteScatterChart3.xlsx'], + ]; + } + + public function testAreaPercentageNoCat(): void + { + $file = self::DIRECTORY . '32readwriteAreaPercentageChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(0, substr_count($data, '')); + } + } +} From 9d36e442e97ec53bbf1c4f32e044684e14d20649 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 21 May 2022 06:46:02 -0700 Subject: [PATCH 12/30] Disable URLImageTest (#2843) This test has always been problematic in that it depends on an external site not under the control of PhpSpreadsheet, and can therefore break at any time. As it has done this morning. Disable it until a better test is available. --- tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php index 3b515090..3eb2ba2c 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php @@ -11,9 +11,12 @@ class URLImageTest extends TestCase { public function testURLImageSource(): void { - if (getenv('SKIP_URL_IMAGE_TEST') === '1') { - self::markTestSkipped('Skipped due to setting of environment variable'); + if (getenv('RUN_URL_IMAGE_TEST') !== '1') { + self::markTestSkipped('Skipped due to no longer bein able to access external URL'); } + //if (getenv('SKIP_URL_IMAGE_TEST') === '1') { + // self::markTestSkipped('Skipped due to setting of environment variable'); + //} $filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.xlsx'); self::assertNotFalse($filename); $reader = IOFactory::createReader('Xlsx'); From ef031e74e1cdb7489eef567c44c1757d9c08664c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 21 May 2022 07:01:18 -0700 Subject: [PATCH 13/30] More Chart Fixes (#2841) * More Chart Fixes Taking up where #2828 left off. Most of the following changes are demonstrated in 32readwriteChartWithImages1: - Adds support for "scheme" colors (because rgb, theme, and index colors just weren't enough for Excel) for DataSeriesValues. See issue #2299. - For chart titles (including axis labels), rather than a font name, Excel supplies a 3-fold series of font names for Latin, East Asian, and Complex Scripts. New properties `latin`, `eastAsian`, and `complexScript` are added to the Font class. I frankly have no idea how, or even if, you can set these in Excel; my test case (sample 32readwriteScatterChart7) is a result of manually editing the XML. - Add support for subscript/superscript to chart titles. This requires a new property `baseLine` in Font (positive=superscript negative=subscript baseline value says how high/low). - Support for underscore with different scheme color than its text, using a new string property `uSchemeClr` in Font. - Support for extra options for strikethrough, using a new string property `strikeType` in Font. - Support for extra options for underscore type, using the existing string property `underline` in Font. - I do not anticipate that any of the new Font properties will be used except for chart titles. - If no default font overrides are found for a Rich Text element in chart titles, and no explicit font overrides are found for a Run under such an element, the font element of the Run is set to null. - PhpSpreadsheet will always write a tag `a:pPr` and, underneath that, an empty tag `a:defRPr`, for default font settings for chart titles and axis labels. Combined with the previous bullet item, this will prevent PhpSpreadsheet from inadvertently overriding the Excel defaults (18 point bold Calibri for chart title, 10 point bold Calibri for axis labels). - Axis labels will now be written to XML in the same manner as chart titles. Among other considerations, this means that they can now have colors. Fix #2700. Supersedes PR #2701. Demonstrated in sample 32readwriteStockChart5. * Fix Some Chart Corruption Fix #2817, where @bridgeplayr gives an excellent description of the problem and how it should be solved. * Fix Bubble Charts Sample produced corrupt output - see issue #2763. After a lot of research, solution was just re-ordering of parameters in a single function call. Bubble 3D had not been supported at all. It is now. Surface Charts remain corrupted. --- phpstan-baseline.neon | 4 +- samples/Chart/33_Chart_create_scatter2.php | 121 ++++++++++++ .../templates/32readwriteScatterChart7.xlsx | Bin 0 -> 31611 bytes samples/templates/32readwriteStockChart5.xlsx | Bin 0 -> 31410 bytes src/PhpSpreadsheet/Chart/DataSeriesValues.php | 30 +++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 123 +++++++++++- src/PhpSpreadsheet/Style/Font.php | 181 ++++++++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 71 +++++-- .../Writer/Xlsx/StringTable.php | 38 +++- .../Xlsx/Charts32ColoredAxisLabelTest.php | 80 ++++++++ .../Writer/Xlsx/Charts32ScatterTest.php | 82 +++++++- 11 files changed, 691 insertions(+), 39 deletions(-) create mode 100644 samples/Chart/33_Chart_create_scatter2.php create mode 100644 samples/templates/32readwriteScatterChart7.xlsx create mode 100644 samples/templates/32readwriteStockChart5.xlsx create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ColoredAxisLabelTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index de63f72f..eabedcc8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4792,7 +4792,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 44 + count: 43 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - @@ -4982,7 +4982,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 5 + count: 4 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php new file mode 100644 index 00000000..59d6bd3b --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -0,0 +1,121 @@ +getActiveSheet(); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] +); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode('yyyy-mm-dd'); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 +]; +// Set the X-Axis Labels +// changed from STRING to NUMBER +// added 2 additional x-axis values associated with each of the 3 metrics +// added FORMATE_CODE_NUMBER +$xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), +]; + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties('yyyy-mm-dd'); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + //DataSeries::STYLE_LINEMARKER // plotStyle + DataSeries::STYLE_MARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Trend Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('N20'); +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$spreadsheet->disconnectWorksheets(); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteScatterChart7.xlsx b/samples/templates/32readwriteScatterChart7.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6c01bf400127cd8c3d4af3f260ba53de37ac070c GIT binary patch literal 31611 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 QTQ%T~5&Vh6`Tp$x0rdzZfdBvi literal 0 HcmV?d00001 diff --git a/samples/templates/32readwriteStockChart5.xlsx b/samples/templates/32readwriteStockChart5.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..364803f2eef1c9b64e92ab904e9ac0154d041f8e GIT binary patch literal 31410 zcmeFY1y@{6v@O~+)_CKt!Gl9^2<{eKgS!(nxH~iw+}+(JxVyVM1b0Hhd z1}KClfUzQm%23lUsMW46V-@QM#n&`zac1-#NlcM4!wPQ=EDKz7F{QF| zcfJl6TKMV96({TRMjAK6n&p*@8Xp>D-@%8ObxBvcOlb?f6 z1en4-zC!InC2sauyC9hL#m9}MAa0{{tzTd|4ZbqR9SiDGToU2%W8Y^z&7Zu@43B}x z6TV+V>o8Y+&Nln9X(Y4KZ{va~eevCMVj|xPpihLLrl$5(;tWyoVp}-DLtpegl|uIp zx39)5Z%6>Z+Z!}M>Hh=#Mm3fXckioN?j6@i@4(l0GPU`}%=AzBe*yo0Fv|biPp^oV zSL|a!4!M+i4IO@5*nogVWZi_N+R0UY{G`{>8l&?m2{*cEh`_1@fzXn^oj$LlYa9I0 zr^DnAyBrmf7+8ECn%pWwl3yHM;OVKIk|Z1}cKXp>7w#7xQYB?PXk9yF>C0P6a%D%h zK1j@6i`9asm^26wFbj!7a0F8QwFl+3HjSRDpcX__&MQM|nz?dL6QlsW0d8@OW~n!V$*#qmfnT=$BR)RV_2Dj1LKKp zN^*!Q=iXPlf#Z5C)1#jYp|5=SY9t_%v}E($2LA6LIm$>}{0I#IAirBjwD*y5w_$d( zcd|CJx3~VsG8U+<+2yl3yR8CZGJ14I&~wcL)8y*l$QPd z9n(rFU$1SdleoN05;HZPQ9I>pQ?Ke-lxM>K2o_|Ilei5VV$ky z@%=bB>3Hbrck?X@uA=N@HV0r>7L7_7r^+z#iwL`N)^)&PP2DcV2aFIS+r;1;N+wJh z(fQDLUmFo)7rvg^8FlLQLvdLOQ*B3sAy}t)Z%y{+D}~T~&AYc;F$}s#pJ>=ID3%3329LB~{pY}gZ{N63?C(q0q4 zIoM#`bNs{3?vRH0F1&*8=~>dkH)l_)$GaJa)8o$RY}C+PTSLz~xXCboLPzD10{O~- z2MCG>zjj)JZF7Kpv(a_dVEKw*+v#VHjn#A~O=2UrD7(>4(7e*xW;<)M#qlY$>B=b1Ey^n)C(2?F~ltnBsOG*nm44sLIL`JrF-b&P?x zJ8-6L=|=ksDh_7S!lhyn3f`mL3Z6Bx_U5wloRVCm7r(VWM8(G3cH!i*DNKq$n6;!T z#HGfPXEea>z?ANc#CfYk8`H_>Q>ocWe;= zKv3_v`Vaf_-_iA7wg~E7e7^VnKRb0KD9ZM;fCnxEw#dgfiYnW}Y}(+(m&>b2$qMF* zY|<0QpNh9Om1lLIHY?9<|fgM?nogv}a3v8LzWFj!;YVf`^*A1%k1&HA#it`uaE}<0q$fY8Wlz8m}ENv!59R z(*xnqObw4@)!kt(w9xTycrp!_rq}<-(C7{=XfG({0wm;3D|Sir&(m5Lfy|yX3pE@u#d40NC;pH%YrLYe z0zdcK$bu*; zb`N$1$xqcd=yG0~XiZl?xko$52ZS;27W^)X^q{Nl%cVEUr9a-H^i^OV#kuwI6Qs1+ z@b$zvbTl2Orp;neAd*|@c3qj;i=?7b?XZ4*r@8>$C6dt3k<=;X7S3NUNV^d-SYX8W zqzw?}41^NJMy|@YgRb`YJ20w(jMlO~JYnx(Y-Z)Ndw62m4v^8XS?#eX;jjVNHP z6R{o(+!=V^k-pF0+uOfIhW3cvo&@+0ws0|wqr@-jM*KQ}%6c*n43rZNKO1$jv;n@t z&Ev@-g=?2lq_{rvUTqVT`jP~3)fRK|KEIjUboH4&Z)PWL;`(;-lYHfHEGf-Sa%xpVt3v$4ZHHDKyJvVm`z7Ohgc`gU zs2@ZLDgc>)_CZ*nM35?I9?Cvu7mig*w^b84af>a zkXm2EP$`%K52MrCg!k4{B}wU%D-rq~@L>zFdli>5Zryi-zY4a*D#LJ!i_tA|ip$Y) za*DHAO7dDta#~6XT1v87O7d7rezKJGoW_AdzthBd!-W*yke+x{l3Ky@K` zfcoHlscLG*%X@S{0%krt&ym>N98v4|D z?g)kc(~>`?N}!R&(RP04IC0sZ29>8YQ1Hy*PG)oON&yMpdu2z^z4`PC(7CyXT77AL zPnP!LgEp(OZpHA=kO1j`_&^>YERYI_4QzzcbfoR@uuY}*)#go)k;w27bT$=`jO`Di zZYAfnE>VXgMC*-1r=*dBQkK3I=B5vrr#AX+YsR|6c)T=RXwkeT0otvE&te*E!v?O@I6+SS8boBp+SQCp`p@+{lR zS(`-BEm#&55Qf;?UpiJMAJ7t}r3*xjcW8OopKPvTSNLp1wiq0hZv|gGs`b$Gq*bH4 z6Id31B;>BOP`qAcEC#=&hjtMv*!vUa_VA;?)D{L|hcx#6Ar%XA_o}B-rL-?Jr-Kdl zjJw|j@aYst8!aJJvUY&e0UJp{zl^Z}^9E-YmeOa^Qmisnr}bL2Bw z?$*uhI%&zYhIX{OTC?s3R8AcVu^E4=)W!u}b$tKSG^Ib?fyHX? z!8o^E3EdHkVK5!H928|nd|$&L{tMhr-VDFrE~hn;6_@IPAIc;+j=d0nCW)1{TX>I(u78^wozru?KNNckg0 zJ}3H`{cVxmOiY=ZB$bRvJP{RnKE`{VEnqweYc-JN6bU%%LdU2nOnH#yv6cN~E9$q9Z#I?IG-xc$qm?InCKkvj2sQW>DII? zYT|gb)qFH@v~`4@+`S2f+C;t6TFWxGq-I`6Cr)r)m^MK76&{t7@ouzP&+v9$i>wD{k|##(}!9vSXub+b|RbA3aaM7;0MN(X4ZtoL1YEuvf^=cfE!P7%nxbii)IPTGQ^;>X? zP$^h8i)j|i`%srxYP~tAc~Yb=#I&GzCEBO84i&le=w#n>va>GgH<>eNk*;gG#N z_wTAY5eMjLJd`dF?J)L{ zx?oDp1{A7dcPy%pIF$&J#KaZ4*Ydn_i8N`GYAyBReIYJD$+s>dunZ!LXMs1obQ>5~ z*N&GdQh$YqL5ripGH=X<>y?y3G{riA+eJrhR~>U%+iT2o>tIq?<&kEx#itdXuR)xHQbudMYc&z4 zh6C(W4Z{;Gm_HbAGR(&pm4%JI_obA|tPOBCh_>f-z^Mmd=259}QB|+dP&Fx&u8V4Y z-OD^@Tl|jVb3Uj2jZyY{S4lyuee7UxjFK?bex7cft<}BX;}l>s->j$jz)#?!IU`)T z1UqomdQ9ka8VL=rCw-2yTa+2OFP!V)YWa2}htV~`!umcKd%9A`wdR&^k!U88cqxg# z>O&%>-n2Oj%NbnDLoq96RRa$@Fs(;$=;!C_%<22>rTBxE~Gu-HNsQY5|wAwDUvlqf~6h35%_bNbMi=6ykZjZY#zvu`FZeviPd=!xWX@GAJ@n^;PyK`d}msZadHtw&Zi( zSmpTRzaPn+sM;IJOa)mUqOMnMtZp_wWGZo;tKq%D{7()Pki(CP`8{$+@ZS3WBYWBY zmA&yJcD*d%kjo!0s7aUo2j0ODclIL?veCn1T`SIyp^16Tj+}eq6U#as9gfPnft&9@1Nmj~dZ%NUYuv^+aBmy{`a z(hkOL&-B+_gfjg-0%(uo92xtA>F)nm?4*E8=;A-T;D42E9o?jqJfovDgY|4oEal?cEp}has5OHTtSEnpI06Ic+F|deO z@{wN=?-oU8IE8?B@qY=L=~l6Z^^YOC(z1~em4o8?u72*MTgXQfE4lWY-xFD!<$?F#O+l;ogemDY$e;R@BJ zwi+@~_oxGxtT?RA`&Ox%03CG3I~YNzp9ngd3_p@^G34d|h1^h0?SBMO5Kb+UdRD_c zB71Dh3Vgt9jyYPGp}hv4lBUrR(G?RQv9UUuK_Eps_|jsgf-djEea5L>xxJS4&>kQszo%WPUav-S%MKt4$tnF&I9xyb zm5xh-ABoJ9XQExJEQy}!XSc7V%%OJvOv$2?7J~|OUGfjQR~gIJx}V3}D8swuDo--T zoh!PoS8yiRV=7M%(W_1a1B6QB7T-C77g24Szg-whW!K9yd%F-6m-FZh_YlyIXFeJ0d zo1Ho)#S^(0QIe7h@x<*l42-rSDns0EH>cz<@*7d0pv1p)iIRBL1!yVKAdPu7Hcdm; zvRXUw06o7wUVIP7;~ED6r+5Tif)7)!IZ0?Uo-|z@dL!Vrdg&h!JXo*{(U;;8`H9g7 zzm!WB+|B$#oNnH}^TaDlT{>U0NokSFJ&ub#!a3{+Ti=$Pz@tgkFiRU0|3&NpLmn|h zq9ORi>BcU$6-es9JGX&G>$vXiL*AnJL9#&F@0bgE=la05cf*UQPLTR1eLWLKaZQyd z%A>fXF_MD;U{`VaJ5lO5+L@g_vq(pk zm)ui`LQpj8d?&S+z27omfMJqv+l9v2(Ec`QRu6{++n*2XyI>e@^%qyFD_R&gYn-J{ zJihj%pUHpSi4liflCk7EsUI#c*M30J6cR#I$ z;l8&0OmD#C65`tD;6aezTD)dKMCG5mR;}hGS*y!DcQ;c@nqc>yWLWR2X;;MIgE-P%cvaHf3*pghyal-TATG?@w$IQ4OVw}>YVLIN-VP+u7u4B!} z^y7kDeQVi|Xp16a2UWuI3VHszznY)ixtplHSU)^X!hK}!tI--m{l;NY?YVlfDEXoJ zxR7~4oGrw@cU@BZ(vtK?bb>@JTVdE@A6#ITX4YPQuhplC2V(X1u!M*)H8TdTo6UB2 zOOB#Ucj=?^RA&=QRvxVgg;U|mHcb?zu=?-Qp-_yv)cEdtrS>y;bDSgxl+)CH^EY)!d_Uk}K;mDv?IZ^5t6KM=Gn$c1KiBbyyB9nHakUR(WWeJ>e?Sjjt_rq&~kOrQy(x(|1iRPn^K+m zjS$t8{CyRgJVP2W%DHGBnn`s$7txdzWetdGB6sd*;U6gOjv@?8lYiJuHuTaw!86uZ z@JWt#K`g0DjqBO6>I?hTBvHr2U-9HpoDw3>Qsq%Wx z`B;SaDXAA^>)mJJt8yb&XxM)lR{9{`Ot zvOHL;z;J{w)(;ZACzyQkxDs4#bu@mm&T7{I0xoV6W@o2 z#Hr3cF_mn8?90>^r^0RjO2jKX^pHctv%iTDew+VDEvv3@hOeXhqr%iQqWEI;tfCX~ z=?!is)rJg@sh_+tcZ>p52V*tOmhZHjW9Z#pP9BQ5S~w+M{D^zrnA=VkC5bC#4xTNR zM>ub#S-4(`tP`sl6*)!Lw_jg6CGgQ$)u!EHpyScA77yMt_BGZN;)ccC9c;XMJVkfa zA#eQA+Mv;mj0`=r?}OlvJVsKz#r?{v9V-wJ-F?v9m)FF*-Q~;&c*}A-bs(H+!{O6- z71inr!v2X}E-8rXRY`faQxC&ixRn>p`3g1OhVyYd<34jnotb8DVy9y(IEHoGLKPG{ zx-CTO6rH-uY!`uqM+;47(Rk47bT5YhFk2ys9CiC-yCHTldTsz~Rc?a4f#~VUEqCqs zC-sNdM|1ef>esJps1yT3_l@O$44hI740O}Zg9*);n(Rta@976TvLv5dw=Co15Ko+i zk{ch|{BvRT8$R(5d}hYYHX9q`zbpD{bShrV@L+arbTTkAVAMKp?sKgFN+9s@?W)AN znhZ+n=p$>jKuR`%LrPI3zc#liE}Z-$JgBgbve=TcJlnIMJElYNed8*Wf? zf`udg(e?1-Q)Xt9{{zslL< zhiKND_m++KjRdU!gp`G$lc|Y{vy-Kr`M=1rnUR%+W(5Pb+qZo|t!uv)=_%Ujv}s9T zK3ZIo^Ib^9OHasoy?TD=lxiD#4)I`_tLzHI&4vt%|1xd2C;&7#I&eSS+?#qjet@m* z>3Is99wQ}dT|6=;MvqjEoF5B+b|}YMK?ukd=LaN>>&W#uAk38N^Kw{ZUK_!W3?%#^S?A9DrK?~oQS!{{vv8+crJ1F4J1Cj9<9)~TYF$3+ zdB^-ej*;CSiC*+O?Hs*x&VTxqCQgQK{|pcFe}4E^_wQ@y#iny(c~{TB`MqXAA;ScO zsHRe?{wQ;8Sp5rFNI)e!C}^M4vuw5^8IGR#v%T&aGjd-py7b+X z+NAF{FjxcXMpMya;m!^A<;t9A3_^o_@^SVi3;UQo;dtTZAo5I<>IS|Qodo9C6!kMw zgNoI#xBe{gSy;y|^qFDvVcrKsp9sD#jD08(x60T+gNrd?7UdCQV=H4fNTj3>g|=iG z38TE}seiTBTGjWqyD*?!45jX2^!MmQO#Z!l%3utAdy5C>2=>lT@@JaDwR*x^uVCGhT%=vi2K{ zkpur8i61JHs5J2xs%!wZ|mQt=*np?cvaxpaWB#1k6?+bqhfOx@7HR4qyX0CuSL&bkaj3)_!4AHhJMqq3CuR&&RC6#gCk$C z9^_Lb%#l=z!S}2So$JQo+}w5ES~QB_SC070+yL71tq{MpWR!_#Kw_dba3;47o0~Dc z`LthsM=Q9Lf2=W||Kv8EKbHCIeNT&uo12h6m#&{u)cwJoZvHzASAkI6n*=1v=j6>| z%>I9bunYWOj1yP@07&@%(wy--&Aso2{#*Tr+peW!zs8B>Lt68;jo{(JFmi)k8>y)f zVkEux$rAoobdrS*l6V=`0q@6`EjRZBQyRIAO5z%FkMM!9ogE(1DHp+iG zR^ICXl`U(-v>J~5^6-7Q-UbfmAK95*9oP z)SKF2M(ZbuBLqh!{HCx))6FLQl0cH@vIM6)^r)i(%om@o{3!Q9zzZRqQ{orCY*O}Y z#$>I4BZg8Zx!{KY;{X`399?s9%StNY>`*j@L#NQLP8eXuJ)Pe3^k4c%W>{tXO6+6k*5sMI9*BvKK6EFxQ35v z|F{0jvOJUvoSG4F2{y;-rgQnJZr2VoaO|SaYwjpF6U|o)v39VSJ|Vz&8sAkUL1Wnl z94+gpShK0L9qQDa`Cadzmx{u~Kb{XieQ^8xah1LTKXOP$p#i0Dj!ey(9GHIy1}WoW zpp#54<6D$7>V+1SDlbmh5XM*?zRwhUl{r<^@q7gt7p&hRM?zVU^R}v+9k?qN-_VmM z(A$9N-1NkC3U*M%W}J}41S=-nz11sN1CkYuka!{oxuLHP8?NHqm$+(nr#ojV{zf^i zNK>ES;N|2!D{wZ>_tGlr!tM)%C02Leh42I z@S4KF8;?Nc`c$~xn-XUowZSm|$=5eZkKpfTv2~w(5e5!?22t`vNS@kXz-U}h-D-XS z8*(Xm1s$asUDn^ITp@})aCMk?W1GR(FmErk@A|;UX`WsR&wV08Ljhh;GQZR+TBPM8 zJ9+#?TAaYvEge!@29Z9ZemTOoo#fi^LS)%Z4Tr>vJb|7K&{jY z{A#7fpYEQ$Yv*=6N*=_w&a8%3TmD zlpq~QA~(h^z&{6VFE9t)adJl~P(pwE=h*O=A-O56Rkpn_l*Y<#0&UXB^0}uo>L8RR zsT5(iX8Xc@7$d0tMaRe8C^L<0{p=Tr!a3Ac5)AJ+lVPw4{Hp_l?xhpJVtUz&Ke5NR;`cQy;709&Gu7^m7X{L+02c zR%p-mIJS;aVi$Y#ae;cpC-6D{+@3|T;Ktp;T%8dIX3NBvQyxlXsj5=xS|=%@@k{o1 zi4_qY&wK(n;bmBCkuAT6wS3`b`wM0HAE*UEx0Ug~iPT47(80guY4IKMf@!^ei58igXY28WTQHXU%*NqA|zrKc>%jL0yWSg|E z7l>RQ9e7a^?N1Zxjiu_kL1%zk(Cq5hOLdcUB1k8kd?cZjcwO%Z{2}gYxYwX6>N~VP z93L1`P%@k9T~=2q z^K6hb1S=d+>E1#@LFuE`a~cMFe?Qg4s1Y0~e{h>b z6_XY9&`|Rm@<^?8YBbe`6;Z&se+H<&o+)a~b^SNf@P0bx-$V|eJu;G5#svWW$^RM9 zwKX-dG-S54H8eM6Wiq!kla!X=<}uWRQUW~w`w$omrlX^i)M1k}WszB#_4>=(9<< zfMt}R8HCR~gWodlv-OY9&hflnnOt@`TvkPb9z{G}Row1f+&=Am<{=VBeo|I((zdBW z_F2MqDN+t0>Mq$5-xCzPQ-xfzlzbzt?TaMsONH$!g>7nt9CL-8a>bkqMV#|QT#7`T z%EesDL|se8U8_Z$tHnG@L_I4+-7Cdh8pPacq`&j#622BJR8KkYa~2dMBG~> zyxYXQJ4JneihDPzy7!29_lbG+$VL`__N(I#X%~!c;*G2L9FV3RmF5zVBN9+85?rnx z&@2^MD;d(P5mzS)X_88=mxgqTB)3W=bV#Q*iectzS8HR4Hv#C3XBu>bQF5#Fxwo)wD6~gg(R6 zQS*Wk(fl#x+)0)EDYfD;jlxOgqA9iVDfQfGk-}+(ycw;s8I7VTyV7yv+Ib&8Kfl=6 z*qF4Cl#Iyi+^For`0SkQlD3rU>gvj-{Ho@L`i83dj?%WD&0SqxjlKR|OHrMZReh6f zQ~Py;2^j3-eRcvlBD(lS_+}Gs~09t22{xGgFHz zQw!6xQ!_KOGjp@k(+hLcOEa_cvvUhGvy0R3#r)#@{LJFw?EJ#g?99sS-16+~+T8rg z{M^d?!uz$lG{3g6xV*TuvaqlMIhmhWvKlwAvKZni$4F5pkA4~v1Z-CzjL{c?5J%J!hdfnlg+`dro2bm11+Pr~C zJa((~;oAJ6STfNFBI&wuJrathvV{W8)+KF{ zX7Z)X2PleS$!FBc6PJm983eC|*x#0Zz3G7ODW%yT;tg88vxf2%uCCF4E>qd+=SyZ&%~Pl+zu)!zsXVP)Kl5g6o!%DeA&Zob6RPy>6cCPluxVX!ssAbmKJy*wv zZ&TmFJ1rs7UtvU`TAm}KMq7Rkzv&(c;nE@Vo9-DR{l0wNk6Sx_v=bWtWpIQlvE`8X z+uuyX5IhzO;U(euS|E}uLfPSl^$sJ&nup(2tt7D%48j2BrN6~~+~+Tc2;mtFOY8Vb zX}IW)8VMNQ2uR23DMH55+N>;zrH<;{k+R+HvYUD0LllO);}auv>JyA55gnm)SPi1MeW&Tfo>3F%x)y4s!JY5 z{q1w*p1{3`He`Dr#oDHKZ^l@<7?9bqSXG2!xR;dCnK8B*pqNn_iEgJG#*wcSftXR1 zJqQy{;w^|{JT!O)+{m^4r%uM#LfsTn*vW*Lzb#A1w$hm~g)4GI2*+}6Cq%Jn86UgR z=Z(UaFk|Gbz$=yxmZ;u$3lBh8VlZ=#;mU2Mf}qBYGGu+I*4z?BKK**5|Zl=%<}!FzxHv8^1)Pellh|tB6Mif^7Y``%CsAd@8xo&?Ha9+;uysp{i&OPyhna#AL=plvo2~ zkL`5|cL?D9JOf7e*M~zh;ecPpI7YP4iT&NZIK1P!V25sm@7GaQCdS0nloC`jxyi~; zSD`|$xfE|eLqcKIFf<|h9L)Icb{IBe?@fno_=BQIgwtq>NtI!&QVR$poI()7%vV_4 zx>!RKC?tbn5$u+3SiBZ=I8r|$WS1W?mMnoJcPssb9Owv*EF$QB$duaj*C6TfTr>$e zMU1^G7-~vGc%tL(%h|YS3n@o~kjU=FL+YL&B?n8*{_R^((un+MWdiUzZ=C`#coJ7b zZVTvv=g;v+;x~Y5(G>z>0sLW<$negGB{(HCAeb*bsNOly#J^aA%@l+YrxODZMwJjg z-}$3GP^b_&7sF5;lf~O84ZgHEj@Z!kllXqn1VBO|iZ~33&|^8Y=m83?l?h~JsstZ( zEZ@Ma0T@ZzAPXco8KnT_Zk&;lA^xA&oPo!Ic=@0}F)D&!7O|Z?yn7%4*3Jh$h*f&g zX%P}9nY3d_U&QHu zM%=DrSeB$Ou{J?_1P4-K>=9p(4SwyGX-=Fn%`1iQ|LQ^U0L^j|mcmd*sX}-04M~rM zSB7jS8vNprlH0MI{4$t8ZcmKI2So$Oc)qXp8(7));syKuRH4}R8{+NWL3P;==FhQ8E+69`~_4KVN)T;9iYVp@t3Z0DRMKFVoX#YaDDsg zNWm!}WrxnD6R^-k>X$%gNNA+FLr+P#N;|~aBphQ~f=0CcG{~8Ut`P=&l({3>ucAp(RI<>=uSo=ngS05hbMkVQzzOBx4gi-L%%nD34yJ6Eu$tc!XtA z3jG+jt586=cs%&2A`ZpgIf-z2A%u;nH_AdxlyJ4CCOCaZ@9G8$?Hif>hZ)ZxLK6V= zxwpO7x%N^>0j3V{YTx#`Nq=LJo1A%ZQ2}1gAPmo)VjnLQlH0yvo0w0uIxR(5Q4@+b zB^(~Z-;1X1u#44~TnTUwV|rPOZ9R4S`P#`U(!@Wt{Z|4{Bww5{2wfwbCI8KVS?60i z?$BN~av$POW^clt0X>G|3Xs;7Ii&U#Z0N8u#sOC!jhzV=O>Uh8*M`FUB?KTKKaj=S zSvFC!mm;$9G?0IX(CBs7HPWH5zbnFK^y@o}l$P?^InyN7At3_u@i05HlO(Ug5n z1IUgX`LF@UVGbTceS%>%IyPnz4Cw~H`>SVVHqf+l2m;m-T}z;7I)h)iY5T}}&XWcz zD0AFEqDuZ%%{5WZcWKk89za}Uu!TzN&F6~B#+3AV^r4f*Q24UXvtMFJ1_Z+e$iuLH2y09s78mF@XbYs*yz?D zMYoE#%iyPq^i6=OfDW|1t9X;%rGE(#V1+46psKQ`0at8?)XM{pTpsM*Z-klh6htf1 z^Z4^t08{)%s^t~{&vV?U6yf9lk7PR%=Y@Y*IK3;k->u`E&-{;RJBAbD#b; zMC%sM*#Xvu8lvgkgnppJ@kxpMt7x40>(|dmB6K@dGI*74n7(3n0SI(dve%e~%ES`d zjjK!1kc!g>{WM$$rcX}KK@KoOt}GJZH5_BO9yF3LM)Yp}HYNQtTptW>ut||()xH%X zI#MhEsgw+9k@W*d09oo4T*neRa9C=C()4qVGf$4+@R2)8m_4o(xVPJt1wH@^#Erx4 z5(FJgkfQiy*jpyS)?D%%)37bV7bHeCpKC5IqNnfQFoSE%!5p-o>mZ1rJVa-y(3)-p z$TnO7AV?houVmoYAL=K$429YaK*sZcfpuQhVdJzbgoXWepx2K!bw^JYog> zy%#^if^w1shcpHQdpr^~e6=LO_8_EcKhM@?Dx^A)vk;<4D7u>jY_YPsvRP1|W|&S6 z^655;Y>J2MH}4K1BomqdmW43vVWU0^?*aEHJu(04hX6fySE_j*=Or*)Xw)>kEBs6d zEILxmHewkVu?*s=56zk5fCw>wufy|G7lM@&f{}$pBDe=}35A7}AYTKbG&#kALblMt zt}0Jq03P#1DSg5Q+bY};r@m;gaD+f3*kK8>kQ5C>M~V{)tG6Rd$-gPb6lMN75{6K@#LImh`@;NYK240UT&|HvMRB*yr4eA$j z$ifFeHFa=yv5Wk%w=JD6mPH)AFob6zdJ7jlL=7tRmd#`+ku3$Rh=7<1Ktxvlq347k z+Y8dNL|z_1PIf`>H-HuMgAp?Dl9Wo~ssR!>B>fshwH}FDxMfGrO>nXdMtu@48Sx4( zMPJqw$a4=DE(PxlsRUX2!aJsfpCA|};P;*&WPuUlJPaqcnUlg((?(LWT7fxqX=6fZ zKOe&UgW0VflI|pY$(y}joHJLRV@=V@i%e$q)qjIwFTU{Hz=V zNa>H&ZlwiN^yJo5Aux>bbLw;|)a(dqpjiTGZdw>^6vP7Es2Cjsfm>KQ7%MlqtRcOa z8@$U+(a)2eUW@P508-BRF$?DXt%cq%1pLGT!w-hx+fD_R0b$Aj`5rm3DLMGx^NUi^ z(bCZ8X+tW*v%IAJ!F1qXrMdAe2=9k#NLLHux+Ozc@&>_r^TFtJZF&4xc~hmFY-Ior z-b{^<9Fp7|qwiT2TId21=*cAPNxt)v?6HEBA@C}rN~3fo9NW+wS9!qELXw&ieF8MYvV3CUqHcl$a~X|z z0(8nJVK$fWRS)paQU&UlGKCZPH~_+~W96r@$>XE%Xau-RyGoQM${2^>kyk!xRAyt4Bc_9b z9AK%oE4RR?i{O%d8EAGtXp``g^V6y;`s(wC^2avx2dkpz(H#D^YBbjLX-?k~4)Bgf z0jhtZ(_BFEYU0zIIVci9j@Vuw-VM@#z-)d;oxeh%kwLFx0rL1kGdZxeomLS~*ZxXE zC6le=mMJ!ksHP!I#}#(9Q4e78^s!k5kLTAxV*-c_YkUkqp6lp)R7S%}fBzko7X2o0hkY7hW@yT2;p5YK3 zbc^z)Dok2C9gtrQDDVo_kOg_b0C}ti87mjuNhD%3AUE02lgN_4*35;^hNGL>ou);|Ic6ObgLa9XxW-DFAqM9 z1+jnyaq~?UahLDs{_oEG8o1;1pN8A8AHYByog{*@hKAn&`0ZM_h~_@}y3nzpftQkR zt!dEEWqDTK#)IJ!h}>tPr8Ju0jw|UZ-0y^UN&JC~jAOy-AHf9Up?d> z;Jppt9h7<}u;gb%Z>wNNZ^p3GE zs14nk21eP^K_ ztAlT7B5}+yceo1OXn?u5h!v=T<^w~s*StGyK!#~P-i|&2-L62LOkv&X-_~`3QT@-V z1tD(>klUqz6hr|OgdP=(#MxCiY6sAFC6K%spN7rd~Owwu!ExYJh|A+KZV zB<8fL+OuX~q!t1Q6g~h7P6^N%hWgjN@x`+z^ff^|YWTUp)o-sOzom$!xd`F~Hb_@Y zTu)4jf)5dbckdp_@tP>)A1xj*FA8w)4}}pW3zIB?z9Q`69q(&EhJy7Q+r4wYi$E8( z84lMe9er{IYIIw02aL+`OK4@GiPhkY55O9ZsQQ8Cb>Tso)1YX0$Lf@qM%EyM|0V=)I520_C@(}j z1LyqpI2cCUcII1DtW@PpZAV{&ml2*H6e3_@lnhPv57^#}gUk?Z-VHs$cKqWf?=(Gj zJ3W50mgr__*E9m(;-c^6>K4T)Bd+yeHh5!TDVmi5tTP$B>h$!!*(lkjrmCVF49WmUw{$;IIJW+?@Zo^B^rpCBof!22DTDX=9|1>qY#q7 zo(6^xQX8Q1nyU;&7+KE%r~;U8pIB0yp4ix5DszD;XIR#jnGCL+X;_6)&PFPW)q41874P^=c;zzQ z6#&5j0Q0_Na!^konka&M-Hzte(=finFbaM!V$(1&V)H#HFd~!C{6f%VB)j;w%Y-%i zfGj8+2Pgsn)V(YK=1TSkg1Rmw$MN&P)KP>I0H+5&l)Zx07XsQN4_WxYh}0ZflfVd* z!PqIhdq!KVZM6&z%V8)71P(y(^zxYvpcVZ7YSgo7L0z9?66UKK7KDK(Wp@~z)?dhl@t6V^)&-1G@F(+QBp zuY_`P|JVzv?dMrLljKh(No4S^Ucu9^1l8Bt+h=kI*waY2U=+vz%HHbfEE8tL1(3+$ zd}$L(HftGY`W)v9fVvH6O_UwDCy=`;5Rl-5 z{jUTC0SOI+0q?^DKJ5FiJ>c&GA)%n}7XV1$?GPYH5C8yl&%7;?U=1zjp~(*mu}uT6VdWcCHJ=^EkZa7ZlKq z(tg+n++uTU(X%_)-kaA30N;(xHERzj5hN?Ol#5|-V&v*^RKq6p&F(9;UT+#8bLG2Ir%{Cq!CzMy8_b2jia-B_r@(LrQ=g@MPgXK_S7p@6?kWZhh~ zt=k?-lyQftIZ-ZLG~%|7%RV~?3rGm6<79fheRmvFkZ+fxLfk&etCEFt}(%A!Yoh62^Bt`@!EqP=QE*F-0YoH<%7 zNFVmPZl3=~Wh1*M-&(bM4J&JBbzfs*vbI}2l(e;l4SZ(^U$Uu-U1dU2-)G$#-_{s; z0EAD=x*qrJXoz}+sg_-QEUIuw;)2JO_8!mAObvSBFM}7I0LxQOEpemO!7RSg=Tp17 z2hv0ySTeQWpSknlW{jzqSzWRTvVcn+1o5%s$$m+6ujeUsOaOHu*1`*9wzKDdjw)o1=Ch5%P4kDK-6=3=@6&Ll6?fA z1Sc>Ab%QLl%tXM1vqR-G{LL9yFe9l!03aj;48$WXA;25ZF`+6E=bLN-MJx(_=uhf`UQu?o`%Bk24zW$4zRQOMF&U>60<6_qOUs&UdhaTZ1qF$B7EOZ z2~v$_<+P^bnr2XPxQHSDCL%%a{=C`7bq4)tfFh``-8LvCL`UZBjwF?@?%E`OLWkKa zrDv4up1N;KRB){VJQlLM;uz?EVI1i2^|VY#FkC{EIeVVDTUvHYRseM+%QrQhN;Yyq z@HOk?D#0|J3fZs(CEZC(M6?=_G%Sbs*pt=d=PGpJ>auxa8mxozdIiDzripV$6lTn2 z0-=@PBD|g>eYVhVNBfc9HM3xHr?57fE-#FJa%+>zx}~2zfviw_U_R0RZ41KAmIfkG zgsMUmz>^Q;v>i;B^0$jS^W*X3O%>FL$N3yN+%|3Ysmz?2`#SEN3D=KzXiThNCwJU_ z;A&oGw9(||E483HonkLrY+1gRGZ~}N9ha&*X=1RxbR~rrOx}!2G)ivf8!(&P5q}??(I#Lz4zVV@1axVN=0mS9Ky~F%gCOO&IWZgn!a>eNmh+fziw z4Cv%>h!IPmF?ewRO1wc?4ZIYe6NJ~$P^Wu8XdrpSzdDU-e1TRSjv1=#kE47K=$n!F zX&|rEH`Du5-c3(6c*cISJ;fU~Co0kItGr+?*FLw?U~>fisP-%)=n7SOUh?P}%T>@w z$?^bsfg;i59u{%qA$UXAS-uBh)TC&xGPx@?s_`Yw-GG&u7Q2;!x}gduC$|Jd^8p6q*e`*K1!6?I74n|KJQe1ui_}uvg6Y>78!1`RLRO9J(I9tl53KJ7)VVJ z0TRlStZyP1$Y{bfGBwH4_Q&d{F`Uf{?)uP56zbvfHZ{V^Ekq_wJ@`s6DuWiS2zE`M zV*?_~a=(b#eT-`lu}i3o3>uqAx215%O*cD8bYPG}(~b;UG-3D%KJta?MQ#HgIyyS* zkmI+$F!umM(_|~1LW`s{O{?cycShiXq3g&V-v@uSs`#*yJP{X|?f3qF`WcEmn>+I@W*-kEayu40v1Y+tZErKW?>F zhtXDtVcs)ng~lSFWd)SHw-KwSv?X09Bvy+;3N)HbLYUG(02G->5_3So?%7k))v|cMl+G z4tZt{gCB!H)R~7GCqf#uF3x4)G%$B;jlDI;P=uA@L(g&}0$d}btK|+?u|j8rr5E>9 zT2f=@>Df89#=RvCm?vWI6$6Tw_)~JsXb2w7_~fE3<#OB8re>&|3r%yAMvs^HRw@iz zbnQtjnJvL*WLq_5C=4v%i9gGf{(Cep;ibOhGd(h%2TK zOdL88ScMS^8Up6gkpV)08vqhgHc=;)07!H)7BOd&-1R@q0RmuzA^eC#lbFVAXCD^% znKAiGi?=^m?=s5u5J?S3{Vm8UPU6mlqy}#>@lb1s=fez6s$vtHOl1lt%oCOsh4kO~ zFoXs9jHtNL;2+5JjU)=5RPBiHUE;QJa*z*KOAwn3GwJ6Zht5_sS*XxaSm=yp5zjeC zq?+lsCc3AE+w-vtd=V8Y>eE@j3uWdS*J9Ft6?-;7<4KsiR-hP@>ne)GJNY?U-JLCH z;w1+@RD9lZG7XMrXwp%_H78!{Wm31lK1py?B9Lv5yrWaqz?a{ro@sLanoK{oeq^C9 zUNY#(o*|GrG~{M^@oPAaE={RxUxTC6Lhp)mLVqk$-=s-pN3+lpheBX1&XI|Qw~uK$ zEHt(Ed1qtiR#;QAWPhIyDtkn#3W14Krbi!wp2gRl1!fZ`x=DPMLVT4X3_98=hRGy_ zF>o4QjsGzJr_Y9XsJqbruDM_z4~>)+B&rg3V&arb5>T~%%F^}kK3kYnCdg{cA@}=> zAFRHE?H5L7%a%h z9QF!JLGq@Yr`b5U>uh#pZ)8OFEeT}HNc9M4ozbopIM>95Z!OsjbxgiPUfjrF=*>Cz zRahfJyrn-1qh#utK-TkAy}ab-&l6Z{wH<*?+9N5NV*DI~_cJp7r8AzgIYO;QP2xbE zs2MYELkrYb3yN1OiGoruRHBuxXl|^Z2fz77dK26>$F#X69iRm7pVvuM zyR?vp{l2m}a*s#aL4H^VJGMkp6heJbnb9s@`9eHcvTqLi+IZI2ovDDjz%@NnXgj!O zG~C=cql_O;Cem#e%WBElC3agrA3dT)CFUq75y6xWY%mjUQ>4(?k|DokiNTw?P){q- zxarEdWJ4#CONJ@Slz?O-Mk5_)UF&IfrszU5|23i-Z8x4%oetC;9i~Mgt2z1>LOy}9 z%K%=_EMD7}>0&*rYGrE@8mB7r=U&!J{U^};qxOT&mA!W#zW1z>ZrWx4<{Gg1_} zohc=oR1((x(I>toAAKhSHhszD-gOKv_PDaWI1gFauv-t#<(R0#?y-aBK<`g zS+cCdPpAlwQ({8X?H)b2T==;vxGiwnZPw@_>@u`c&}1LG$BIK*rhF3xJ^ncboVgfm14y9g0)da@HMm6|T?Al5Y1&M|TnbFQ_Q`WhAn?clZT zL4dw!thYy^XvtQa*}6ogr@%4bKhLK$MBKIAX0FVdk;%CT;oCGDc0K0Qylf8{BxQ`b z!uN?^U{kC~LDSPBmk{^uuREKdSc3QPkXBT`oP3#5t3nC7aG z%TIHR;X5VS>6?-XAjNSKm|Ekaa>}`ryn$mwwNJu<1MQ9;^yfK#;gno-K4XVe(ax7i zQ)jPy7GklQ8*GQ%78EkEJOZsB;$XD50C@99fZBd$`j}jU&FBt)jIhEt??2p$o4)&C7 zI%-={!4&1yKu3~ z*FvoD8K%q%rF^)H>{RArwdcrzI3DQg?_;u5@eA|(d~mklfev9PIC=brL=;_5ObB!)*^E*NS?8YayrI{;YE9w z(7B7znkpWO*5$#(KrLFP6Pc(zXR1~x)4g(2bGwz7ioxdOBKA@OBqnCBY83?&y{~{3 z{5rK59aO_$I)s2x(n@qmi9M%L%oW`WV@_ehCKKM2uw*6kHO8}QjUuP6A=t*DTSYfh zWud2_>KIWwH~G!%$}{41sirT_zJXL^38)6bT7y6|-(Pyi?MpRbSgKSfz_^~9WQq9% zb)#nJ#3N%8fX8oenTI1xBSXC;!vnvt<+_l?tJ;RzhQPd8N?(emqV|sfGlK>Mc3OcD zFhFou_@`n60pOlc6!builZT#ga-HS$u`2{k9DQ+ihPnsjrhlUHSIC~Vyz~e@5>yA%eeDgia55J4XwMRHMdrZ+|15zz?%Rj8PUaz{N?~s3;jG+k6iQ3(ZR7oeRhUwgD^;)*A+&_6`u_K`RyAA2gPc+rbla?~^G`NsegFHP!_pE}S z5}ZOevUi1imwcxe6;sj2pN-=E;LlryM!obKW$`N$u2V}Zfy#v|rqb$F)y|$?RJ*ji zzAEBWEOoU@rqPO!>^g6qf_qreUg?8g?q}&XWncQgH0xH2Z9nW3ziNkyBRg?Y;Xmq?*Z}Af-q4w zh87Jg!u!;hP#-Rfen)00PEcA-CNDG{jnNTtK|UiidRYRj zk@V`b+K_C3@Y{CX48pBTq+pYAWl(PyWVjG=S=r&NkTYz)4tZ?{n=!-CuPd@PjSOj= zQqHEP6i@N<0#`N1c`hazE5#-$T`^%8Yc$Db**&yvruh_K*7{XV`Aw!&@4gVpJnMK4 z5&^Z2YBQ@84a96k6>B^H=nGt$7}i{fUt1GaL_;pP76xR>qF+v7VH|e-eYD~_i9jmj znApK@Bk!Q?Zx)Ev>u_2%<$2mq9mK{>S@PW?hhLQ50|J27I7m`L(C^gcsT2Fo*SA!& zL>k9L>PWsR)@<{|Bu>Uvj8(^dXnu83>RoCYw*&hPAXZEzw;;Ue??MX9A$jwwuy~kq zA;$1yaA%+|U(HcR@=NcNv-z(bO38!tP*^X#cq!Xe*?dI8!hm^U{6{&)nYA_@IFdv$ zt*fh{37>erOSd!kvSnkO$+pa=VtbCqRC@WHd-xH_F&+Ji^)cGus2yKP&N9Iso=^ON zEI|}n?8%6#c*@masVUU)s-**!DpA5!Rxz@-6OoI$F)Q9{F!&OYR*mT%aMnFMhTLXD zWVZV0{U@vSPO(t%@I0ypx^~3Ws(HI~y8>?-xUt zQAKbEX7R&bQ$48NqTo6#6`{6l15qJeEO;zW&aOuJUCm@0R55w7Byd?p%|E~O4e-BX z2;`Mq0I9?&B51Bz7M&jMAH3UcQoWUNz=8nex@coJvhKu&<`yAuN+@jwz|U0_ZuEAl z9`?w_VThbF$>-4^UW&4+?uD?{9C~~lAkP-rEWq;IremjWGBLPTn2>zGqCr389WiW= z#VGC#Y10f~@te6Ew9~Se8k%0K-&rHbglXVE1TkAhwiZOx#GH>|?{T!uoKab^9hk0I zM%AP+d&Ita^i8f32A z{8}Wwrl#pjzuvA)9kd_qh?(GGl9lostb$2CgnxN5tM{Dw}A zx*?U{*AMI`=^c*0L3=g62;MS9Kc>8^U`)12J(wQ9XRZ6HEBYmlurhEQ8pbR85Xrcb zNy3}@Su2V)+!^Ow&B_wGxYw`y9~CO}G`pfMmaj;6saJGjwt&C(=C5DaQ~GYmr~Epk zpKZ#TQ1G6>EnT|GZZ3QAVPUslv8}_5=WYL)CsqsgM@G>=)nh*hrtx3Zn*LzV;O{!G z@cwUjdBF-Y^(6oJEZT2YebZfZUgQG#9?RbcY~Qd}OYc@zCB~Hu9?sb7FAmlE21-l6 zK!!koyPcB%)fRzBE6Zg&;54P!@Apv#q|fC3X~WpV_vAZY{t6}9tKs=h)N9C9a)uq5 zVQk3z<=fiX%QpQN$;QKYcsJY>*G8!RKi5f)2ILsF<`D*ZAxRmI2O#}vx8ECm_<9;| z#;wz$)5~YjqAiudH<|#yH?NV3%RP+*c6^1hi&;}oJTuX# zqOjj6Gju`RvKed#*YemRy2`ys?tk;uhw0ih>vPn{7b+wSwx35dkh`SelPSJID;%Rw z2N2)5aZ0o5r2xCFkH<%I)@+hvDK_2&@Y9{wGz#a%!@=Z{jZ?g|J4_NB-oV4gInY3! zG3X+*q*d6C0?R200Olv-0|i^v&P*~9;!%I{*xJia{M3hp%Jgc8E;ll%GO5WPA&?z? z(+w#7JUeo1ja8H~g%{17Xh)YYQkwpCzt6z5q=6%HCRm=5^81GF9kx{~A<^2eNNR3s zi1x@I9Pcb;NquWn$;&Wue^HqEz5hBhLMq6l7*s64b<(95YFa3KG_sz$bsCIQN^)Yo z@@enI?J)m_)`!-xGeVFb-~|N(*0tzcbodU$;59MpJDwn-K5i_BGO0fN6WucxH+IrQ z>3G|Km(QCB;ol(V*x%?{+iuG;RwUI^1;h;8aWt)Xy^9D6t3byHZ3Mlrzkrns z+?vHpe5D0R?53BA4cFt_Urbwwz_%u90kv1^WVF7>z}_)rL0Hd@WsOu%k6gDOgd2#$ z^0A0bu(`mpA(?*#vE{C9v+bi6Q#0Fqm1mS%*uI=KQjJ~EYT-#OJh!IuEPH~O4J{|U zUAi?pyapAgegb}yjKY@!3-fq2YIDc!8gDmiVaeigQ`dbV$>J4LR|Lc*+Of~0wMxSN*?Glp(WLHJ zDfDPa39=vp&1g@tkWeTdShc8EW1qb)G!yl5jzf5@d4XDU_->hM2(IK?NUtFlOTI$c z1=wQp6k;|?9zL6PAgd7J+~*P0Hg2AD^(%38{sP|(@u}pCQ&*{k3%RRbX|HpB6{MWV zX#ba;qSsMDH@AQ6fHzi9ot&pw{>qWS(`A3Slf}W4*+D!SW*}$}waQRJ()Nw!K z03H%lNMicFCrlOk(K%C|fwgeQDJNV$#GJe@1ano)1wUCkD^a0Y!H@iA!E-7af7z;L z(W*{(-j2w1wpF=YY%*dy_!%}5vVn&7MD$Y6n*~^<7ot3vt+8u}@N}xq_HURe;H1c& zwHXCK#Zh%B0Q|Agztv|FGzqo@EyF5#noNhszJcs1N>E2YL5fRg1OL}UbvB;!jfGU% z6+lWNoO6_=D z=%Sl04slSjUS+5bZ?U+>tE3XvX9^fngqB=7Lg(B$I?q`NrV!>W_kK{-qf0m*A1@Y) z!L^(G?w?MW$4uV8^-|KsfZpJ&jJ%qJ;WkN~zB*R2JK=f`8@qycMmuQRfu|-&6;GIa zWBe3@?1e_#hbxH0j8iYJ{H^jWDn^71_4oDiq~QXxctdVcI7T?|&L1L_ivyZNmg@(+ zOG*daQtNavVe$y}_G{!6iA)i&Y|Sy5R425#r6Lz}hNDmG6q@KUFbwimbn)AT*6ZU~ z*%ADl+zgZ6xF}9bXartHX^)cW&Qu3FO|bPZronKUsx5H`d#W9E)YRXb-A+)j*aZk% z=O1ufyUwr3rFzi7nS7c+d_DFmO~yg3mf)t|8Wx$a9mW_a#3ZfCmCE*$8|SN)j}sDg z#$g&yhL`G&AI&-?+}o*FNMiRH1gP(Kw1ldcp@Q2X%qId>V#3@Stqp!Y5Au~;w$n)4 zjf~!XlUk*Xu!xgB*>L_!@olA;vFY1WQ@twKIyz^-JY_Oz(GkR@WZPF*PEK7LapJce z0V!3$?@3NV!RRSf$RsW3D4&AQy|>=++`GVjEvC3FqG`ZtaR@paBl1QazRYKh(*m8s z6tcURddMNFh#YDjE2>2wZS0k?eX~_wnkOsRmXn?N!Qnqrvdv(VLNTUg`cltui<}@H zv9($Tq&zNBwW7Bx)NRqe?8~56N*~AeO0ocoZUU-_0(T)qjfMc@vuv>W8g#(2d0h2U zdRKo9>KLC=UDGvxJM5U}ofbCg=)r#FdXG?1K}xzRwvTO_s-O_4?am2nZ@Ys4=^p%W z_uso(Ue>y}MQ|rP0S?BBPZ;;A>0za2+xNIlV6p*&Z|ug&#-kzl&xIZXYHG`4wGtunG_Ot|Mlt0fE$0o z+a+_TkQjSdvh}M4UU*e;f#~pc^r|?miovd8bUZ;ksT3&n~Q3O;*f=Y~`~QHlB*9JDLC1IQq*=~Bh}33r_9!-|8x04(MW zT8hHeWAb_1)jP82jED$PQD%M+p(_WIHZ=2(;&g9Np(;ylZzqDHzA?X{kzLlS+y$Ye z`9d;$N=#^%ialV}ZmJ*_9!BUDTi>_RdM-Wrg4IDb6a$aYTYkuXEVXZuZ(7+KNUn-! z;{7cLten$`^MyQ7F2$huR;vkOtGC~a$X?hI`i{ZgrZKFUN3E}Ncv&r9BuL)B!}luE z*IMLJLdcS1UfdHBbadN41z#2YJ3FWj_d{F2`9t~0j(^Ud|A`$_S0rQ#UTC>mld#%T zhp6>3gOZaC+-uK8PdL7z{1{VQ7D(1vqCMNn3{k}<8Pr)-u%B?`CFbS_WlXs7cuV1D z^-iQ*z89K|q$NJ(fyPeV(C$f5oult_FM!thF2cm$7K2zM&@T#It3@cS@w`goGy%91 zLA#(RZrg(1dyD6M=3jsOV`!>taZ!xvE=<|IH-!zfDIyb?c#Pap7=NpyT%>8VJ&O2G z=l)|oNDdx+-n-zOq5jiqID*A+T+N-;%w1i7%f3AM%*yn_%Kn?09Ju8G`PQfVy(Hy* zGX%X4GwoZ_=2&GarAo3`t0@X%Aa60az%;l<;*}VLcYeK6h!^aR;g~a_kPGRsB26xc z92g>=Ck9UpmWfuR4~Oq#tcpQ{SOPVV@^% z-2Dk-{0r6yQ`Z^we6DJuyoI=JDW7qTS2~e*X@gSM-HW7qRsRae)%)SuHp&zErA5!z zoHtz=X3CyHxx7|zN*s*KHJjvl2?)#uCekr|0j1>qH(60<3Tg7o*emlXQ)omAo`lWZH8*Pu$RWCEyHuZY;r5r6K)tb3$Yik`bi3L9Nvp`v^ z`}V6m(r@41iMV5rxYV1*AO5PbsX!HsRB#YjUvda4RQODc?{d+*Sk~%|JxM znRPRjKX+{JP_HqsDQdvgpN4F4At9(xjI&5ort7ioxtd*@D*Wm=v@&C-p=XjvDUb=( zwBmB6uD)enV1Aha)M96joJF%FWx{z5>bCYbJiHe$I;R{Wks(!yrHJi~m6j%x7L$gM z{z*Tie|YRZg5A#V@V((!fAy8os@sRFwH8#Z+fIYg*5N2Y7xPvLN=+ZKSaD!p7q_L!=kR*^|-$lNU%8YA>jB5;zC)EfP zkKBfn4v~bDsKjE%27!MZU0PMzS=y&{_V!iN%`f;bE6Kh+E~YqU68I5eybvxDqwy(} zulgyQzQivbq=26Se1()SV0IRig7HGJGXqpZX7QX)r2ykOpG>$g<*&pWwT(|T`T?J!wVHw6?{()bLjqUV@Rc`eVM{(&ng>v zPM)oW#S)ZvIt@J=KxQfTBI6@1+>QGjN~`-C6Bj`_C$rC0;xy(YobMAk9i{pngXt&X&s| z1Vht{qAY)T8kbhAElviTQuVx3tUP=_Jn`k1kE}c!Zfgoy)BT_ZD@s!hUc@hx?G7Zd zgkNuigBV3=as^7SL-IT_I4+E)*%p#fv){F(1=Kaaw>d5+Z7=k57WzMaKXgIX4 z%DN2k&D*(lx+%U#ld=4i#njXQFD7T}T4XuEN<5w5opMqNv>w;=P6Bs5jbU z3V9u2{2^Hl15I3JA+Fxz^UZZXpjXVh;Dk}yJl2u3_sz|OZxAjWOR&GeQFinQUEGN* zJ7o%TC$hU;7JuDfy451%zM(tL^>sbff<+ zq<^pfVe(OBxxWJZb=tyz8$PUW!8PHJJNw&(eQNl0Dz`^dBJc&ue??3Gp4jcF@n7ew zdNc(9+`)sY!Nz}n_;0gUJ;ixCThSv@C(?fo;%{>oJw3k z6y>S9_ah1+cpNnt<*C;9Q-G%urH=q9q`v_k1xufrK9yp8G(DjBWBOE>@hQSnp~6Q5 zRI1+y|8e2}JGgm@@>DDB5oL<@H_G3X)BYelj--Dosq=_%2#&_Ux%gCI=PAI`q4LPW&Un(+d!f2t$^?5guyC zzqtLUx;(iY@$|%=M(vM)+b{nYz{e=aRKV=lK4cIse$EJdGS50U>Sv z7r_7d2%m;ykAU}o0G|Y7%uhc54$hvQ_S2~35mdqM_fh^OMtN%eSEu*U764!X>kt0T m1%7J&S3l*y%^RKm%luzni?SRv%)@tZz%OR-J3H5hPyY`;By7(B literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index a4f8f61d..cf3e0853 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -73,6 +73,9 @@ class DataSeriesValues */ private $fillColor; + /** @var string */ + private $schemeClr = ''; + /** * Line Width. * @@ -83,6 +86,9 @@ class DataSeriesValues /** @var bool */ private $scatterLines = true; + /** @var bool */ + private $bubble3D = false; + /** * Create a new DataSeriesValues object. * @@ -440,4 +446,28 @@ class DataSeriesValues return $this; } + + public function getBubble3D(): bool + { + return $this->bubble3D; + } + + public function setBubble3D(bool $bubble3D): self + { + $this->bubble3D = $bubble3D; + + return $this; + } + + public function getSchemeClr(): string + { + return $this->schemeClr; + } + + public function setSchemeClr(string $schemeClr): self + { + $this->schemeClr = $schemeClr; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 4e3cd02d..4480c6ff 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -288,6 +288,8 @@ class Chart $lineWidth = null; $pointSize = null; $noFill = false; + $schemeClr = ''; + $bubble3D = false; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -304,11 +306,16 @@ class Chart break; case 'spPr': - $ln = $seriesDetail->children($namespacesChartMeta['a'])->ln; + $children = $seriesDetail->children($namespacesChartMeta['a']); + $ln = $children->ln; $lineWidth = self::getAttribute($ln, 'w', 'string'); if (is_countable($ln->noFill) && count($ln->noFill) === 1) { $noFill = true; } + $sf = $children->solidFill->schemeClr; + if ($sf) { + $schemeClr = self::getAttribute($sf, 'val', 'string'); + } break; case 'marker': @@ -342,6 +349,10 @@ class Chart case 'yVal': $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); + break; + case 'bubble3D': + $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean'); + break; } } @@ -367,6 +378,28 @@ class Chart $seriesValues[$seriesIndex]->setLineWidth((int) $lineWidth); } } + if ($schemeClr) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setSchemeClr($schemeClr); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setSchemeClr($schemeClr); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setSchemeClr($schemeClr); + } + } + if ($bubble3D) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setBubble3D($bubble3D); + } + } } } @@ -514,6 +547,9 @@ class Chart $defaultStrikethrough = null; $defaultBaseline = null; $defaultFontName = null; + $defaultLatin = null; + $defaultEastAsian = null; + $defaultComplexScript = null; $defaultColor = null; if (isset($titleDetailPart->pPr->defRPr)) { /** @var ?int */ @@ -528,9 +564,20 @@ class Chart $defaultStrikethrough = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string'); /** @var ?int */ $defaultBaseline = self::getAttribute($titleDetailPart->pPr->defRPr, 'baseline', 'integer'); + if (isset($titleDetailPart->defRPr->rFont['val'])) { + $defaultFontName = (string) $titleDetailPart->defRPr->rFont['val']; + } if (isset($titleDetailPart->pPr->defRPr->latin)) { /** @var ?string */ - $defaultFontName = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + $defaultLatin = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->ea)) { + /** @var ?string */ + $defaultEastAsian = self::getAttribute($titleDetailPart->pPr->defRPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->cs)) { + /** @var ?string */ + $defaultComplexScript = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string'); } if (isset($titleDetailPart->pPr->defRPr->solidFill->srgbClr)) { /** @var ?string */ @@ -538,12 +585,18 @@ class Chart } } foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { - if (isset($titleDetailElement->t)) { - $objText = $value->createTextRun((string) $titleDetailElement->t); - } - if ($objText === null || $objText->getFont() === null) { + if ( + (string) $titleDetailElementKey !== 'r' + || !isset($titleDetailElement->t) + ) { continue; } + $objText = $value->createTextRun((string) $titleDetailElement->t); + if ($objText->getFont() === null) { + // @codeCoverageIgnoreStart + continue; + // @codeCoverageIgnoreEnd + } $fontSize = null; $bold = null; $italic = null; @@ -551,15 +604,29 @@ class Chart $strikethrough = null; $baseline = null; $fontName = null; + $latinName = null; + $eastAsian = null; + $complexScript = null; $fontColor = null; + $uSchemeClr = null; if (isset($titleDetailElement->rPr)) { // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { + // @codeCoverageIgnoreStart $fontName = (string) $titleDetailElement->rPr->rFont['val']; + // @codeCoverageIgnoreEnd } if (isset($titleDetailElement->rPr->latin)) { /** @var ?string */ - $fontName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); + $latinName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->ea)) { + /** @var ?string */ + $eastAsian = self::getAttribute($titleDetailElement->rPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->cs)) { + /** @var ?string */ + $complexScript = self::getAttribute($titleDetailElement->rPr->cs, 'typeface', 'string'); } /** @var ?int */ $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); @@ -583,43 +650,72 @@ class Chart /** @var ?string */ $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); + if (isset($titleDetailElement->rPr->uFill->solidFill->schemeClr)) { + /** @var ?string */ + $uSchemeClr = self::getAttribute($titleDetailElement->rPr->uFill->solidFill->schemeClr, 'val', 'string'); + } /** @var ?string */ - $strikethrough = self::getAttribute($titleDetailElement->rPr, 's', 'string'); + $strikethrough = self::getAttribute($titleDetailElement->rPr, 'strike', 'string'); } + $fontFound = false; + $latinName = $latinName ?? $defaultLatin; + if ($latinName !== null) { + $objText->getFont()->setLatin($latinName); + $fontFound = true; + } + $eastAsian = $eastAsian ?? $defaultEastAsian; + if ($eastAsian !== null) { + $objText->getFont()->setEastAsian($eastAsian); + $fontFound = true; + } + $complexScript = $complexScript ?? $defaultComplexScript; + if ($complexScript !== null) { + $objText->getFont()->setComplexScript($complexScript); + $fontFound = true; + } $fontName = $fontName ?? $defaultFontName; if ($fontName !== null) { + // @codeCoverageIgnoreStart $objText->getFont()->setName($fontName); + $fontFound = true; + // @codeCoverageIgnoreEnd } $fontSize = $fontSize ?? $defaultFontSize; if (is_int($fontSize)) { $objText->getFont()->setSize(floor($fontSize / 100)); + $fontFound = true; } $fontColor = $fontColor ?? $defaultColor; if ($fontColor !== null) { $objText->getFont()->setColor(new Color($fontColor)); + $fontFound = true; } $bold = $bold ?? $defaultBold; if ($bold !== null) { $objText->getFont()->setBold($bold); + $fontFound = true; } $italic = $italic ?? $defaultItalic; if ($italic !== null) { $objText->getFont()->setItalic($italic); + $fontFound = true; } $baseline = $baseline ?? $defaultBaseline; if ($baseline !== null) { + $objText->getFont()->setBaseLine($baseline); if ($baseline > 0) { $objText->getFont()->setSuperscript(true); } elseif ($baseline < 0) { $objText->getFont()->setSubscript(true); } + $fontFound = true; } $underscore = $underscore ?? $defaultUnderscore; @@ -628,18 +724,29 @@ class Chart $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); } elseif ($underscore == 'dbl') { $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } elseif ($underscore !== '') { + $objText->getFont()->setUnderline($underscore); } else { $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); } + $fontFound = true; + if ($uSchemeClr) { + $objText->getFont()->setUSchemeClr($uSchemeClr); + } } $strikethrough = $strikethrough ?? $defaultStrikethrough; if ($strikethrough !== null) { + $objText->getFont()->setStrikeType($strikethrough); if ($strikethrough == 'noStrike') { $objText->getFont()->setStrikethrough(false); } else { $objText->getFont()->setStrikethrough(true); } + $fontFound = true; + } + if ($fontFound === false) { + $objText->setFont(null); } } diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 13fe2b67..e5b056c9 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -18,6 +18,29 @@ class Font extends Supervisor */ protected $name = 'Calibri'; + /** + * The following 6 are used only for chart titles, I think. + * + *@var string + */ + private $latin = ''; + + /** @var string */ + private $eastAsian = ''; + + /** @var string */ + private $complexScript = ''; + + /** @var int */ + private $baseLine = 0; + + /** @var string */ + private $strikeType = ''; + + /** @var string */ + private $uSchemeClr = ''; + // end of chart title items + /** * Font Size. * @@ -170,6 +193,15 @@ class Font extends Supervisor if (isset($styleArray['name'])) { $this->setName($styleArray['name']); } + if (isset($styleArray['latin'])) { + $this->setLatin($styleArray['latin']); + } + if (isset($styleArray['eastAsian'])) { + $this->setEastAsian($styleArray['eastAsian']); + } + if (isset($styleArray['complexScript'])) { + $this->setComplexScript($styleArray['complexScript']); + } if (isset($styleArray['bold'])) { $this->setBold($styleArray['bold']); } @@ -213,6 +245,33 @@ class Font extends Supervisor return $this->name; } + public function getLatin(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getLatin(); + } + + return $this->latin; + } + + public function getEastAsian(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getEastAsian(); + } + + return $this->eastAsian; + } + + public function getComplexScript(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getComplexScript(); + } + + return $this->complexScript; + } + /** * Set Name. * @@ -235,6 +294,51 @@ class Font extends Supervisor return $this; } + public function setLatin(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['latin' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->latin = $fontname; + } + + return $this; + } + + public function setEastAsian(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['eastAsian' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->eastAsian = $fontname; + } + + return $this; + } + + public function setComplexScript(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['complexScript' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->complexScript = $fontname; + } + + return $this; + } + /** * Get Size. * @@ -418,6 +522,69 @@ class Font extends Supervisor return $this; } + public function getBaseLine(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBaseLine(); + } + + return $this->baseLine; + } + + public function setBaseLine(int $baseLine): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['baseLine' => $baseLine]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->baseLine = $baseLine; + } + + return $this; + } + + public function getStrikeType(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikeType(); + } + + return $this->strikeType; + } + + public function setStrikeType(string $strikeType): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['strikeType' => $strikeType]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->strikeType = $strikeType; + } + + return $this; + } + + public function getUSchemeClr(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUSchemeClr(); + } + + return $this->uSchemeClr; + } + + public function setUSchemeClr(string $uSchemeClr): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['uSchemeClr' => $uSchemeClr]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->uSchemeClr = $uSchemeClr; + } + + return $this; + } + /** * Get Underline. * @@ -546,6 +713,15 @@ class Font extends Supervisor $this->underline . ($this->strikethrough ? 't' : 'f') . $this->color->getHashCode() . + '*' . + $this->latin . + '*' . + $this->eastAsian . + '*' . + $this->complexScript . + $this->strikeType . + $this->uSchemeClr . + (string) $this->baseLine . __CLASS__ ); } @@ -553,12 +729,17 @@ class Font extends Supervisor protected function exportArray1(): array { $exportedArray = []; + $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); $this->exportArray2($exportedArray, 'color', $this->getColor()); + $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); + $this->exportArray2($exportedArray, 'eastAsian', $this->getEastAsian()); $this->exportArray2($exportedArray, 'italic', $this->getItalic()); + $this->exportArray2($exportedArray, 'latin', $this->getLatin()); $this->exportArray2($exportedArray, 'name', $this->getName()); $this->exportArray2($exportedArray, 'size', $this->getSize()); $this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough()); + $this->exportArray2($exportedArray, 'strikeType', $this->getStrikeType()); $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index f18d9216..0f65ae3b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -9,8 +9,8 @@ use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -121,6 +121,10 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $title->getCaption(); if ((is_array($caption)) && (count($caption) > 0)) { @@ -304,9 +308,9 @@ class Chart extends WriterPart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { if ($chartType === DataSeries::TYPE_BUBBLECHART) { - $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis, ($chartType === DataSeries::TYPE_SCATTERCHART) ? 'c:valAx' : 'c:catAx'); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); @@ -367,9 +371,19 @@ class Chart extends WriterPart * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis, string $element = 'c:catAx'): void + private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void { - $objWriter->startElement($element); + // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc + // In that case, xAxis is NOT a category. + $AxisFormat = $yAxis->getAxisNumberFormat(); + if ( + $AxisFormat === Properties::FORMAT_CODE_DATE + || $AxisFormat == Properties::FORMAT_CODE_NUMBER + ) { + $objWriter->startElement('c:valAx'); + } else { + $objWriter->startElement('c:catAx'); + } if ($id1 > 0) { $objWriter->startElement('c:axId'); @@ -403,20 +417,20 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $xAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); - $objWriter->endElement(); $layout = $xAxisLabel->getLayout(); $this->writeLayout($objWriter, $layout); @@ -746,18 +760,17 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $yAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); - - $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); @@ -1104,13 +1117,26 @@ class Chart extends WriterPart } // Formatting for the points - if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()))) { + if ( + $groupType == DataSeries::TYPE_LINECHART + || $groupType == DataSeries::TYPE_STOCKCHART + || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()) + || ($plotSeriesValues !== false && $plotSeriesValues->getSchemeClr()) + ) { $plotLineWidth = 12700; if ($plotSeriesValues) { $plotLineWidth = $plotSeriesValues->getLineWidth(); } $objWriter->startElement('c:spPr'); + $schemeClr = $plotLabel ? $plotLabel->getSchemeClr() : null; + if ($schemeClr) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', $schemeClr); + $objWriter->endElement(); + $objWriter->endElement(); + } $objWriter->startElement('a:ln'); $objWriter->writeAttribute('w', $plotLineWidth); if ($groupType == DataSeries::TYPE_STOCKCHART || $groupType === DataSeries::TYPE_SCATTERCHART) { @@ -1186,7 +1212,14 @@ class Chart extends WriterPart $objWriter->startElement('c:cat'); } - $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + // xVals (Categories) are not always 'str' + // Test X-axis Label's Datatype to decide 'str' vs 'num' + $CategoryDatatype = $plotSeriesCategory->getDataType(); + if ($CategoryDatatype == DataSeriesValues::DATASERIES_TYPE_NUMBER) { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'num'); + } else { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + } $objWriter->endElement(); } @@ -1377,7 +1410,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); $objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index 6808f33e..da7d825b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -203,7 +203,8 @@ class StringTable extends WriterPart if (!$richText instanceof RichText) { $textRun = $richText; $richText = new RichText(); - $richText->createTextRun($textRun); + $run = $richText->createTextRun($textRun); + $run->setFont(null); } if ($prefix !== null) { @@ -241,7 +242,11 @@ class StringTable extends WriterPart } $objWriter->writeAttribute('u', $underlineType); // Strikethrough - $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); + $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike')); + // Superscript/subscript + if ($element->getFont()->getBaseLine()) { + $objWriter->writeAttribute('baseline', (string) $element->getFont()->getBaseLine()); + } // Color $objWriter->startElement($prefix . 'solidFill'); @@ -250,10 +255,33 @@ class StringTable extends WriterPart $objWriter->endElement(); // srgbClr $objWriter->endElement(); // solidFill + // Underscore Color + if ($element->getFont()->getUSchemeClr()) { + $objWriter->startElement($prefix . 'uFill'); + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . 'schemeClr'); + $objWriter->writeAttribute('val', $element->getFont()->getUSchemeClr()); + $objWriter->endElement(); // schemeClr + $objWriter->endElement(); // solidFill + $objWriter->endElement(); // uFill + } + // fontName - $objWriter->startElement($prefix . 'latin'); - $objWriter->writeAttribute('typeface', $element->getFont()->getName()); - $objWriter->endElement(); + if ($element->getFont()->getLatin()) { + $objWriter->startElement($prefix . 'latin'); + $objWriter->writeAttribute('typeface', $element->getFont()->getLatin()); + $objWriter->endElement(); + } + if ($element->getFont()->getEastAsian()) { + $objWriter->startElement($prefix . 'ea'); + $objWriter->writeAttribute('typeface', $element->getFont()->getEastAsian()); + $objWriter->endElement(); + } + if ($element->getFont()->getComplexScript()) { + $objWriter->startElement($prefix . 'cs'); + $objWriter->writeAttribute('typeface', $element->getFont()->getComplexScript()); + $objWriter->endElement(); + } $objWriter->endElement(); } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ColoredAxisLabelTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ColoredAxisLabelTest.php new file mode 100644 index 00000000..71b8da28 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ColoredAxisLabelTest.php @@ -0,0 +1,80 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testStock5(): void + { + $file = self::DIRECTORY . '32readwriteStockChart5.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $xAxisLabel = $chart->getXAxisLabel(); + $captionArray = $xAxisLabel->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('X-Axis Title in Green', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('00B050', $font->getColor()->getRGB()); + + $yAxisLabel = $chart->getYAxisLabel(); + $captionArray = $yAxisLabel->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Y-Axis Title in Red', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('FF0000', $font->getColor()->getRGB()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php index dc7fbc0b..76389727 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php @@ -57,7 +57,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -127,7 +127,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -141,7 +141,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Courier New', $font->getName()); + self::assertSame('Courier New', $font->getLatin()); self::assertEquals(10, $font->getSize()); self::assertFalse($font->getBold()); self::assertFalse($font->getItalic()); @@ -155,7 +155,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -224,7 +224,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -258,4 +258,76 @@ class Charts32ScatterTest extends AbstractFunctional $reloadedSpreadsheet->disconnectWorksheets(); } + + public function testScatter7(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart7.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Latin/EA/CS Title ABCאבגDEFァ', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertGreaterThan(0, count($elements)); + foreach ($elements as $run) { + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Times New Roman', $font->getLatin()); + self::assertSame('Malgun Gothic', $font->getEastAsian()); + self::assertSame('Courier New', $font->getComplexScript()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + } + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } } From 567b9601b030fab03f5e946d33cbcea7f8bb2d54 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 21 May 2022 07:21:14 -0700 Subject: [PATCH 14/30] Allow Csv Reader to Store Null String in Spreadsheet (#2842) Fix #2840 (and also #2839 but that's Q&A, not an issue). Csv Reader does not populate cells which contain null string. This PR provides an option for the reader to store null strings as it does with any other string. --- docs/topics/reading-and-writing-to-file.md | 12 +++-- src/PhpSpreadsheet/Reader/Csv.php | 21 ++++++++- .../Reader/Csv/CsvIssue2840Test.php | 45 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2840Test.php diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 19928f04..0bcc1909 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -449,8 +449,7 @@ $spreadsheet = $reader->loadSpreadsheetFromString($data); #### Setting CSV options Often, CSV files are not really "comma separated", or use semicolon (`;`) -as a separator. You can instruct -`\PhpOffice\PhpSpreadsheet\Reader\Csv` some options before reading a CSV +as a separator. You can set some options before reading a CSV file. The separator will be auto-detected, so in most cases it should not be necessary @@ -506,6 +505,12 @@ $reader->setSheetIndex(0); $spreadsheet = $reader->load('sample.csv'); ``` +The CSV reader will normally not load null strings into the spreadsheet. +To load them: +```php +$reader->setPreserveNullString(true); +``` + Finally, you can set a callback to be invoked when the constructor is executed, either through `new Csv()` or `IOFactory::load`, and have that callback set the customizable attributes to whatever @@ -584,8 +589,7 @@ $writer->save("05featuredemo.csv"); #### Setting CSV options Often, CSV files are not really "comma separated", or use semicolon (`;`) -as a separator. You can instruct -`\PhpOffice\PhpSpreadsheet\Writer\Csv` some options before writing a CSV +as a separator. You can set some options before writing a CSV file: ```php diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index b604ceef..65a71edb 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -103,6 +103,9 @@ class Csv extends BaseReader */ protected $preserveNumericFormatting = false; + /** @var bool */ + private $preserveNullString = false; + /** * Create a new CSV Reader instance. */ @@ -300,9 +303,11 @@ class Csv extends BaseReader } } - public function setTestAutoDetect(bool $value): void + public function setTestAutoDetect(bool $value): self { $this->testAutodetect = $value; + + return $this; } private function setAutoDetect(?string $value): ?string @@ -390,7 +395,7 @@ class Csv extends BaseReader foreach ($rowData as $rowDatum) { $this->convertBoolean($rowDatum, $preserveBooleanString); $numberFormatMask = $this->convertFormattedNumber($rowDatum); - if ($rowDatum !== '' && $this->readFilter->readCell($columnLetter, $currentRow)) { + if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($this->contiguous) { if ($noOutputYet) { $noOutputYet = false; @@ -625,4 +630,16 @@ class Csv extends BaseReader return ($encoding === '') ? $dflt : $encoding; } + + public function setPreserveNullString(bool $value): self + { + $this->preserveNullString = $value; + + return $this; + } + + public function getPreserveNullString(): bool + { + return $this->preserveNullString; + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2840Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2840Test.php new file mode 100644 index 00000000..34d0a864 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2840Test.php @@ -0,0 +1,45 @@ +getPreserveNullString()); + $inputData = <<loadSpreadsheetFromString($inputData); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame($expected, $sheet->toArray()); + $spreadsheet->disconnectWorksheets(); + } + + public function testNullStringLoad(): void + { + $reader = new Csv(); + $reader->setPreserveNullString(true); + $inputData = <<loadSpreadsheetFromString($inputData); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame($expected, $sheet->toArray()); + $spreadsheet->disconnectWorksheets(); + } +} From 7c1c896959642f8094156dd82298bbb51dc92b9d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 21 May 2022 08:07:59 -0700 Subject: [PATCH 15/30] Change and Re-enable URLImageTest (#2844) Disabled it earlier because its reliance on an external site not under our control was causing problems. URL in spreadsheet is now changed to point to an image in phpspreadsheet.readthedocs.io, which should be more reliable. Test is re-enabled. --- .../Reader/Xlsx/URLImageTest.php | 11 ++++------- tests/data/Reader/XLSX/urlImage.xlsx | Bin 10097 -> 10114 bytes 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php index 3eb2ba2c..e7f99010 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php @@ -11,12 +11,9 @@ class URLImageTest extends TestCase { public function testURLImageSource(): void { - if (getenv('RUN_URL_IMAGE_TEST') !== '1') { - self::markTestSkipped('Skipped due to no longer bein able to access external URL'); + if (getenv('SKIP_URL_IMAGE_TEST') === '1') { + self::markTestSkipped('Skipped due to setting of environment variable'); } - //if (getenv('SKIP_URL_IMAGE_TEST') === '1') { - // self::markTestSkipped('Skipped due to setting of environment variable'); - //} $filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.xlsx'); self::assertNotFalse($filename); $reader = IOFactory::createReader('Xlsx'); @@ -29,7 +26,7 @@ class URLImageTest extends TestCase self::assertInstanceOf(Drawing::class, $drawing); // Check if the source is a URL or a file path self::assertTrue($drawing->getIsURL()); - self::assertSame('https://www.globalipmanager.com/DataFiles/Pics/20/Berniaga.comahp2.jpg', $drawing->getPath()); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/topics/images/01-03-filter-icon-1.png', $drawing->getPath()); $imageContents = file_get_contents($drawing->getPath()); self::assertNotFalse($imageContents); $filePath = tempnam(sys_get_temp_dir(), 'Drawing'); @@ -39,7 +36,7 @@ class URLImageTest extends TestCase unlink($filePath); self::assertNotFalse($mimeType); $extension = File::mime2ext($mimeType); - self::assertSame('jpeg', $extension); + self::assertSame('png', $extension); } } } diff --git a/tests/data/Reader/XLSX/urlImage.xlsx b/tests/data/Reader/XLSX/urlImage.xlsx index 01e8e24de3254416230528e48b4deb637682661e..422958221cc13f3526c192cff9e95d58302bf3e3 100644 GIT binary patch delta 1435 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 Date: Mon, 23 May 2022 00:08:06 +0200 Subject: [PATCH 16/30] Minor performance tweak --- src/PhpSpreadsheet/Worksheet/Validations.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index cd76d2a4..0d668d6f 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -42,10 +42,9 @@ class Validations public static function validateCellOrCellRange($cellRange): string { if (is_string($cellRange) || is_numeric($cellRange)) { - // Convert a single column reference like 'A' to 'A:A' - $cellRange = (string) preg_replace('/^([A-Z]+)$/', '${1}:${1}', (string) $cellRange); - // Convert a single row reference like '1' to '1:1' - $cellRange = (string) preg_replace('/^(\d+)$/', '${1}:${1}', $cellRange); + // Convert a single column reference like 'A' to 'A:A', + // a single row reference like '1' to '1:1' + $cellRange = (string) preg_replace('/^([A-Z]+|\d+)$/', '${1}:${1}', (string) $cellRange); } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { $cellRange = new CellRange($cellRange, $cellRange); } From 924ec23eb07d39fc5e97acfd02e716c6112fa4ca Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 23 May 2022 12:12:05 +0200 Subject: [PATCH 17/30] Don't load invalid Print Area, because this will corrupt the internal print area definition, and break on Writing --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a53df5cf..8f9bc6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) - Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) - +- Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849) ## 1.23.0 - 2022-04-24 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index ecae5ad7..8eadb39c 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1530,13 +1530,18 @@ class Xlsx extends BaseReader $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $newRangeSets = []; foreach ($rangeSets as $rangeSet) { - [$sheetName, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + if (empty($rangeSet)) { + continue; + } if (strpos($rangeSet, ':') === false) { $rangeSet = $rangeSet . ':' . $rangeSet; } $newRangeSets[] = str_replace('$', '', $rangeSet); } - $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + if (count($newRangeSets) > 0) { + $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + } break; default: From 2e5ebea1101c0a9ba61b5bee3d8a1010e49e6975 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 23 May 2022 13:38:08 +0200 Subject: [PATCH 18/30] Add support for writing Worksheet Visibility for Ods --- CHANGELOG.md | 2 + src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 22 ++++++++++ src/PhpSpreadsheet/Writer/Ods/Content.php | 3 ++ .../Writer/Ods/ContentTest.php | 23 ++++++++++ tests/data/Writer/Ods/content-empty.xml | 5 ++- .../Writer/Ods/content-hidden-worksheet.xml | 42 +++++++++++++++++++ tests/data/Writer/Ods/content-with-data.xml | 10 ++++- 7 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/data/Writer/Ods/content-hidden-worksheet.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index a53df5cf..fd1a6eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. +- Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) + ### Changed - Memory and speed improvements, particularly for the Cell Collection, and the Writers. diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index 66194468..1bf2c463 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -10,12 +10,14 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Style { public const CELL_STYLE_PREFIX = 'ce'; public const COLUMN_STYLE_PREFIX = 'co'; public const ROW_STYLE_PREFIX = 'ro'; + public const TABLE_STYLE_PREFIX = 'ta'; private $writer; @@ -221,6 +223,26 @@ class Style $this->writer->endElement(); // Close style:style } + public function writeTableStyle(Worksheet $worksheet, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId) + ); + + $this->writer->startElement('style:table-properties'); + + $this->writer->writeAttribute( + 'table:display', + $worksheet->getSheetState() === Worksheet::SHEETSTATE_VISIBLE ? 'true' : 'false' + ); + + $this->writer->endElement(); // Close style:table-properties + $this->writer->endElement(); // Close style:style + } + public function write(CellStyle $style): void { $this->writer->startElement('style:style'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index a8da1019..00ab0643 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -123,6 +123,7 @@ class Content extends WriterPart for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) { $objWriter->startElement('table:table'); $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); + $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1)); $objWriter->writeElement('office:forms'); foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { $objWriter->startElement('table:table-column'); @@ -289,6 +290,8 @@ class Content extends WriterPart $sheetCount = $spreadsheet->getSheetCount(); for ($i = 0; $i < $sheetCount; ++$i) { $worksheet = $spreadsheet->getSheet($i); + $styleWriter->writeTableStyle($worksheet, $i + 1); + $worksheet->calculateColumnWidths(); foreach ($worksheet->getColumnDimensions() as $columnDimension) { if ($columnDimension->getWidth() !== -1.0) { diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php index 917a0410..81c244a7 100644 --- a/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php @@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Writer\Ods\Content; use PHPUnit\Framework\TestCase; @@ -106,4 +107,26 @@ class ContentTest extends TestCase self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-with-data.xml', $xml); } + + public function testWriteWithHiddenWorksheet(): void + { + $workbook = new Spreadsheet(); + + // Worksheet 1 + $worksheet1 = $workbook->getActiveSheet(); + $worksheet1->setCellValue('A1', 1); + + // Worksheet 2 + $worksheet2 = $workbook->createSheet(); + $worksheet2->setTitle('New Worksheet'); + $worksheet2->setCellValue('A1', 2); + + $worksheet2->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + + // Write + $content = new Content(new Ods($workbook)); + $xml = $content->write(); + + self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-hidden-worksheet.xml', $xml); + } } diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index 867cfa3e..84f4c239 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -3,6 +3,9 @@ + + + @@ -12,7 +15,7 @@ - + diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml new file mode 100644 index 00000000..88a53257 --- /dev/null +++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + 2 + + + + + + + + \ No newline at end of file diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index fff47f65..12140fa9 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -3,6 +3,12 @@ + + + + + + @@ -62,7 +68,7 @@ - + @@ -107,7 +113,7 @@ - + From 3fae29d6134f42b9b555e6b7ccf17a6eed370532 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 23 May 2022 15:30:45 +0200 Subject: [PATCH 19/30] Add support for reading Worksheet Visibility for Ods --- CHANGELOG.md | 2 + phpstan-baseline.neon | 25 -------- src/PhpSpreadsheet/Reader/Ods.php | 1 + .../Reader/Ods/PageSettings.php | 47 +++++++++++++++ .../Reader/Ods/HiddenWorksheetTest.php | 57 ++++++++++++++++++ tests/data/Reader/Ods/HiddenSheet.ods | Bin 0 -> 2821 bytes 6 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php create mode 100644 tests/data/Reader/Ods/HiddenSheet.ods diff --git a/CHANGELOG.md b/CHANGELOG.md index a53df5cf..779f435f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. +- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) + ### Changed - Memory and speed improvements, particularly for the Cell Collection, and the Writers. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eabedcc8..ad2777a0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1945,36 +1945,11 @@ parameters: count: 6 path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$masterPrintStylesCrossReference has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$masterStylesCrossReference has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$officeNs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$pageLayoutStyles has no type specified\\.$#" count: 1 path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesFo has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesNs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\Properties\\:\\:load\\(\\) has parameter \\$namespacesMeta with no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 27c58edb..7e776ab7 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -588,6 +588,7 @@ class Ods extends BaseReader break; } } + $pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); ++$worksheetID; } diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php index 8d24fd0c..4d2fd992 100644 --- a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -8,16 +8,41 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PageSettings { + /** + * @var string + */ private $officeNs; + /** + * @var string + */ private $stylesNs; + /** + * @var string + */ private $stylesFo; + /** + * @var string + */ + private $tableNs; + + /** + * @var string[] + */ + private $tableStylesCrossReference = []; + private $pageLayoutStyles = []; + /** + * @var string[] + */ private $masterStylesCrossReference = []; + /** + * @var string[] + */ private $masterPrintStylesCrossReference = []; public function __construct(DOMDocument $styleDom) @@ -32,6 +57,7 @@ class PageSettings $this->officeNs = $styleDom->lookupNamespaceUri('office'); $this->stylesNs = $styleDom->lookupNamespaceUri('style'); $this->stylesFo = $styleDom->lookupNamespaceUri('fo'); + $this->tableNs = $styleDom->lookupNamespaceUri('table'); } private function readPageSettingStyles(DOMDocument $styleDom): void @@ -98,12 +124,33 @@ class PageSettings foreach ($styleXReferences as $styleXreferenceSet) { $styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name'); $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name'); + $styleFamilyName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'family'); + if (!empty($styleFamilyName) && $styleFamilyName === 'table') { + $styleVisibility = 'true'; + foreach ($styleXreferenceSet->getElementsByTagNameNS($this->stylesNs, 'table-properties') as $tableProperties) { + $styleVisibility = $tableProperties->getAttributeNS($this->tableNs, 'display'); + } + $this->tableStylesCrossReference[$styleXRefName] = $styleVisibility; + } if (!empty($stylePageLayoutName)) { $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; } } } + public function setVisibilityForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->tableStylesCrossReference)) { + return; + } + + $worksheet->setSheetState( + $this->tableStylesCrossReference[$styleName] === 'false' + ? Worksheet::SHEETSTATE_HIDDEN + : Worksheet::SHEETSTATE_VISIBLE + ); + } + public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void { if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php new file mode 100644 index 00000000..9edaa9b7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php @@ -0,0 +1,57 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/data/Reader/Ods/HiddenSheet.ods b/tests/data/Reader/Ods/HiddenSheet.ods new file mode 100644 index 0000000000000000000000000000000000000000..9b1d01a69cfd9833776b874d4ecb1d669a344d79 GIT binary patch literal 2821 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 Date: Mon, 23 May 2022 09:21:52 -0700 Subject: [PATCH 20/30] Eliminate Corruption in Surface Chart Samples (#2846) * Eliminate Corruption in Surface Chart Samples Fix #2763 (bubble charts were fixed earlier). * Improvement in Testing for Numeric Values The original proposal was to test for numeric values by checking whether the format was NUMERIC or DATE. This is pretty inflexible, and left open a wide gap concerning what to do with GENERAL. Changed to allow the user to set a `numeric` property at the same time as `axisNumberProperties`, eliminating these concerns. User can set property when chart is created (new test member Charts32CatAxValAxTest), or can set it when spreadsheet with chart is read (new method testCatAxValAx in Charts32XmlTest). Sample 33_Chart_create_scatter2 creates a slightly wider chart when `numeric` is set to true than it does when unset, but chart is otherwise the same. Corrected a bug which occured twice in Chart. Methods getChartAxisX and getChartAxisY created a new Axis object if the chart pointer was null, but neglected to attach the new object to the chart. Thus, subsequent calls to the methods return different objects. Code is changed to attach the new Axis to the Chart. --- phpstan-baseline.neon | 20 -- samples/Chart/33_Chart_create_scatter2.php | 8 +- src/PhpSpreadsheet/Chart/Axis.php | 21 ++- src/PhpSpreadsheet/Chart/Chart.php | 66 ++++++- src/PhpSpreadsheet/Chart/Properties.php | 1 + src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 20 ++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 111 ++++++++++-- .../Writer/Xlsx/Charts32CatAxValAxTest.php | 171 ++++++++++++++++++ .../Writer/Xlsx/Charts32XmlTest.php | 58 ++++++ 9 files changed, 434 insertions(+), 42 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ad2777a0..f031bfe6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4775,26 +4775,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Parameter \\#3 \\$id1 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$id1 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$id2 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#5 \\$id2 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#6 \\$yAxis of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null given\\.$#" count: 1 diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index 59d6bd3b..1d6e331c 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -25,7 +25,7 @@ $worksheet->fromArray( ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], ] ); -$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode('yyyy-mm-dd'); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); $worksheet->getColumnDimension('A')->setAutoSize(true); $worksheet->setSelectedCells('A1'); @@ -67,7 +67,7 @@ $dataSeriesValues = [ // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); -$xAxis->setAxisNumberProperties('yyyy-mm-dd'); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); // Build the dataseries $series = new DataSeries( @@ -78,7 +78,7 @@ $series = new DataSeries( $xAxisTickValues, // plotCategory $dataSeriesValues, // plotValues null, // plotDirection - null, // smooth line + false, // smooth line //DataSeries::STYLE_LINEMARKER // plotStyle DataSeries::STYLE_MARKER // plotStyle ); @@ -107,7 +107,7 @@ $chart = new Chart( // Set the position where the chart should appear in the worksheet $chart->setTopLeftPosition('A7'); -$chart->setBottomRightPosition('N20'); +$chart->setBottomRightPosition('P20'); // Add the chart to the worksheet $worksheet->addChart($chart); diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index eeed326d..089ebdb9 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -18,6 +18,7 @@ class Axis extends Properties private $axisNumber = [ 'format' => self::FORMAT_CODE_GENERAL, 'source_linked' => 1, + 'numeric' => null, ]; /** @@ -131,15 +132,26 @@ class Axis extends Properties 'size' => null, ]; + private const NUMERIC_FORMAT = [ + Properties::FORMAT_CODE_NUMBER, + Properties::FORMAT_CODE_DATE, + ]; + /** * Get Series Data Type. * * @param mixed $format_code */ - public function setAxisNumberProperties($format_code): void + public function setAxisNumberProperties($format_code, ?bool $numeric = null): void { - $this->axisNumber['format'] = (string) $format_code; + $format = (string) $format_code; + $this->axisNumber['format'] = $format; $this->axisNumber['source_linked'] = 0; + if (is_bool($numeric)) { + $this->axisNumber['numeric'] = $numeric; + } elseif (in_array($format, self::NUMERIC_FORMAT, true)) { + $this->axisNumber['numeric'] = true; + } } /** @@ -162,6 +174,11 @@ class Axis extends Properties return (string) $this->axisNumber['source_linked']; } + public function getAxisIsNumericFormat(): bool + { + return (bool) $this->axisNumber['numeric']; + } + /** * Set Axis Options Properties. * diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 2efec5bb..80d3d5f3 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -140,6 +140,18 @@ class Chart */ private $bottomRightYOffset = 10; + /** @var ?int */ + private $rotX; + + /** @var ?int */ + private $rotY; + + /** @var ?int */ + private $rAngAx; + + /** @var ?int */ + private $perspective; + /** * Create a new Chart. * @@ -351,8 +363,9 @@ class Chart if ($this->yAxis !== null) { return $this->yAxis; } + $this->yAxis = new Axis(); - return new Axis(); + return $this->yAxis; } /** @@ -365,8 +378,9 @@ class Chart if ($this->xAxis !== null) { return $this->xAxis; } + $this->xAxis = new Axis(); - return new Axis(); + return $this->xAxis; } /** @@ -681,4 +695,52 @@ class Chart return $renderer->render($outputDestination); } + + public function getRotX(): ?int + { + return $this->rotX; + } + + public function setRotX(?int $rotX): self + { + $this->rotX = $rotX; + + return $this; + } + + public function getRotY(): ?int + { + return $this->rotY; + } + + public function setRotY(?int $rotY): self + { + $this->rotY = $rotY; + + return $this; + } + + public function getRAngAx(): ?int + { + return $this->rAngAx; + } + + public function setRAngAx(?int $rAngAx): self + { + $this->rAngAx = $rAngAx; + + return $this; + } + + public function getPerspective(): ?int + { + return $this->perspective; + } + + public function setPerspective(?int $perspective): self + { + $this->perspective = $perspective; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index ef22fb52..9f1a5ce0 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -37,6 +37,7 @@ abstract class Properties const FORMAT_CODE_CURRENCY = '$#,##0.00'; const FORMAT_CODE_ACCOUNTING = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)'; const FORMAT_CODE_DATE = 'm/d/yyyy'; + const FORMAT_CODE_DATE_ISO8601 = 'yyyy-mm-dd'; const FORMAT_CODE_TIME = '[$-F400]h:mm:ss AM/PM'; const FORMAT_CODE_PERCENTAGE = '0.00%'; const FORMAT_CODE_FRACTION = '# ?/?'; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 4480c6ff..e3498e20 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -55,12 +55,20 @@ class Chart $XaxisLabel = $YaxisLabel = $legend = $title = null; $dispBlanksAs = $plotVisOnly = null; $plotArea = null; + $rotX = $rotY = $rAngAx = $perspective = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetailsC = $chartDetails->children($namespacesChartMeta['c']); switch ($chartDetailsKey) { + case 'view3D': + $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer'); + $rotY = self::getAttribute($chartDetails->rotY, 'val', 'integer'); + $rAngAx = self::getAttribute($chartDetails->rAngAx, 'val', 'integer'); + $perspective = self::getAttribute($chartDetails->perspective, 'val', 'integer'); + + break; case 'plotArea': $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; @@ -220,6 +228,18 @@ class Chart } } $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, $dispBlanksAs, $XaxisLabel, $YaxisLabel); + if (is_int($rotX)) { + $chart->setRotX($rotX); + } + if (is_int($rotY)) { + $chart->setRotY($rotY); + } + if (is_int($rAngAx)) { + $chart->setRAngAx($rAngAx); + } + if (is_int($perspective)) { + $chart->setPerspective($perspective); + } return $chart; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 0f65ae3b..77cbb872 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -9,7 +9,6 @@ use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -75,6 +74,33 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', 0); $objWriter->endElement(); + $objWriter->startElement('c:view3D'); + $rotX = $chart->getRotX(); + if (is_int($rotX)) { + $objWriter->startElement('c:rotX'); + $objWriter->writeAttribute('val', "$rotX"); + $objWriter->endElement(); + } + $rotY = $chart->getRotY(); + if (is_int($rotY)) { + $objWriter->startElement('c:rotY'); + $objWriter->writeAttribute('val', "$rotY"); + $objWriter->endElement(); + } + $rAngAx = $chart->getRAngAx(); + if (is_int($rAngAx)) { + $objWriter->startElement('c:rAngAx'); + $objWriter->writeAttribute('val', "$rAngAx"); + $objWriter->endElement(); + } + $perspective = $chart->getPerspective(); + if (is_int($perspective)) { + $objWriter->startElement('c:perspective'); + $objWriter->writeAttribute('val', "$perspective"); + $objWriter->endElement(); + } + $objWriter->endElement(); // view3D + $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY(), $chart->getMajorGridlines(), $chart->getMinorGridlines()); $this->writeLegend($objWriter, $chart->getLegend()); @@ -200,7 +226,7 @@ class Chart extends WriterPart return; } - $id1 = $id2 = 0; + $id1 = $id2 = $id3 = '0'; $this->seriesIndex = 0; $objWriter->startElement('c:plotArea'); @@ -230,6 +256,10 @@ class Chart extends WriterPart $objWriter->startElement('c:scatterStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); + } elseif ($groupType === DataSeries::TYPE_SURFACECHART_3D || $groupType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:wireframe'); + $objWriter->writeAttribute('val', $plotStyle ? '1' : '0'); + $objWriter->endElement(); } $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType); @@ -280,9 +310,10 @@ class Chart extends WriterPart $objWriter->endElement(); } - // Generate 2 unique numbers to use for axId values - $id1 = '75091328'; - $id2 = '75089408'; + // Generate 3 unique numbers to use for axId values + $id1 = '110438656'; + $id2 = '110444544'; + $id3 = '110365312'; // used in Surface Chart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { $objWriter->startElement('c:axId'); @@ -291,6 +322,11 @@ class Chart extends WriterPart $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); + } } else { $objWriter->startElement('c:firstSliceAng'); $objWriter->writeAttribute('val', 0); @@ -314,6 +350,9 @@ class Chart extends WriterPart } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $this->writeSerAxis($objWriter, $id2, $id3); + } } $objWriter->endElement(); @@ -375,17 +414,13 @@ class Chart extends WriterPart { // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc // In that case, xAxis is NOT a category. - $AxisFormat = $yAxis->getAxisNumberFormat(); - if ( - $AxisFormat === Properties::FORMAT_CODE_DATE - || $AxisFormat == Properties::FORMAT_CODE_NUMBER - ) { + if ($yAxis->getAxisIsNumericFormat()) { $objWriter->startElement('c:valAx'); } else { $objWriter->startElement('c:catAx'); } - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id1); $objWriter->endElement(); @@ -459,7 +494,7 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); $objWriter->endElement(); - if ($id2 > 0) { + if ($id2 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -501,7 +536,7 @@ class Chart extends WriterPart { $objWriter->startElement('c:valAx'); - if ($id2 > 0) { + if ($id2 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -926,7 +961,7 @@ class Chart extends WriterPart $objWriter->endElement(); //effectList $objWriter->endElement(); //end spPr - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -969,6 +1004,54 @@ class Chart extends WriterPart $objWriter->endElement(); } + /** + * Write Ser Axis, for Surface chart. + */ + private function writeSerAxis(XMLWriter $objWriter, string $id2, string $id3): void + { + $objWriter->startElement('c:serAx'); + + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); // axId + + $objWriter->startElement('c:scaling'); + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', 'minMax'); + $objWriter->endElement(); // orientation + $objWriter->endElement(); // scaling + + $objWriter->startElement('c:delete'); + $objWriter->writeAttribute('val', '0'); + $objWriter->endElement(); // delete + + $objWriter->startElement('c:axPos'); + $objWriter->writeAttribute('val', 'b'); + $objWriter->endElement(); // axPos + + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', 'out'); + $objWriter->endElement(); // majorTickMark + + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', 'none'); + $objWriter->endElement(); // minorTickMark + + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', 'nextTo'); + $objWriter->endElement(); // tickLblPos + + $objWriter->startElement('c:crossAx'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); // crossAx + + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', 'autoZero'); + $objWriter->endElement(); // crosses + + $objWriter->endElement(); //serAx + } + /** * Get the data series type(s) for a chart plot series. * diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php new file mode 100644 index 00000000..af33baa1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php @@ -0,0 +1,171 @@ +outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + /** + * @dataProvider providerCatAxValAx + */ + public function test1CatAx1ValAx(?bool $numeric): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + // changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date + $worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] + ); + $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); + $worksheet->getColumnDimension('A')->setAutoSize(true); + $worksheet->setSelectedCells('A1'); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 + ]; + // Set the X-Axis Labels + // changed from STRING to NUMBER + // added 2 additional x-axis values associated with each of the 3 metrics + // added FORMATE_CODE_NUMBER + $xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + // added FORMAT_CODE_NUMBER + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), + ]; + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers + $xAxis = new Axis(); + //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, $numeric); + } else { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + } + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + //DataSeries::STYLE_LINEMARKER // plotStyle + DataSeries::STYLE_MARKER // plotStyle + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test Scatter Trend Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('P20'); + // Add the chart to the worksheet + $worksheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } elseif ($numeric === true) { + self::assertSame(0, substr_count($data, '')); } } + + /** + * @dataProvider providerCatAxValAx + */ + public function testCatAxValAx(?bool $numeric): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $xAxis->getAxisNumberFormat()); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, true); + } + $yAxis = $chart->getChartAxisY(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $yAxis->getAxisNumberFormat()); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); + $yAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); + } + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart2.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } elseif ($numeric === true) { + self::assertSame(0, substr_count($data, '')); + self::assertSame(2, substr_count($data, '')); + } else { + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + } + } + + public function providerCatAxValAx(): array + { + return [ + [true], + [false], + [null], + ]; + } } From 8914c6126576b880fd67e0e39a251cf38bb97a4f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 23 May 2022 18:32:20 +0200 Subject: [PATCH 21/30] Minor performance tweak --- src/PhpSpreadsheet/Worksheet/Validations.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index 0d668d6f..a56dda4c 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -65,9 +65,12 @@ class Validations [$worksheet, $addressRange] = Worksheet::extractSheetTitle($cellRange, true); // Convert Column ranges like 'A:C' to 'A1:C1048576' - $addressRange = (string) preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); - // Convert Row ranges like '1:3' to 'A1:XFD3' - $addressRange = (string) preg_replace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); + // or Row ranges like '1:3' to 'A1:XFD3' + $addressRange = (string) preg_replace( + ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'], + ['${1}1:${2}1048576', 'A${1}:XFD${2}'], + $addressRange + ); return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); } From 23ce21901d60c48239c8a5ae56ff5f37623d21b8 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 24 May 2022 14:08:41 +0200 Subject: [PATCH 22/30] Add support for reading Worksheet Visibility for Gnumeric --- src/PhpSpreadsheet/Reader/Gnumeric.php | 5 ++ .../Reader/Gnumeric/HiddenWorksheetTest.php | 56 ++++++++++++++++++ .../data/Reader/Gnumeric/HiddenSheet.gnumeric | Bin 0 -> 1871 bytes 3 files changed, 61 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/HiddenWorksheetTest.php create mode 100644 tests/data/Reader/Gnumeric/HiddenSheet.gnumeric diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index ee2e8d3d..ca087e61 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -272,6 +272,11 @@ class Gnumeric extends BaseReader // name in line with the formula, not the reverse $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + $visibility = $sheetOrNull->attributes()['Visibility'] ?? 'GNM_SHEET_VISIBILITY_VISIBLE'; + if ((string) $visibility !== 'GNM_SHEET_VISIBILITY_VISIBLE') { + $this->spreadsheet->getActiveSheet()->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + } + if (!$this->readDataOnly) { (new PageSetup($this->spreadsheet)) ->printInformation($sheet) diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/HiddenWorksheetTest.php new file mode 100644 index 00000000..ffa3b88d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/HiddenWorksheetTest.php @@ -0,0 +1,56 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/data/Reader/Gnumeric/HiddenSheet.gnumeric b/tests/data/Reader/Gnumeric/HiddenSheet.gnumeric new file mode 100644 index 0000000000000000000000000000000000000000..4f503c28d6355657fbbc477ec1c22bd5478032a6 GIT binary patch literal 1871 zcmV-V2e9}biwFP!000001MOLDZ`(Ey{@!1qX~4eOvK=Q))Yyd+E3pB`32Y@@I}8FX z(KZ)K^hhdk`s;TjWkr&cChb;q*qj1^c)UB_9q;8~di;JJQ|k_ML0HnUoArim;Ur`c zNfsUZ+CQru+V8t>ju%Pneqj7|#@MX|B8hN;)UlURu3X2tzrU|9k~GGggmuOj&Rw%n zk7Q(LylX*9y!U&WtJP>UonJ3T({PDnR3nLyCBPGe3C?ezO}8hlv3q!vsH(slgsy{IVYwmr-gU*2-3Lp?$!C^GL<13WM(GUkV<(p=s<=Zm#a(}L?HLBMHU~RTocs;e z!0k#B{yS_*n8b<{NHh~a#EggqD3vS-S-hfHrPdEIgNR_oAsC^=(H9Lk>y4(bvY}tXK~H8l;-?0qR(2X3A)8lLj`hyGeU43twyU=YwXurZNJ%Y_xIgq z>ysAHyBlt8ekw4pMR-mU99f#8Jnq;FF?UrAMJ*y+KGUVo5)_JF_h5jadadD4+NWk}lH#*6xOxZ%cXE0@-f(?qr%RM9aP<8ms?VD)5yw%Vh2MV8u~SL-oKR{R`EWc% zG)$?EEsWJ9MRbB8cc8wZ2)zW&C>~18vyrW5?>Z+4pfrFlC-`6^R}&w^>wpDdD63SM zpopwHb}L)<$%06b-QO!r>FE+G+w3>5*iDAm)TDbs=@&N6&Td2$nc4=xSOfh-&F&|hEg;tClv?%6(@-ddeA<$ z2YY3>|2*-|e%c!D1-2CvL{&<+m)VvR9Oh( zpm;7H`5X0@CAg}IuM=77TFhly&HBNMGIIuq^0hLXQm6#cbQOgMEHg=9riCyE&;kI$ z@_}OUJafzvcmZ-~uFvyC z9Kl^ayDe3TeBlUqOu&l-c~%v@g1oSd{8jJV`+|ZMf=ucX0dKK()+aQj%mq(0{XVc@5{3GyPz>DZ<;8_J z_NT#c99)fhr`~q9aL%}}bQ|&XcTW^Gc$3NHWE%9QL4P;}6(7C+qYh)%b1leJiB7<3 z=_uj^FBC!&5^)KmNKaI~n5dDd|Hcw&y228BdZ~8|lGwMmDqCx{20cm^iE&}j8x7CL z!MQi~CcP01+rr+B**m%P{mTp6`oPi3$7`uio-V|%#Y_b*M(Ba1((>8Lw3S}7>I|dsV7D$Mn}!Q1=dX?@;#+b?;F3{{(dxmLmUK`1P=lB(jXhIP%dfQ((9^|KHku^B0%9 JW;Ep>008&2h}r-E literal 0 HcmV?d00001 From bb7e083745b52f74c79b4b6ecca36c25238f767e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 24 May 2022 14:34:13 +0200 Subject: [PATCH 23/30] Add Hidden Worksheet unit tests for other Readers --- docs/references/features-cross-reference.md | 22 ++++++- .../Reader/Ods/HiddenWorksheetTest.php | 1 - .../Reader/Xls/HiddenWorksheetTest.php | 56 ++++++++++++++++++ .../Reader/Xlsx/HiddenWorksheetTest.php | 56 ++++++++++++++++++ tests/data/Reader/XLS/HiddenSheet.xls | Bin 0 -> 25600 bytes tests/data/Reader/XLSX/HiddenSheet.xlsx | Bin 0 -> 8650 bytes 6 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/HiddenWorksheetTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenWorksheetTest.php create mode 100644 tests/data/Reader/XLS/HiddenSheet.xls create mode 100644 tests/data/Reader/XLSX/HiddenSheet.xlsx diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 399be82e..d18c0969 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -16,7 +16,7 @@ XLS XLSX - Excel2003XML + XML (Excel2003XML) Ods Gnumeric CSV @@ -732,12 +732,32 @@ + + Hidden Worksheets + ✔ + ✔ + + ✔ + ✔ + N/A + + + + + + + + + + + Coloured Tabs + N/A diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php index 9edaa9b7..c02c8771 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php @@ -32,7 +32,6 @@ class HiddenWorksheetTest extends TestCase $sheetAssertions = $assertions[$worksheet->getTitle()]; foreach ($sheetAssertions as $test => $expectedResult) { - $testMethodName = 'get' . ucfirst($test); $actualResult = $worksheet->getSheetState(); self::assertSame( $expectedResult, diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenWorksheetTest.php new file mode 100644 index 00000000..82fd6e12 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenWorksheetTest.php @@ -0,0 +1,56 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenWorksheetTest.php new file mode 100644 index 00000000..130d76a6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenWorksheetTest.php @@ -0,0 +1,56 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/data/Reader/XLS/HiddenSheet.xls b/tests/data/Reader/XLS/HiddenSheet.xls new file mode 100644 index 0000000000000000000000000000000000000000..39ced197d14fdf80347adf4c7b0a6b5fb4e1c12b GIT binary patch literal 25600 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 ahCex;1z$4Z7Wg)O`uq#^$6HJV^8X2WP4(;m literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLSX/HiddenSheet.xlsx b/tests/data/Reader/XLSX/HiddenSheet.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6d1d56a29c3b2f2dc0b1a2167062d1b3cb26b3a0 GIT binary patch literal 8650 zcmeHMWmH_rwr(K7Nr2!I2=3AZ2^t(4YusIfyF0-lKm)-E5InfMTOc?D_jGVguwbt< zbLVD;nfv?wxVL((Q|p{Md#~!Vzb*A`unfWj8~`E!82|u01CZSDlB>W00I?4MfF}TC zcx_QTTW3>SXMGh9ds8P}7IzzK@|*|obXfp+*!%w*|HWURM`g∈f%6eMgAAEKv?3 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 S3j+YCu-|J~#%3e?dG~*7g!9V) literal 0 HcmV?d00001 From c26215fdaba2df9c27a4ef408ee0885a1f0d15b7 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 24 May 2022 14:36:16 +0200 Subject: [PATCH 24/30] Update Change Log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74773e34..fe190f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. -- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) +- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) and Gnumeric Reader [PR #2853](https://github.com/PHPOffice/PhpSpreadsheet/pull/2853) - Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) ### Changed From 9e4ff929b4444464984d33c972bb0230afff7c0b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 25 May 2022 00:56:26 -0700 Subject: [PATCH 25/30] Namespacing Part 3 - Charts (#2852) * Namespacing Part 3 - Charts Charts still need some work, but they are now in good enough shape that the code that reads them can handle namespacing properly. Rather than a set of static routines, Reader/Xlsx/Chart needs to be changed to a regular object, with the relevant namespaces passed in the constructor. I am still looking for a test spreadsheet with non-standard namespacing for a full-blown test, but, in the meantime, the existing tests and samples will suffice. When I find a test case, I will certainly add it. The resulting code is actually simpler than it had been because of the elimination of the need for a NamespacesMeta variable in all the function signatures. Because of that, enough errors disappeared from Phpstan that I decided to just finish that job - its baseline file now dips below 5,000 lines. * Update Change Log Need to document many chart changes. --- CHANGELOG.md | 8 +- phpstan-baseline.neon | 130 ------------------- src/PhpSpreadsheet/Reader/Xlsx.php | 3 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 156 +++++++++++++---------- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 2 +- 5 files changed, 98 insertions(+), 201 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe190f74..290474a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Add point size option for scatter charts +- Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) - Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. - Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) and Gnumeric Reader [PR #2853](https://github.com/PHPOffice/PhpSpreadsheet/pull/2853) - Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) +- Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) +- Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) ### Changed @@ -36,6 +38,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) - Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) - Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849) +- Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) +- Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) +- Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) ## 1.23.0 - 2022-04-24 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f031bfe6..a8da7936 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2515,136 +2515,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has parameter \\$chartDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has parameter \\$plotType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$seriesDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValues\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValues\\(\\) has parameter \\$dataType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValues\\(\\) has parameter \\$seriesValueSet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValuesMultiLevel\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValuesMultiLevel\\(\\) has parameter \\$dataType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValuesMultiLevel\\(\\) has parameter \\$seriesValueSet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartLayoutDetails\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartLayoutDetails\\(\\) has parameter \\$chartDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartLayoutDetails\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartTitle\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:parseRichText\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readChartAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readChartAttributes\\(\\) has parameter \\$chartDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#1 \\$position of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend constructor expects string, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#3 \\$overlay of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend constructor expects bool, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#3 \\$plotOrder of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeries constructor expects array\\, array\\ given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$pointCount of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues constructor expects int, null given\\.$#" - count: 4 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#6 \\$displayBlanksAs of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart constructor expects string, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#7 \\$plotDirection of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeries constructor expects string\\|null, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredColumn\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 8eadb39c..df2b36c1 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1645,7 +1645,8 @@ class Xlsx extends BaseReader if ($this->includeCharts) { $chartEntryRef = ltrim((string) $contentType['PartName'], '/'); $chartElements = $this->loadZip($chartEntryRef); - $objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml')); + $chartReader = new Chart(); + $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml')); if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; if (isset($chartDetails[$chartPositionRef])) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index e3498e20..f6b1edd5 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -16,6 +16,18 @@ use SimpleXMLElement; class Chart { + /** @var string */ + private $cNamespace; + + /** @var string */ + private $aNamespace; + + public function __construct(string $cNamespace = Namespaces::CHART, string $aNamespace = Namespaces::DRAWINGML) + { + $this->cNamespace = $cNamespace; + $this->aNamespace = $aNamespace; + } + /** * @param string $name * @param string $format @@ -47,10 +59,9 @@ class Chart * * @return \PhpOffice\PhpSpreadsheet\Chart\Chart */ - public static function readChart(SimpleXMLElement $chartElements, $chartName) + public function readChart(SimpleXMLElement $chartElements, $chartName) { - $namespacesChartMeta = $chartElements->getNamespaces(true); - $chartElementsC = $chartElements->children($namespacesChartMeta['c']); + $chartElementsC = $chartElements->children($this->cNamespace); $XaxisLabel = $YaxisLabel = $legend = $title = null; $dispBlanksAs = $plotVisOnly = null; @@ -60,7 +71,7 @@ class Chart switch ($chartElementKey) { case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { - $chartDetailsC = $chartDetails->children($namespacesChartMeta['c']); + $chartDetailsC = $chartDetails->children($this->cNamespace); switch ($chartDetailsKey) { case 'view3D': $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer'); @@ -75,24 +86,24 @@ class Chart foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { case 'layout': - $plotAreaLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $plotAreaLayout = $this->chartLayoutDetails($chartDetail); break; case 'catAx': if (isset($chartDetail->title)) { - $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } break; case 'dateAx': if (isset($chartDetail->title)) { - $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } break; case 'valAx': if (isset($chartDetail->title, $chartDetail->axPos)) { - $axisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + $axisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string'); switch ($axPos) { @@ -113,70 +124,72 @@ class Chart case 'barChart': case 'bar3DChart': $barDirection = self::getAttribute($chartDetail->barDir, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotDirection($barDirection); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotDirection("$barDirection"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'lineChart': case 'line3DChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($chartDetail); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'areaChart': case 'area3DChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($chartDetail); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'doughnutChart': case 'pieChart': case 'pie3DChart': $explosion = isset($chartDetail->ser->explosion); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($explosion); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$explosion"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'scatterChart': + /** @var string */ $scatterStyle = self::getAttribute($chartDetail->scatterStyle, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle($scatterStyle); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'bubbleChart': $bubbleScale = self::getAttribute($chartDetail->bubbleScale, 'val', 'integer'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($bubbleScale); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$bubbleScale"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'radarChart': + /** @var string */ $radarStyle = self::getAttribute($chartDetail->radarStyle, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle($radarStyle); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'surfaceChart': case 'surface3DChart': $wireFrame = self::getAttribute($chartDetail->wireframe, 'val', 'boolean'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($wireFrame); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$wireFrame"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'stockChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($plotAreaLayout); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($plotAreaLayout); break; } @@ -185,7 +198,7 @@ class Chart $plotAreaLayout = new Layout(); } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); - self::setChartAttributes($plotAreaLayout, $plotAttributes); + $this->setChartAttributes($plotAreaLayout, $plotAttributes); break; case 'plotVisOnly': @@ -197,7 +210,7 @@ class Chart break; case 'title': - $title = self::chartTitle($chartDetails, $namespacesChartMeta); + $title = $this->chartTitle($chartDetails); break; case 'legend': @@ -215,19 +228,19 @@ class Chart break; case 'layout': - $legendLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $legendLayout = $this->chartLayoutDetails($chartDetail); break; } } - $legend = new Legend($legendPos, $legendLayout, $legendOverlay); + $legend = new Legend("$legendPos", $legendLayout, (bool) $legendOverlay); break; } } } } - $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, $dispBlanksAs, $XaxisLabel, $YaxisLabel); + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel); if (is_int($rotX)) { $chart->setRotX($rotX); } @@ -244,25 +257,25 @@ class Chart return $chart; } - private static function chartTitle(SimpleXMLElement $titleDetails, array $namespacesChartMeta) + private function chartTitle(SimpleXMLElement $titleDetails): Title { $caption = []; $titleLayout = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { switch ($titleDetailKey) { case 'tx': - $titleDetails = $chartDetail->rich->children($namespacesChartMeta['a']); + $titleDetails = $chartDetail->rich->children($this->aNamespace); foreach ($titleDetails as $titleKey => $titleDetail) { switch ($titleKey) { case 'p': - $titleDetailPart = $titleDetail->children($namespacesChartMeta['a']); - $caption[] = self::parseRichText($titleDetailPart); + $titleDetailPart = $titleDetail->children($this->aNamespace); + $caption[] = $this->parseRichText($titleDetailPart); } } break; case 'layout': - $titleLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $titleLayout = $this->chartLayoutDetails($chartDetail); break; } @@ -271,12 +284,12 @@ class Chart return new Title($caption, $titleLayout); } - private static function chartLayoutDetails($chartDetail, $namespacesChartMeta) + private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout { if (!isset($chartDetail->manualLayout)) { return null; } - $details = $chartDetail->manualLayout->children($namespacesChartMeta['c']); + $details = $chartDetail->manualLayout->children($this->cNamespace); if ($details === null) { return null; } @@ -288,13 +301,13 @@ class Chart return new Layout($layout); } - private static function chartDataSeries($chartDetail, $namespacesChartMeta, $plotType) + private function chartDataSeries(SimpleXMLElement $chartDetail, string $plotType): DataSeries { $multiSeriesType = null; $smoothLine = false; $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = []; - $seriesDetailSet = $chartDetail->children($namespacesChartMeta['c']); + $seriesDetailSet = $chartDetail->children($this->cNamespace); foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) { switch ($seriesDetailKey) { case 'grouping': @@ -322,11 +335,11 @@ class Chart break; case 'tx': - $seriesLabel[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + $seriesLabel[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail); break; case 'spPr': - $children = $seriesDetail->children($namespacesChartMeta['a']); + $children = $seriesDetail->children($this->aNamespace); $ln = $children->ln; $lineWidth = self::getAttribute($ln, 'w', 'string'); if (is_countable($ln->noFill) && count($ln->noFill) === 1) { @@ -343,9 +356,9 @@ class Chart $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string'); $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; if (count($seriesDetail->spPr) === 1) { - $ln = $seriesDetail->spPr->children($namespacesChartMeta['a']); + $ln = $seriesDetail->spPr->children($this->aNamespace); if (count($ln->solidFill) === 1) { - $srgbClr = self::getattribute($ln->solidFill->srgbClr, 'val', 'string'); + $srgbClr = self::getAttribute($ln->solidFill->srgbClr, 'val', 'string'); } } @@ -355,19 +368,19 @@ class Chart break; case 'cat': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail); break; case 'val': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); break; case 'xVal': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); break; case 'yVal': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); break; case 'bubble3D': @@ -423,17 +436,21 @@ class Chart } } + /** @phpstan-ignore-next-line */ return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); } - private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, ?string $marker = null, ?string $srgbClr = null, ?string $pointSize = null) + /** + * @return mixed + */ + private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?string $srgbClr = null, ?string $pointSize = null) { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->strRef->strCache)) { - $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -442,9 +459,9 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->numRef->numCache)) { - $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); + $seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace)); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -453,10 +470,10 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -465,10 +482,10 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -480,7 +497,7 @@ class Chart return null; } - private static function chartDataSeriesValues($seriesValueSet, $dataType = 'n') + private function chartDataSeriesValues(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array { $seriesVal = []; $formatCode = ''; @@ -500,7 +517,7 @@ class Chart $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); if ($dataType == 's') { $seriesVal[$pointVal] = (string) $seriesValue->v; - } elseif ($seriesValue->v === ExcelError::NA()) { + } elseif ((string) $seriesValue->v === ExcelError::NA()) { $seriesVal[$pointVal] = null; } else { $seriesVal[$pointVal] = (float) $seriesValue->v; @@ -517,7 +534,7 @@ class Chart ]; } - private static function chartDataSeriesValuesMultiLevel($seriesValueSet, $dataType = 'n') + private function chartDataSeriesValuesMultiLevel(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array { $seriesVal = []; $formatCode = ''; @@ -538,7 +555,7 @@ class Chart $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); if ($dataType == 's') { $seriesVal[$pointVal][] = (string) $seriesValue->v; - } elseif ($seriesValue->v === ExcelError::NA()) { + } elseif ((string) $seriesValue->v === ExcelError::NA()) { $seriesVal[$pointVal] = null; } else { $seriesVal[$pointVal][] = (float) $seriesValue->v; @@ -556,7 +573,7 @@ class Chart ]; } - private static function parseRichText(SimpleXMLElement $titleDetailPart) + private function parseRichText(SimpleXMLElement $titleDetailPart): RichText { $value = new RichText(); $objText = null; @@ -773,7 +790,10 @@ class Chart return $value; } - private static function readChartAttributes($chartDetail) + /** + * @param null|Layout|SimpleXMLElement $chartDetail + */ + private function readChartAttributes($chartDetail): array { $plotAttributes = []; if (isset($chartDetail->dLbls)) { @@ -806,7 +826,7 @@ class Chart /** * @param mixed $plotAttributes */ - private static function setChartAttributes(Layout $plotArea, $plotAttributes): void + private function setChartAttributes(Layout $plotArea, $plotAttributes): void { foreach ($plotAttributes as $plotAttributeKey => $plotAttributeValue) { switch ($plotAttributeKey) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 77cbb872..4ea26438 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -963,7 +963,7 @@ class Chart extends WriterPart if ($id1 !== '0') { $objWriter->startElement('c:crossAx'); - $objWriter->writeAttribute('val', $id2); + $objWriter->writeAttribute('val', $id1); $objWriter->endElement(); if ($xAxis->getAxisOptionsProperty('horizontal_crosses_value') !== null) { From bf46ff1e0cf4537f549c3e5d5a6bb3f8e3a9c3f9 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 25 May 2022 12:36:25 +0200 Subject: [PATCH 26/30] Explicit type-cast result from preg_replace() calls to string, cleans up a few phpstan issues --- phpstan-baseline.neon | 70 ------------------- .../Calculation/Calculation.php | 4 +- .../Calculation/DateTimeExcel/DateValue.php | 2 +- src/PhpSpreadsheet/Calculation/Functions.php | 2 +- .../Calculation/LookupRef/Helpers.php | 2 +- .../LookupRef/RowColumnInformation.php | 13 ++-- src/PhpSpreadsheet/Document/Properties.php | 6 +- src/PhpSpreadsheet/Helper/Html.php | 4 +- src/PhpSpreadsheet/Helper/Sample.php | 2 +- src/PhpSpreadsheet/Reader/Csv/Delimiter.php | 6 +- src/PhpSpreadsheet/Reader/Html.php | 2 +- .../Reader/Ods/FormulaTranslator.php | 24 +++---- src/PhpSpreadsheet/Reader/Xls.php | 6 +- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 2 +- src/PhpSpreadsheet/Reader/Xml.php | 4 +- src/PhpSpreadsheet/Reader/Xml/Properties.php | 2 +- src/PhpSpreadsheet/ReferenceHelper.php | 2 +- .../ConditionalFormatting/CellMatcher.php | 4 +- .../Wizard/WizardAbstract.php | 4 +- .../Style/NumberFormat/DateFormatter.php | 3 +- .../Style/NumberFormat/Formatter.php | 10 +-- .../NumberFormat/PercentageFormatter.php | 2 +- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 2 +- src/PhpSpreadsheet/Worksheet/Table.php | 2 +- src/PhpSpreadsheet/Writer/Html.php | 6 +- src/PhpSpreadsheet/Writer/Xls/Parser.php | 4 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 8 +-- 27 files changed, 63 insertions(+), 135 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a8da7936..6284ec2e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -125,11 +125,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Parameter \\#3 \\$formula of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:translateSeparator\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$cellStack has no type specified\\.$#" count: 1 @@ -750,11 +745,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/LookupRef/Offset.php - - - message: "#^Parameter \\#1 \\$columnAddress of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:columnIndexFromString\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php - - message: "#^Binary operation \"/\" between array\\|float\\|int\\|string and array\\|float\\|int\\|string results in an error\\.$#" count: 2 @@ -1710,11 +1700,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Helper/Html.php - - - message: "#^Parameter \\#1 \\$text of method PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\ITextElement\\:\\:setText\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$bold has no type specified\\.$#" count: 1 @@ -1815,11 +1800,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Helper/Sample.php - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\IOFactory\\:\\:createReader\\(\\) should return PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\IReader but returns object\\.$#" count: 1 @@ -2725,11 +2705,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/ReferenceHelper.php - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\Run\\:\\:\\$font \\(PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" count: 1 @@ -3725,26 +3700,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php - - - message: "#^Parameter \\#2 \\$subject of function preg_split expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php - - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\PercentageFormatter\\:\\:format\\(\\) has parameter \\$value with no type specified\\.$#" count: 1 path: src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php - - - message: "#^Parameter \\#1 \\$format of function sprintf expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\CellIterator\\:\\:adjustForExistingOnlyRange\\(\\) has no return type specified\\.$#" count: 1 @@ -4180,11 +4140,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Html.php - - - message: "#^Parameter \\#1 \\$string of function htmlspecialchars expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - message: "#^Parameter \\#1 \\$vAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapVAlign\\(\\) expects string, string\\|null given\\.$#" count: 1 @@ -4380,16 +4335,6 @@ parameters: count: 7 path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xls\\\\Parser\\:\\:\\$spreadsheet has no type specified\\.$#" count: 1 @@ -4490,21 +4435,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - - - message: "#^Parameter \\#2 \\$subject of function preg_match_all expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - message: "#^Parameter \\#4 \\$isError of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xls\\\\Worksheet\\:\\:writeBoolErr\\(\\) expects bool, int given\\.$#" count: 3 diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 7a48a7cb..65105f8e 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3197,7 +3197,7 @@ class Calculation string $toSeparator ): string { // Function Names - $formula = preg_replace($from, $to, $formula); + $formula = (string) preg_replace($from, $to, $formula); // Temporarily adjust matrix separators so that they won't be confused with function arguments $formula = self::translateSeparator(';', '|', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); @@ -4180,7 +4180,7 @@ class Calculation $length = strlen($val); if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { - $val = preg_replace('/\s/u', '', $val); + $val = (string) preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function $valToUpper = strtoupper($val); } else { diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index 466fe4a6..52543a72 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -49,7 +49,7 @@ class DateValue $baseYear = SharedDateHelper::getExcelCalendar(); $dateValue = trim($dateValue ?? '', '"'); // Strip any ordinals because they're allowed in Excel (English only) - $dateValue = preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue) ?? ''; + $dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); // Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany) $dateValue = str_replace(['/', '.', '-', ' '], ' ', $dateValue); diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 8075811f..1cc980b8 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -159,7 +159,7 @@ class Functions } elseif (!is_numeric($condition)) { if ($condition !== '""') { // Not an empty string // Escape any quotes in the string value - $condition = preg_replace('/"/ui', '""', $condition); + $condition = (string) preg_replace('/"/ui', '""', $condition); } $condition = Calculation::wrapResult(strtoupper($condition)); } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 28e8df89..7408a66e 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -43,7 +43,7 @@ class Helpers if ($namedRange !== null) { $workSheet = $namedRange->getWorkSheet(); $sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle(); - $value = preg_replace('/^=/', '', $namedRange->getValue()); + $value = (string) preg_replace('/^=/', '', $namedRange->getValue()); self::adjustSheetTitle($sheetTitle, $value); $cellAddress1 = $sheetTitle . $value; $cellAddress = $cellAddress1; diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php index 1f848045..8bce07e9 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -50,7 +50,7 @@ class RowColumnInformation if (is_array($cellAddress)) { foreach ($cellAddress as $columnKey => $value) { - $columnKey = preg_replace('/[^a-z]/i', '', $columnKey); + $columnKey = (string) preg_replace('/[^a-z]/i', '', $columnKey); return (int) Coordinate::columnIndexFromString($columnKey); } @@ -66,8 +66,8 @@ class RowColumnInformation [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/[^a-z]/i', '', $startAddress); - $endAddress = preg_replace('/[^a-z]/i', '', $endAddress); + $startAddress = (string) preg_replace('/[^a-z]/i', '', $startAddress); + $endAddress = (string) preg_replace('/[^a-z]/i', '', $endAddress); return range( (int) Coordinate::columnIndexFromString($startAddress), @@ -75,7 +75,7 @@ class RowColumnInformation ); } - $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); + $cellAddress = (string) preg_replace('/[^a-z]/i', '', $cellAddress); return (int) Coordinate::columnIndexFromString($cellAddress); } @@ -159,14 +159,13 @@ class RowColumnInformation [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/\D/', '', $startAddress); - $endAddress = preg_replace('/\D/', '', $endAddress); + $startAddress = (string) preg_replace('/\D/', '', $startAddress); + $endAddress = (string) preg_replace('/\D/', '', $endAddress); return array_map( function ($value) { return [$value]; }, - // @phpstan-ignore-next-line range($startAddress, $endAddress) ); } diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 2d461c59..afdeea99 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -171,9 +171,9 @@ class Properties if (is_numeric($timestamp)) { $timestamp = (float) $timestamp; } else { - $timestamp = preg_replace('/[.][0-9]*$/', '', $timestamp) ?? ''; - $timestamp = preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp) ?? ''; - $timestamp = preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp) ?? ''; + $timestamp = (string) preg_replace('/[.][0-9]*$/', '', $timestamp); + $timestamp = (string) preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp); + $timestamp = (string) preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp); $timestamp = (float) (new DateTime($timestamp))->format('U'); } } diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index ce5a3d1d..632efebc 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -642,7 +642,7 @@ class Html $text = ltrim($text); } // Trim any spaces immediately after a line break - $text = preg_replace('/\n */mu', "\n", $text); + $text = (string) preg_replace('/\n */mu', "\n", $text); $element->setText($text); } } @@ -792,7 +792,7 @@ class Html protected function parseTextNode(DOMText $textNode): void { - $domText = preg_replace( + $domText = (string) preg_replace( '/\s+/u', ' ', str_replace(["\r", "\n"], ' ', $textNode->nodeValue ?? '') diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 257a02a8..8ce37003 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -85,7 +85,7 @@ class Sample $file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); $info = pathinfo($file); $category = str_replace('_', ' ', $info['dirname']); - $name = str_replace('_', ' ', preg_replace('/(|\.php)/', '', $info['filename'])); + $name = str_replace('_', ' ', (string) preg_replace('/(|\.php)/', '', $info['filename'])); if (!in_array($category, ['.', 'boostrap', 'templates'])) { if (!isset($files[$category])) { $files[$category] = []; diff --git a/src/PhpSpreadsheet/Reader/Csv/Delimiter.php b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php index fc298957..029d4a18 100644 --- a/src/PhpSpreadsheet/Reader/Csv/Delimiter.php +++ b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php @@ -140,12 +140,12 @@ class Delimiter $line = $line . $newLine; // Drop everything that is enclosed to avoid counting false positives in enclosures - $line = preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line); + $line = (string) preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line); // See if we have any enclosures left in the line // if we still have an enclosure then we need to read the next line as well - } while (preg_match('/(' . $enclosure . ')/', $line ?? '') > 0); + } while (preg_match('/(' . $enclosure . ')/', $line) > 0); - return $line ?? false; + return ($line !== '') ? $line : false; } } diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 437d931d..4edf3cf8 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -619,7 +619,7 @@ class Html extends BaseReader { foreach ($element->childNodes as $child) { if ($child instanceof DOMText) { - $domText = preg_replace('/\s+/u', ' ', trim($child->nodeValue ?? '')); + $domText = (string) preg_replace('/\s+/u', ' ', trim($child->nodeValue ?? '')); if (is_string($cellContent)) { // simply append the text if the cell content is a plain text string $cellContent .= $domText; diff --git a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php index f2ad1a3d..7ba380e9 100644 --- a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php +++ b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php @@ -13,17 +13,17 @@ class FormulaTranslator // 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); + $excelAddress = (string) preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); // Cell range reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress ?? ''); + $excelAddress = (string) preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress); // Cell reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress ?? ''); + $excelAddress = (string) preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress); // Cell range reference - $excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress ?? ''); + $excelAddress = (string) preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress); // Simple cell reference - $excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress ?? ''); + $excelAddress = (string) preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress); - return $excelAddress ?? ''; + return $excelAddress; } public static function convertToExcelFormulaValue(string $openOfficeFormula): string @@ -38,15 +38,15 @@ class FormulaTranslator // so that conversion isn't done in string values if ($tKey = !$tKey) { // Cell range reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); + $value = (string) preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); // Cell reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); + $value = (string) preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value); // Cell range reference - $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); + $value = (string) preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value); // Simple cell reference - $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + $value = (string) preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value); // Convert references to defined names/formulae - $value = str_replace('$$', '', $value ?? ''); + $value = str_replace('$$', '', $value); // Convert ODS function argument separators to Excel function argument separators $value = Calculation::translateSeparator(';', ',', $value, $inFunctionBracesLevel); @@ -69,7 +69,7 @@ class FormulaTranslator Calculation::FORMULA_CLOSE_MATRIX_BRACE ); - $value = preg_replace('/COM\.MICROSOFT\./ui', '', $value); + $value = (string) preg_replace('/COM\.MICROSOFT\./ui', '', $value); } } diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index af1104f5..10f12dfe 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4521,17 +4521,17 @@ class Xls extends BaseReader // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!) if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) { - $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells); + $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells); } // first row '1' + last row '65536' indicates that full column is selected if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) { - $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells); + $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells); } // first column 'A' + last column 'IV' indicates that full row is selected if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) { - $selectedCells = preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells); + $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells); } $this->phpSheet->setSelectedCells($selectedCells); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index fdc56ce1..374da9f6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -22,7 +22,7 @@ class AutoFilter public function load(): void { // Remove all "$" in the auto filter range - $autoFilterRange = preg_replace('/\$/', '', $this->worksheetXml->autoFilter['ref'] ?? ''); + $autoFilterRange = (string) preg_replace('/\$/', '', $this->worksheetXml->autoFilter['ref'] ?? ''); if (strpos($autoFilterRange, ':') !== false) { $this->readAutoFilter($autoFilterRange, $this->worksheetXml); } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index bac40961..0b5e0966 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -90,9 +90,9 @@ class Xml extends BaseReader // Retrieve charset encoding if (preg_match('//m', $data, $matches)) { $charSet = strtoupper($matches[1]); - if (1 == preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet)) { + if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) { $data = StringHelper::convertEncoding($data, 'UTF-8', $charSet); - $data = preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); + $data = (string) preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); } } $this->fileContents = $data; diff --git a/src/PhpSpreadsheet/Reader/Xml/Properties.php b/src/PhpSpreadsheet/Reader/Xml/Properties.php index 1c3e421a..9e10526e 100644 --- a/src/PhpSpreadsheet/Reader/Xml/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xml/Properties.php @@ -44,7 +44,7 @@ class Properties foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { $propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']); - $propertyName = preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); + $propertyName = (string) preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); $this->processCustomProperty($docProps, $propertyName, $propertyValue, $propertyAttributes); } diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 4a6ee039..046c5894 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -687,7 +687,7 @@ class ReferenceHelper ksort($cellTokens); ksort($newCellTokens); } // Update cell references in the formula - $formulaBlock = str_replace('\\', '', preg_replace($cellTokens, $newCellTokens, $formulaBlock)); + $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock)); } } } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php index 7d520120..e0dc0efb 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php @@ -219,7 +219,7 @@ class CellMatcher foreach ($splitCondition as &$value) { // Only count/replace in alternating array entries (ie. not in quoted strings) if ($i = !$i) { - $value = preg_replace_callback( + $value = (string) preg_replace_callback( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', [$this, 'conditionCellAdjustment'], $value @@ -287,7 +287,7 @@ class CellMatcher $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); $expression = array_pop($conditions); - $expression = preg_replace( + $expression = (string) preg_replace( '/\b' . $this->referenceCell . '\b/i', (string) $this->wrapCellValue(), $expression diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php index df9daab3..3eb7d54e 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php @@ -133,7 +133,7 @@ abstract class WizardAbstract foreach ($splitCondition as &$value) { // Only count/replace in alternating array entries (ie. not in quoted strings) if ($i = !$i) { - $value = preg_replace_callback( + $value = (string) preg_replace_callback( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', function ($matches) use ($referenceColumnIndex, $referenceRow) { return self::reverseCellAdjustment($matches, $referenceColumnIndex, $referenceRow); @@ -174,7 +174,7 @@ abstract class WizardAbstract foreach ($splitCondition as &$value) { // Only count/replace in alternating array entries (ie. not in quoted strings) if ($i = !$i) { - $value = preg_replace_callback( + $value = (string) preg_replace_callback( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', [$this, 'conditionCellAdjustment'], $value diff --git a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php index b8e14b8b..6c4d9d6b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php @@ -129,11 +129,10 @@ class DateFormatter // but we don't want to change any quoted strings /** @var callable */ $callable = [self::class, 'setLowercaseCallback']; - $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format); + $format = (string) preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format); // Only process the non-quoted blocks for date format characters - /** @phpstan-ignore-next-line */ $blocks = explode('"', $format); foreach ($blocks as $key => &$block) { if ($key % 2 == 0) { diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 01407e64..3e4bdc46 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -51,12 +51,12 @@ class Formatter for ($idx = 0; $idx < $cnt; ++$idx) { if (preg_match($color_regex, $sections[$idx], $matches)) { $colors[$idx] = $matches[0]; - $sections[$idx] = preg_replace($color_regex, '', $sections[$idx]); + $sections[$idx] = (string) preg_replace($color_regex, '', $sections[$idx]); } if (preg_match($cond_regex, $sections[$idx], $matches)) { $condops[$idx] = $matches[1]; $condvals[$idx] = $matches[2]; - $sections[$idx] = preg_replace($cond_regex, '', $sections[$idx]); + $sections[$idx] = (string) preg_replace($cond_regex, '', $sections[$idx]); } } $color = $colors[0]; @@ -112,7 +112,7 @@ class Formatter return $value; } - $format = preg_replace_callback( + $format = (string) preg_replace_callback( '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', function ($matches) { return str_replace('.', chr(0x00), $matches[0]); @@ -121,7 +121,7 @@ class Formatter ); // Convert any other escaped characters to quoted strings, e.g. (\T to "T") - $format = preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); + $format = (string) preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal) $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format); @@ -130,7 +130,7 @@ class Formatter // In Excel formats, "_" is used to add spacing, // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space - $format = preg_replace('/_.?/ui', ' ', $format); + $format = (string) preg_replace('/_.?/ui', ' ', $format); // Let's begin inspecting the format and converting the value to a formatted string diff --git a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php index 334c40df..f4d3412b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -35,7 +35,7 @@ class PercentageFormatter extends BaseFormatter $wholePartSize += $decimalPartSize; $replacement = "{$wholePartSize}.{$decimalPartSize}"; - $mask = preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); + $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); /** @var float */ $valueFloat = $value; diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index dd33d5d5..05b2e9a0 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -145,7 +145,7 @@ class AutoFilter $this->evaluated = false; if ($this->workSheet !== null) { $thisrange = $this->range; - $range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? ''; + $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 66839d41..ffdbf9a7 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -212,7 +212,7 @@ class Table { if ($this->workSheet !== null) { $thisrange = $this->range; - $range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? ''; + $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 45d4ce85..3ef65928 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -662,13 +662,13 @@ class Html extends BaseWriter $filename = $drawing->getPath(); // Strip off eventual '.' - $filename = preg_replace('/^[.]/', '', $filename); + $filename = (string) preg_replace('/^[.]/', '', $filename); // Prepend images root $filename = $this->getImagesRoot() . $filename; // Strip off eventual '.' if followed by non-/ - $filename = preg_replace('@^[.]([^/])@', '$1', $filename); + $filename = (string) preg_replace('@^[.]([^/])@', '$1', $filename); // Convert UTF8 data to PCDATA $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); @@ -1326,7 +1326,7 @@ class Html extends BaseWriter // Converts the cell content so that spaces occuring at beginning of each new line are replaced by   // Example: " Hello\n to the world" is converted to "  Hello\n to the world" - $cellData = preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); + $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); // convert newline "\n" to '
' $cellData = nl2br($cellData); diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 4033fd53..618b639d 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -778,8 +778,8 @@ class Parser */ private function getRefIndex($ext_ref) { - $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. - $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. + $ext_ref = (string) preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. + $ext_ref = (string) preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' // Check if there is a sheet range eg., Sheet1:Sheet2. diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 74e145cc..6c1c97cb 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -1039,7 +1039,7 @@ class Worksheet extends BIFFwriter $record = 0x01B8; // Record identifier // Strip URL type - $url = preg_replace('/^internal:/', '', $url); + $url = (string) preg_replace('/^internal:/', '', $url); // Pack the undocumented parts of the hyperlink stream $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); @@ -1095,8 +1095,8 @@ class Worksheet extends BIFFwriter // Strip URL type and change Unix dir separator to Dos style (if needed) // - $url = preg_replace('/^external:/', '', $url); - $url = preg_replace('/\//', '\\', $url); + $url = (string) preg_replace('/^external:/', '', $url); + $url = (string) preg_replace('/\//', '\\', $url); // Determine if the link is relative or absolute: // relative if link contains no dir separator, "somefile.xls" @@ -1125,7 +1125,7 @@ class Worksheet extends BIFFwriter $up_count = pack('v', $up_count); // Store the short dos dir name (null terminated) - $dir_short = preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; + $dir_short = (string) preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; // Store the long dir name as a wchar string (non-null terminated) $dir_long = $dir_long . "\0"; From ff8f80104356721fe188635ec80132e95a826091 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 25 May 2022 13:32:06 +0200 Subject: [PATCH 27/30] Use preg_replace() with arrays, where appropriate, because it's more efficient that caling several times --- .../Reader/Ods/FormulaTranslator.php | 49 ++++++++++++------- src/PhpSpreadsheet/Writer/Xls/Parser.php | 3 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 3 +- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php index 7ba380e9..4abdf11e 100644 --- a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php +++ b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php @@ -13,15 +13,23 @@ class FormulaTranslator // 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 = (string) preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); - // Cell range reference in another sheet - $excelAddress = (string) preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress); - // Cell reference in another sheet - $excelAddress = (string) preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress); - // Cell range reference - $excelAddress = (string) preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress); - // Simple cell reference - $excelAddress = (string) preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress); + $excelAddress = (string) preg_replace( + [ + '/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', + '/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', // Cell range reference in another sheet + '/\$?([^\.]+)\.([^\.]+)/miu', // Cell reference in another sheet + '/\.([^\.]+):\.([^\.]+)/miu', // Cell range reference + '/\.([^\.]+)/miu', // Simple cell reference + ], + [ + '$1!$2:$4', + '$1!$2:$3', + '$1!$2', + '$1:$2', + '$1', + ], + $excelAddress + ); return $excelAddress; } @@ -37,14 +45,21 @@ class FormulaTranslator // Only replace in alternate array entries (i.e. non-quoted blocks) // so that conversion isn't done in string values if ($tKey = !$tKey) { - // Cell range reference in another sheet - $value = (string) preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); - // Cell reference in another sheet - $value = (string) preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value); - // Cell range reference - $value = (string) preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value); - // Simple cell reference - $value = (string) preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value); + $value = (string) preg_replace( + [ + '/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference in another sheet + '/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet + '/\[\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference + '/\[\.([^\.]+)\]/miu', // Simple cell reference + ], + [ + '$1!$2:$3', + '$1!$2', + '$1:$2', + '$1', + ], + $value + ); // Convert references to defined names/formulae $value = str_replace('$$', '', $value); diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 618b639d..2f75f908 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -778,8 +778,7 @@ class Parser */ private function getRefIndex($ext_ref) { - $ext_ref = (string) preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. - $ext_ref = (string) preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. + $ext_ref = (string) preg_replace(["/^'/", "/'$/"], ['', ''], $ext_ref); // Remove leading and trailing ' if any. $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' // Check if there is a sheet range eg., Sheet1:Sheet2. diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 6c1c97cb..37865518 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -1095,8 +1095,7 @@ class Worksheet extends BIFFwriter // Strip URL type and change Unix dir separator to Dos style (if needed) // - $url = (string) preg_replace('/^external:/', '', $url); - $url = (string) preg_replace('/\//', '\\', $url); + $url = (string) preg_replace(['/^external:/', '/\//'], ['', '\\'], $url); // Determine if the link is relative or absolute: // relative if link contains no dir separator, "somefile.xls" From 8089c9bf20c6af0c06f312d1a3754842d4942927 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 25 May 2022 18:38:54 +0200 Subject: [PATCH 28/30] Declare a few return datatypes --- phpstan-baseline.neon | 25 ------------------- .../Calculation/TextData/Trim.php | 4 +-- src/PhpSpreadsheet/Reader/Xlsx.php | 12 ++++----- src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php | 2 +- 4 files changed, 9 insertions(+), 34 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6284ec2e..5a90a21b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2265,11 +2265,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:boolean\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:boolean\\(\\) has parameter \\$value with no type specified\\.$#" count: 1 @@ -2340,11 +2335,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has parameter \\$add with no type specified\\.$#" count: 1 @@ -2405,21 +2395,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:stripWhiteSpaceFromStyleString\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:stripWhiteSpaceFromStyleString\\(\\) has parameter \\$string with no type specified\\.$#" count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:toCSSArray\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:toCSSArray\\(\\) has parameter \\$style with no type specified\\.$#" count: 1 @@ -4885,8 +4865,3 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Xlfn\\:\\:addXlfn\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php - diff --git a/src/PhpSpreadsheet/Calculation/TextData/Trim.php b/src/PhpSpreadsheet/Calculation/TextData/Trim.php index e52c0095..27eceb93 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Trim.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Trim.php @@ -14,7 +14,7 @@ class Trim * @param mixed $stringValue String Value to check * Or can be an array of values * - * @return null|array|string + * @return array|string * If an array of values is passed as the argument, then the returned result will also be an array * with the same dimensions */ @@ -26,7 +26,7 @@ class Trim $stringValue = Helpers::extractString($stringValue); - return preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); + return (string) preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); } /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index df2b36c1..455fe07a 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1461,7 +1461,7 @@ class Xlsx extends BaseReader } // unparsed drawing AlternateContent - $xmlAltDrawing = $this->loadZip($fileDrawing, Namespaces::COMPATIBILITY); + $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY); if ($xmlAltDrawing->AlternateContent) { foreach ($xmlAltDrawing->AlternateContent as $alternateContent) { @@ -1821,12 +1821,12 @@ class Xlsx extends BaseReader return $array[$key] ?? null; } - private static function dirAdd($base, $add) + private static function dirAdd($base, $add): string { - return preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); + return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } - private static function toCSSArray($style) + private static function toCSSArray($style): array { $style = self::stripWhiteSpaceFromStyleString($style); @@ -1857,12 +1857,12 @@ class Xlsx extends BaseReader return $style; } - public static function stripWhiteSpaceFromStyleString($string) + public static function stripWhiteSpaceFromStyleString($string): string { return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); } - private static function boolean($value) + private static function boolean($value): bool { if (is_object($value)) { $value = (string) $value; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php index c88ef245..6fc0c66a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -152,7 +152,7 @@ class Xlfn */ public static function addXlfn(string $funcstring): string { - return preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring); + return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring); } /** From 3302701ae73297513190f6ab39577000190a9eff Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 26 May 2022 15:20:41 +0200 Subject: [PATCH 29/30] Declare a few return datatypes --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ docs/index.md | 27 ++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a94e0d3..40b025e7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,44 @@ PhpSpreadsheet is a library written in pure PHP and offers a set of classes that allow you to read and write various spreadsheet file formats such as Excel and LibreOffice Calc. +## PHP version support + +LTS: Support for PHP versions will only be maintained for a period of six months beyond the +[end of life of that PHP version](https://www.php.net/eol.php). + +Currently the required PHP minimum version is PHP __7.3__. + +See the `composer.json` for other requirements. + +## Installation + +Use [composer](https://getcomposer.org) to install PhpSpreadsheet into your project: + +```sh +composer require phpoffice/phpspreadsheet +``` + +If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: +```json lines +{ + "require": { + "phpoffice/phpspreadsheet": "^1.23" + }, + "config": { + "platform": { + "php": "7.3" + } + } +} +``` +and then run +```sh +composer install +``` +to ensure that the correct dependencies are retrieved to match your deployment environment. + +See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. + ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). diff --git a/docs/index.md b/docs/index.md index 98a2d3d8..ff137c26 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,9 +30,14 @@ for details. ### PHP version support -Support for PHP versions will only be maintained for a period of six months beyond the +LTS: Support for PHP versions will only be maintained for a period of six months beyond the [end of life of that PHP version](https://www.php.net/eol.php). +Currently the required PHP minimum version is PHP 7.3. The last PHP release was 7.3.33 on 6th December 2021, so PhpSpreadsheet will support PHP 7.3 until 6th June 2022. +PHP 7.4 is officially [End of Life](https://www.php.net/supported-versions.php) on 28th November 2022, and PhpSpreadsheet will continue to support PHP 7.4 for six months after that date. + +See the `composer.json` for other requirements. + ## Installation Use [composer](https://getcomposer.org) to install PhpSpreadsheet into your project: @@ -47,6 +52,26 @@ Or also download the documentation and samples if you plan to use them: composer require phpoffice/phpspreadsheet --prefer-source ``` +If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: +```json lines +{ + "require": { + "phpoffice/phpspreadsheet": "^1.23" + }, + "config": { + "platform": { + "php": "7.3" + } + } +} +``` +and then run +```sh +composer install +``` +to ensure that the correct dependencies are retrieved to match your deployment environment. + +See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. ## Hello World From ab52991dc66f16d3aa531658f934374eb12cd7bb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 28 May 2022 18:45:47 -0700 Subject: [PATCH 30/30] More Bubble Chart Fixes (#2856) Continuing the work from #2828, #2841, #2846, and #2852. This is probably my last change in this area for a while. Bubble charts can have bubbles of different sizes. Phpspreadsheet had not supported this. Openpyxl comes with sample code to generate such a chart. I was especially drawn to that solution because its namespace usage would have been unexpected before 2852. And it turned out to come with other surprises - use of absolute paths in the .rels files (PhpSpreadsheet expected only relative), use of a one-cell anchor to place the chart (PhpSpreadsheet expected two-cell anchor or absolute positioning), plaintext in the legend (Phpspreadsheet expected RichText), no cached values for chart data. Excel handles the file okay, and this PR makes sure PhpSpreadsheet does as well. This file is now Samples/Templates/32readwriteBubbleChart2, and is used for both generating a sample output file and in formal tests. A new sample in the 33* series demonstrates how to code these. --- phpstan-baseline.neon | 2 +- samples/Chart/33_Chart_create_bubble.php | 124 ++++++++++++++++++ .../templates/32readwriteBubbleChart2.xlsx | Bin 0 -> 6607 bytes src/PhpSpreadsheet/Chart/Chart.php | 15 +++ src/PhpSpreadsheet/Chart/DataSeries.php | 29 ++++ src/PhpSpreadsheet/Chart/DataSeriesValues.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 39 +++++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 22 +++- src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php | 2 + src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 84 +++++++----- src/PhpSpreadsheet/Writer/Xlsx/Drawing.php | 13 ++ .../Reader/Xlsx/ChartsOpenpyxlTest.php | 115 ++++++++++++++++ 12 files changed, 408 insertions(+), 39 deletions(-) create mode 100644 samples/Chart/33_Chart_create_bubble.php create mode 100644 samples/templates/32readwriteBubbleChart2.xlsx create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/ChartsOpenpyxlTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5a90a21b..778090a8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4547,7 +4547,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 43 + count: 42 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - diff --git a/samples/Chart/33_Chart_create_bubble.php b/samples/Chart/33_Chart_create_bubble.php new file mode 100644 index 00000000..33feea62 --- /dev/null +++ b/samples/Chart/33_Chart_create_bubble.php @@ -0,0 +1,124 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['Number of Products', 'Sales in USD', 'Market share'], + [14, 12200, 15], + [20, 60000, 33], + [18, 24400, 10], + [22, 32000, 42], + [], + [12, 8200, 18], + [15, 50000, 30], + [19, 22400, 15], + [25, 25000, 50], + ] +); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker + +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['2013']), // 2013 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['2014']), // 2014 +]; + +// Set the X-Axis values +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesCategories = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$7:$A$10', null, 4), +]; + +// Set the Y-Axis values +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$7:$B$10', null, 4), +]; + +// Set the Z-Axis values (bubble size) +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesBubbles = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$7:$C$10', null, 4), +]; + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_BUBBLECHART, // plotType + null, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $dataSeriesCategories, // plotCategory + $dataSeriesValues // plotValues +); +$series->setPlotBubbleSizes($dataSeriesBubbles); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +// Create the chart +$chart = new Chart( + 'chart1', // name + null, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('E1'); +$chart->setBottomRightPosition('M15'); + +// Add the chart to the worksheet +$worksheet->addChart($chart); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->getColumnDimension('B')->setAutoSize(true); +$worksheet->getColumnDimension('C')->setAutoSize(true); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteBubbleChart2.xlsx b/samples/templates/32readwriteBubbleChart2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..206cfaea4f625e4323051f852e9c24722b2efa06 GIT binary patch literal 6607 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 literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 80d3d5f3..ec6342c5 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -152,6 +152,9 @@ class Chart /** @var ?int */ private $perspective; + /** @var bool */ + private $oneCellAnchor = false; + /** * Create a new Chart. * @@ -743,4 +746,16 @@ class Chart return $this; } + + public function getOneCellAnchor(): bool + { + return $this->oneCellAnchor; + } + + public function setOneCellAnchor(bool $oneCellAnchor): self + { + $this->oneCellAnchor = $oneCellAnchor; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 067d30e5..dca1186e 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -107,6 +107,13 @@ class DataSeries */ private $plotValues = []; + /** + * Plot Bubble Sizes. + * + * @var DataSeriesValues[] + */ + private $plotBubbleSizes = []; + /** * Create a new DataSeries. * @@ -339,6 +346,28 @@ class DataSeries return false; } + /** + * Get Plot Bubble Sizes. + * + * @return DataSeriesValues[] + */ + public function getPlotBubbleSizes(): array + { + return $this->plotBubbleSizes; + } + + /** + * Set Plot Bubble Sizes. + * + * @param DataSeriesValues[] $plotBubbleSizes + */ + public function setPlotBubbleSizes(array $plotBubbleSizes): self + { + $this->plotBubbleSizes = $plotBubbleSizes; + + return $this; + } + /** * Get Number of Plot Series. * diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index cf3e0853..6747934a 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -328,7 +328,7 @@ class DataSeriesValues */ public function isMultiLevelSeries() { - if (count($this->dataValues) > 0) { + if (!empty($this->dataValues)) { return is_array(array_values($this->dataValues)[0]); } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 455fe07a..8562339b 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -157,6 +157,10 @@ class Xlsx extends BaseReader Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING, ]; + private const REL_TO_CHART = [ + Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART, + ]; + /** * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. * @@ -408,17 +412,21 @@ class Xlsx extends BaseReader // Read the theme first, because we need the colour scheme when reading the styles [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); + $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; + $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS); $theme = null; $this->styleReader = new Styles(); foreach ($wbRels->Relationship as $relx) { $rel = self::getAttributes($relx); $relTarget = (string) $rel['Target']; + if (substr($relTarget, 0, 4) === '/xl/') { + $relTarget = substr($relTarget, 4); + } switch ($rel['Type']) { case "$xmlNamespaceBase/theme": $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; $themeOrderAdditional = count($themeOrderArray); - $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS); $xmlThemeName = self::getAttributes($xmlTheme); @@ -1204,12 +1212,20 @@ class Xlsx extends BaseReader . '/_rels/' . basename($fileWorksheet) . '.rels'; + if (substr($drawingFilename, 0, 7) === 'xl//xl/') { + $drawingFilename = substr($drawingFilename, 4); + } if ($zip->locateName($drawingFilename)) { $relsWorksheet = $this->loadZipNoNamespace($drawingFilename, Namespaces::RELATIONSHIPS); $drawings = []; foreach ($relsWorksheet->Relationship as $ele) { if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") { - $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + $eleTarget = (string) $ele['Target']; + if (substr($eleTarget, 0, 4) === '/xl/') { + $drawings[(string) $ele['Id']] = substr($eleTarget, 1); + } else { + $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + } } } @@ -1234,7 +1250,13 @@ class Xlsx extends BaseReader $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']); } elseif ($eleType === "$xmlNamespaceBase/chart") { if ($this->includeCharts) { - $charts[self::dirAdd($fileDrawing, $ele['Target'])] = [ + $eleTarget = (string) $ele['Target']; + if (substr($eleTarget, 0, 4) === '/xl/') { + $index = substr($eleTarget, 1); + } else { + $index = self::dirAdd($fileDrawing, $eleTarget); + } + $charts[$index] = [ 'id' => (string) $ele['Id'], 'sheet' => $docSheet->getTitle(), ]; @@ -1326,6 +1348,7 @@ class Xlsx extends BaseReader 'width' => $width, 'height' => $height, 'worksheetTitle' => $docSheet->getTitle(), + 'oneCellAnchor' => true, ]; } } @@ -1645,7 +1668,7 @@ class Xlsx extends BaseReader if ($this->includeCharts) { $chartEntryRef = ltrim((string) $contentType['PartName'], '/'); $chartElements = $this->loadZip($chartEntryRef); - $chartReader = new Chart(); + $chartReader = new Chart($chartNS, $drawingNS); $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml')); if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; @@ -1662,6 +1685,9 @@ class Xlsx extends BaseReader // oneCellAnchor or absoluteAnchor (e.g. Chart sheet) $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']); + if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) { + $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']); + } } } } @@ -1823,6 +1849,11 @@ class Xlsx extends BaseReader private static function dirAdd($base, $add): string { + $add = "$add"; + if (substr($add, 0, 4) === '/xl/') { + $add = substr($add, 4); + } + return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index f6b1edd5..98507af8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -305,7 +305,7 @@ class Chart { $multiSeriesType = null; $smoothLine = false; - $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = []; + $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = $seriesBubbles = []; $seriesDetailSet = $chartDetail->children($this->cNamespace); foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) { @@ -382,6 +382,10 @@ class Chart case 'yVal': $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + break; + case 'bubbleSize': + $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + break; case 'bubble3D': $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean'); @@ -435,9 +439,11 @@ class Chart } } } - /** @phpstan-ignore-next-line */ - return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); + $series = new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); + $series->setPlotBubbleSizes($seriesBubbles); + + return $series; } /** @@ -494,6 +500,16 @@ class Chart return $seriesValues; } + if (isset($seriesDetail->v)) { + return new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + null, + null, + 1, + [(string) $seriesDetail->v] + ); + } + return null; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index eb768d09..57a88bb0 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -76,5 +76,7 @@ class Namespaces const PURL_DRAWING = 'http://purl.oclc.org/ooxml/drawingml/main'; + const PURL_CHART = 'http://purl.oclc.org/ooxml/drawingml/chart'; + const PURL_WORKSHEET = 'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet'; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 4ea26438..08d578e6 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -284,9 +284,12 @@ class Chart extends WriterPart $objWriter->endElement(); } } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) { - $objWriter->startElement('c:bubbleScale'); - $objWriter->writeAttribute('val', 25); - $objWriter->endElement(); + $scale = ($plotGroup === null) ? '' : (string) $plotGroup->getPlotStyle(); + if ($scale !== '') { + $objWriter->startElement('c:bubbleScale'); + $objWriter->writeAttribute('val', $scale); + $objWriter->endElement(); + } $objWriter->startElement('c:showNegBubbles'); $objWriter->writeAttribute('val', 0); @@ -1326,7 +1329,23 @@ class Chart extends WriterPart } if ($groupType === DataSeries::TYPE_BUBBLECHART) { - $this->writeBubbles($plotSeriesValues, $objWriter); + if (!empty($plotGroup->getPlotBubbleSizes()[$plotSeriesIdx])) { + $objWriter->startElement('c:bubbleSize'); + $this->writePlotSeriesValues( + $plotGroup->getPlotBubbleSizes()[$plotSeriesIdx], + $objWriter, + $groupType, + 'num' + ); + $objWriter->endElement(); + if ($plotSeriesValues !== false) { + $objWriter->startElement('c:bubble3D'); + $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); + $objWriter->endElement(); + } + } else { + $this->writeBubbles($plotSeriesValues, $objWriter); + } } $objWriter->endElement(); @@ -1420,38 +1439,43 @@ class Chart extends WriterPart $objWriter->writeRawData($plotSeriesValues->getDataSource()); $objWriter->endElement(); - $objWriter->startElement('c:' . $dataType . 'Cache'); + $count = $plotSeriesValues->getPointCount(); + $source = $plotSeriesValues->getDataSource(); + $values = $plotSeriesValues->getDataValues(); + if ($count > 1 || ($count === 1 && "=$source" !== (string) $values[0])) { + $objWriter->startElement('c:' . $dataType . 'Cache'); - if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) { - if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) { - $objWriter->startElement('c:formatCode'); - $objWriter->writeRawData($plotSeriesValues->getFormatCode()); - $objWriter->endElement(); - } - } - - $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); - $objWriter->endElement(); - - $dataValues = $plotSeriesValues->getDataValues(); - if (!empty($dataValues)) { - if (is_array($dataValues)) { - foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { - $objWriter->startElement('c:pt'); - $objWriter->writeAttribute('idx', $plotSeriesKey); - - $objWriter->startElement('c:v'); - $objWriter->writeRawData($plotSeriesValue); - $objWriter->endElement(); + if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) { + if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) { + $objWriter->startElement('c:formatCode'); + $objWriter->writeRawData($plotSeriesValues->getFormatCode()); $objWriter->endElement(); } } + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->endElement(); + + $dataValues = $plotSeriesValues->getDataValues(); + if (!empty($dataValues)) { + if (is_array($dataValues)) { + foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', $plotSeriesKey); + + $objWriter->startElement('c:v'); + $objWriter->writeRawData($plotSeriesValue); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + } + + $objWriter->endElement(); // *Cache } - $objWriter->endElement(); - - $objWriter->endElement(); + $objWriter->endElement(); // *Ref } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 33eee1e0..7693c72c 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -108,6 +108,19 @@ class Drawing extends WriterPart $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); $objWriter->endElement(); + } elseif ($chart->getOneCellAnchor()) { + $objWriter->startElement('xdr:oneCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset'])); + $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); } else { $objWriter->startElement('xdr:absoluteAnchor'); $objWriter->startElement('xdr:pos'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartsOpenpyxlTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartsOpenpyxlTest.php new file mode 100644 index 00000000..a7343af5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartsOpenpyxlTest.php @@ -0,0 +1,115 @@ +setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + + self::assertSame('Sheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertEmpty($chart->getTitle()); + self::assertTrue($chart->getOneCellAnchor()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $labels = $dataSeries->getPlotLabels(); + self::assertCount(2, $labels); + self::assertSame(['2013'], $labels[0]->getDataValues()); + self::assertSame(['2014'], $labels[1]->getDataValues()); + + $plotCategories = $dataSeries->getPlotCategories(); + self::assertCount(2, $plotCategories); + $categories = $plotCategories[0]; + self::assertSame('Number', $categories->getDataType()); + self::assertSame('\'Sheet\'!$A$2:$A$5', $categories->getDataSource()); + self::assertFalse($categories->getBubble3D()); + $categories = $plotCategories[1]; + self::assertCount(2, $plotCategories); + self::assertSame('Number', $categories->getDataType()); + self::assertSame('\'Sheet\'!$A$7:$A$10', $categories->getDataSource()); + self::assertFalse($categories->getBubble3D()); + + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(2, $plotValues); + $values = $plotValues[0]; + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$B$2:$B$5', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + $values = $plotValues[1]; + self::assertCount(2, $plotValues); + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$B$7:$B$10', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + + $plotValues = $dataSeries->getPlotBubbleSizes(); + self::assertCount(2, $plotValues); + $values = $plotValues[0]; + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$C$2:$C$5', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + $values = $plotValues[1]; + self::assertCount(2, $plotValues); + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$C$7:$C$10', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + + $spreadsheet->disconnectWorksheets(); + } + + public function testXml(): void + { + $infile = self::DIRECTORY . '32readwriteBubbleChart2.xlsx'; + $file = 'zip://'; + $file .= $infile; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(0, substr_count($data, 'c:'), 'unusual choice of prefix'); + self::assertSame(0, substr_count($data, 'bubbleScale')); + self::assertSame(1, substr_count($data, '2013'), 'v tag for 2013'); + self::assertSame(1, substr_count($data, '2014'), 'v tag for 2014'); + self::assertSame(0, substr_count($data, 'numCache'), 'no cached values'); + } + $file = 'zip://'; + $file .= $infile; + $file .= '#xl/drawings/_rels/drawing1.xml.rels'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(1, substr_count($data, 'Target="/xl/charts/chart1.xml"'), 'Unusual absolute address in drawing rels file'); + } + $file = 'zip://'; + $file .= $infile; + $file .= '#xl/worksheets/_rels/sheet1.xml.rels'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(1, substr_count($data, 'Target="/xl/drawings/drawing1.xml"'), 'Unusual absolute address in worksheet rels file'); + } + } +}