From 5de82981d8583c7a8503ae41b16978f72eb4e82c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 16 Jul 2022 22:08:44 -0700 Subject: [PATCH 01/11] Html Reader Not Handling non-ASCII Data Correctly (#2943) * Html Reader Not Handling non-ASCII Data Correctly Fix #2942. Code was changed by #2894 because PHP8.2 will deprecate how it was being done. See linked issue for more details. Dom loadhtml assumes ISO-8859-1 in the absence of a charset attribute or equivalent, and there is no way to override that assumption. Sigh. The suggested replacements are unsuitable in one way or another. I think this will work with minimal disruption (replace ampersand, less than, and greater than with entities representing illegal characters, then use htmlentities, then restore ampersand, less than, and greater than). * Better Implementation Use regexp to escape non-ASCII. Less kludgey, less reliant on the vagaries of the PHP maintainers. * Additional Tests Test non-ASCII outside of cell contents: sheet title, image alt attribute. * Apply Same Change in Second Location Forgot to change loadFromString. * Additional Test Confirm escaped ampersand is handled correctly. --- src/PhpSpreadsheet/Reader/Html.php | 23 ++++++++++-- .../Reader/Html/HtmlImageTest.php | 4 +-- .../Reader/Html/Issue2942Test.php | 36 +++++++++++++++++++ tests/data/Reader/HTML/utf8chars.html | 28 +++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php create mode 100644 tests/data/Reader/HTML/utf8chars.html diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 3d859e15..76f128e0 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -201,7 +201,7 @@ class Html extends BaseReader /** * Loads Spreadsheet from file. */ - protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + public function loadSpreadsheetFromFile(string $filename): Spreadsheet { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -651,7 +651,13 @@ class Html extends BaseReader // Reload the HTML file into the DOM object try { $convert = $this->securityScanner->scanFile($filename); - $loaded = $dom->loadHTML($convert); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } @@ -662,6 +668,11 @@ class Html extends BaseReader return $this->loadDocument($dom, $spreadsheet); } + private static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + /** * Spreadsheet from content. * @@ -674,7 +685,13 @@ class Html extends BaseReader // Reload the HTML file into the DOM object try { $convert = $this->securityScanner->scan($content); - $loaded = $dom->loadHTML($convert); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php index cf4157e3..fe0117ca 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php @@ -13,7 +13,7 @@ class HtmlImageTest extends TestCase $html = ' - +
test imagetest image voilà
'; $filename = HtmlHelper::createHtml($html); @@ -24,7 +24,7 @@ class HtmlImageTest extends TestCase $drawing = $firstSheet->getDrawingCollection()[0]; self::assertEquals($imagePath, $drawing->getPath()); self::assertEquals('A1', $drawing->getCoordinates()); - self::assertEquals('test image', $drawing->getName()); + self::assertEquals('test image voilà', $drawing->getName()); self::assertEquals('100', $drawing->getWidth()); self::assertEquals('100', $drawing->getHeight()); } diff --git a/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php new file mode 100644 index 00000000..3a41805c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php @@ -0,0 +1,36 @@ +éàâèî'; + $reader = new Html(); + $spreadsheet = $reader->loadFromString($content); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + } + + public function testLoadFromFile(): void + { + $file = 'tests/data/Reader/HTML/utf8chars.html'; + $reader = new Html(); + $spreadsheet = $reader->loadSpreadsheetFromFile($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Test Utf-8 characters voilà', $sheet->getTitle()); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + self::assertSame('αβγδε', $sheet->getCell('B1')->getValue()); + self::assertSame('𐐁𐐂𐐃 & だけち', $sheet->getCell('A2')->getValue()); + self::assertSame('אבגדה', $sheet->getCell('B2')->getValue()); + self::assertSame('𪔀𪔁𪔂', $sheet->getCell('C2')->getValue()); + self::assertSame('᠐᠑᠒', $sheet->getCell('A3')->getValue()); + self::assertSame('അആ', $sheet->getCell('B3')->getValue()); + self::assertSame('กขฃ', $sheet->getCell('C3')->getValue()); + self::assertSame('✀✐✠', $sheet->getCell('D3')->getValue()); + } +} diff --git a/tests/data/Reader/HTML/utf8chars.html b/tests/data/Reader/HTML/utf8chars.html new file mode 100644 index 00000000..8d58c798 --- /dev/null +++ b/tests/data/Reader/HTML/utf8chars.html @@ -0,0 +1,28 @@ + + + + +Test Utf-8 characters voilà + + + + + + + + + + + + + + + + + + + + +
éàâèîαβγδε
𐐁𐐂𐐃 & だけちאבגדה𪔀𪔁𪔂
᠐᠑᠒അആกขฃ✀✐✠
+ + From a062521a18b52040863939b5b71ae93a6b66231d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 16 Jul 2022 22:30:11 -0700 Subject: [PATCH 02/11] Fixes for Surface Charts (#2933) Fix #2931. If surface charts are written with c:grouping tag, Excel will treat them as corrupt. Also, some 3D-ish Xml tags are required when 2D surface charts are written, else they will be treated as 3D. Also eliminate a duplicate line identified by #2932. --- src/PhpSpreadsheet/Chart/Axis.php | 1 - src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 54 +++++---- .../Chart/Issue2931Test.php | 111 ++++++++++++++++++ 3 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/Issue2931Test.php diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 1f55cf03..222ee8e8 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -144,7 +144,6 @@ class Axis extends Properties $this->setAxisOption('orientation', $axisOrientation); $this->setAxisOption('major_tick_mark', $majorTmt); $this->setAxisOption('minor_tick_mark', $minorTmt); - $this->setAxisOption('minor_tick_mark', $minorTmt); $this->setAxisOption('minimum', $minimum); $this->setAxisOption('maximum', $maximum); $this->setAxisOption('major_unit', $majorUnit); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 08935721..bfcf91a2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -72,30 +72,22 @@ class Chart extends WriterPart $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(); + $surface2D = false; + $plotArea = $chart->getPlotArea(); + if ($plotArea !== null) { + $seriesArray = $plotArea->getPlotGroup(); + foreach ($seriesArray as $series) { + if ($series->getPlotType() === DataSeries::TYPE_SURFACECHART) { + $surface2D = true; + + break; + } + } } + $this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90); + $this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D); + $this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D); + $this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D); $objWriter->endElement(); // view3D $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY()); @@ -124,6 +116,18 @@ class Chart extends WriterPart return $objWriter->getData(); } + private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void + { + if ($value === null && $surface2D) { + $value = $default; + } + if ($value !== null) { + $objWriter->startElement($tag); + $objWriter->writeAttribute('val', "$value"); + $objWriter->endElement(); + } + } + /** * Write Chart Title. */ @@ -913,8 +917,8 @@ class Chart extends WriterPart $objWriter->endElement(); } - if ($plotGroup->getPlotGrouping() !== null) { - $plotGroupingType = $plotGroup->getPlotGrouping(); + $plotGroupingType = $plotGroup->getPlotGrouping(); + if ($plotGroupingType !== null && $groupType !== DataSeries::TYPE_SURFACECHART && $groupType !== DataSeries::TYPE_SURFACECHART_3D) { $objWriter->startElement('c:grouping'); $objWriter->writeAttribute('val', $plotGroupingType); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php new file mode 100644 index 00000000..960eb04e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php @@ -0,0 +1,111 @@ +getActiveSheet(); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['5-6']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['6-7']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['7-8']), + ]; + + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [1, 2, 3, 4, 5, 6, 7, 8, 9]), + ]; + + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6, 6, 6, 5.9, 6, 6]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6.5, 7, 7, 7, 7, 7]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 7, 8, 8, 8, 8, 7.9]), + ]; + + $series = new DataSeries( + DataSeries::TYPE_SURFACECHART, + DataSeries::GROUPING_STANDARD, // grouping should not be written for surface chart + range(0, count($dataSeriesValues) - 1), + $dataSeriesLabels, + $xAxisTickValues, + $dataSeriesValues, + null, // plotDirection + false, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle + ); + + $plotArea = new PlotArea(null, [$series]); + $legend = new ChartLegend(ChartLegend::POSITION_BOTTOM, null, false); + + $title = new Title('График распредления температур в пределах кр'); + + $chart = new Chart( + 'chart2', + $title, + $legend, + $plotArea, + true, + DataSeries::EMPTY_AS_GAP, + ); + + $chart->setTopLeftPosition('$A$1'); + $chart->setBottomRightPosition('$P$20'); + + $sheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + + // rotX etc. should be generated for surfaceChart 2D + // even when unspecified. + $expectedXml2D = [ + '', + ]; + $expectedXml3D = [ + '', + ]; + $expectedXmlNoX = [ + 'c:grouping', + ]; + + // confirm that file contains expected tags + foreach ($expectedXml2D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $series->setPlotType(DataSeries::TYPE_SURFACECHART_3D); + $plotArea = new PlotArea(null, [$series]); + $chart->setPlotArea($plotArea); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + // confirm that file contains expected tags + foreach ($expectedXml3D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $spreadsheet->disconnectWorksheets(); + } +} From 4bf4278a39350a7500e4923b101243f010749ae3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 17 Jul 2022 06:27:56 -0700 Subject: [PATCH 03/11] VLOOKUP Breaks When Array Contains Null Cells (#2939) Fix #2934. Null is passed to StringHelper::strtolower which expects string. Same problem appears to be applicable to HLOOKUP. I noted the following problem in the code, but will document it here as well. Excel's results are not consistent when a non-numeric string is passed as the third parameter. For example, if cell Z1 contains `xyz`, Excel will return a REF error for function `VLOOKUP(whatever,whatever,Z1)`, but it returns a VALUE error for function `VLOOKUP(whatever,whatever,"xyz")`. I don't think PhpSpreadsheet can match both behaviors. For now, it will return VALUE for both, with similar results for other errors. While studying the returned errors, I realized there is something that needs to be deprecated. `ExcelError::$errorCodes` is a public static array. This means that a user can change its value, which should not be allowed. It is replaced by a constant. Since the original is public, I think it needs to stay, but with a deprecation notice; users can reference and change it, but it will be unused in the rest of the code. I suppose this might be considered a break in functionality (that should not have been allowed in the first place). --- .../Calculation/Information/ErrorValue.php | 2 +- .../Calculation/Information/ExcelError.php | 33 ++++++++++------ .../Calculation/LookupRef/HLookup.php | 2 +- .../Calculation/LookupRef/LookupBase.php | 12 +++++- .../Calculation/LookupRef/VLookup.php | 6 +-- .../Functions/LookupRef/VLookupTest.php | 34 ++++++++++++----- tests/data/Calculation/LookupRef/HLOOKUP.php | 10 +++++ tests/data/Calculation/LookupRef/VLOOKUP.php | 38 +++++++++++++++++-- 8 files changed, 106 insertions(+), 31 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index dda2c705..4b9f818f 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes, true); + return in_array($value, ExcelError::ERROR_CODES, true); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 5ca74a3e..06f38663 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -13,7 +13,7 @@ class ExcelError * * @var array */ - public static $errorCodes = [ + public const ERROR_CODES = [ 'null' => '#NULL!', // 1 'divisionbyzero' => '#DIV/0!', // 2 'value' => '#VALUE!', // 3 @@ -30,12 +30,23 @@ class ExcelError 'calculation' => '#CALC!', //14 ]; + /** + * List of error codes. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use ERROR_CODES constant instead. + * + * @var array + */ + public static $errorCodes = self::ERROR_CODES; + /** * @param mixed $value */ public static function throwError($value): string { - return in_array($value, self::$errorCodes, true) ? $value : self::$errorCodes['value']; + return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value']; } /** @@ -52,7 +63,7 @@ class ExcelError } $i = 1; - foreach (self::$errorCodes as $errorCode) { + foreach (self::ERROR_CODES as $errorCode) { if ($value === $errorCode) { return $i; } @@ -71,7 +82,7 @@ class ExcelError */ public static function null(): string { - return self::$errorCodes['null']; + return self::ERROR_CODES['null']; } /** @@ -83,7 +94,7 @@ class ExcelError */ public static function NAN(): string { - return self::$errorCodes['num']; + return self::ERROR_CODES['num']; } /** @@ -95,7 +106,7 @@ class ExcelError */ public static function REF(): string { - return self::$errorCodes['reference']; + return self::ERROR_CODES['reference']; } /** @@ -111,7 +122,7 @@ class ExcelError */ public static function NA(): string { - return self::$errorCodes['na']; + return self::ERROR_CODES['na']; } /** @@ -123,7 +134,7 @@ class ExcelError */ public static function VALUE(): string { - return self::$errorCodes['value']; + return self::ERROR_CODES['value']; } /** @@ -135,7 +146,7 @@ class ExcelError */ public static function NAME(): string { - return self::$errorCodes['name']; + return self::ERROR_CODES['name']; } /** @@ -145,7 +156,7 @@ class ExcelError */ public static function DIV0(): string { - return self::$errorCodes['divisionbyzero']; + return self::ERROR_CODES['divisionbyzero']; } /** @@ -155,6 +166,6 @@ class ExcelError */ public static function CALC(): string { - return self::$errorCodes['calculation']; + return self::ERROR_CODES['calculation']; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index d67718ce..e2d27bde 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -66,7 +66,7 @@ class HLookup extends LookupBase */ private static function hLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray[$column] as $rowKey => $rowData) { diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index 8e451fe4..a001540c 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -19,8 +19,16 @@ abstract class LookupBase protected static function validateIndexLookup(array $lookup_array, $index_number): int { - // index_number must be a number greater than or equal to 1 - if (!is_numeric($index_number) || $index_number < 1) { + // index_number must be a number greater than or equal to 1. + // Excel results are inconsistent when index is non-numeric. + // VLOOKUP(whatever, whatever, SQRT(-1)) yields NUM error, but + // VLOOKUP(whatever, whatever, cellref) yields REF error + // when cellref is '=SQRT(-1)'. So just try our best here. + // Similar results if string (literal yields VALUE, cellRef REF). + if (!is_numeric($index_number)) { + throw new Exception(ExcelError::throwError($index_number)); + } + if ($index_number < 1) { throw new Exception(ExcelError::VALUE()); } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 53a7badc..edeb1aa8 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -68,8 +68,8 @@ class VLookup extends LookupBase { reset($a); $firstColumn = key($a); - $aLower = StringHelper::strToLower($a[$firstColumn]); - $bLower = StringHelper::strToLower($b[$firstColumn]); + $aLower = StringHelper::strToLower((string) $a[$firstColumn]); + $bLower = StringHelper::strToLower((string) $b[$firstColumn]); if ($aLower == $bLower) { return 0; @@ -84,7 +84,7 @@ class VLookup extends LookupBase */ private static function vLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray as $rowKey => $rowData) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php index 4e05ea8a..5b7647be 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php @@ -3,26 +3,42 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class VLookupTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerVLOOKUP * * @param mixed $expectedResult + * @param mixed $value + * @param mixed $table + * @param mixed $index */ - public function testVLOOKUP($expectedResult, ...$args): void + public function testVLOOKUP($expectedResult, $value, $table, $index, ?bool $lookup = null): void { - $result = LookupRef::VLOOKUP(...$args); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + if (is_array($table)) { + $sheet->fromArray($table); + $dimension = $sheet->calculateWorksheetDimension(); + } else { + $sheet->getCell('A1')->setValue($table); + $dimension = 'A1'; + } + if ($lookup === null) { + $lastarg = ''; + } else { + $lastarg = $lookup ? ',TRUE' : ',FALSE'; + } + $sheet->getCell('Z98')->setValue($value); + $sheet->getCell('Z97')->setValue($index); + + $sheet->getCell('Z99')->setValue("=VLOOKUP(Z98,$dimension,Z97$lastarg)"); + $result = $sheet->getCell('Z99')->getCalculatedValue(); self::assertEquals($expectedResult, $result); + $spreadsheet->disconnectWorksheets(); } public function providerVLOOKUP(): array diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index 078ed007..ee1b919e 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -186,4 +186,14 @@ return [ 3, true, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, 102], + [null, 'Red'], + ], + 2, + false, + ], ]; diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index 2162d49a..21146638 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -98,7 +98,7 @@ return [ ['10y1', 7.0], ['10y2', 10.0], ], - 'NaN', + -5, ], [ '#REF!', @@ -111,9 +111,9 @@ return [ '#REF!', '10y2', [ - 2.0, - 7.0, - 10.0, + [2.0], + [7.0], + [10.0], ], 2.0, ], @@ -163,4 +163,34 @@ return [ 3, null, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, null], + [102, 'Red'], + ], + 2, + false, + ], + 'string supplied as index' => [ + '#VALUE!', + 102, + [ + [null, null], + [102, 'Red'], + ], + 'xyz', + false, + ], + 'num error propagated' => [ + '#NUM!', + 102, + [ + [null, null], + [102, 'Red'], + ], + '=SQRT(-1)', + false, + ], ]; From 051598ecfad3fdc508b728db7d2e1d500d33216a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 17 Jul 2022 06:46:22 -0700 Subject: [PATCH 04/11] Add Chart Axis Option textRotation (#2940) Fix #2705. Add to Axis class, Reader Xlsx Chart, and Writer Xlsx Chart. Add feature to an existing 32* sample, to an existing 33* sample, and a formal unit test. --- samples/Chart/33_Chart_create_scatter2.php | 1 + .../templates/32readwriteScatterChart8.xlsx | Bin 12409 -> 12438 bytes src/PhpSpreadsheet/Chart/Axis.php | 5 ++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 10 ++++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 34 ++++++++++++++++++ .../Chart/Charts32ScatterTest.php | 3 ++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index ef6353bb..eed87119 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -124,6 +124,7 @@ $dataSeriesValues[2]->setScatterLines(false); // points not connected $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); $yAxis = new Axis(); $yAxis->setLineStyleProperties( diff --git a/samples/templates/32readwriteScatterChart8.xlsx b/samples/templates/32readwriteScatterChart8.xlsx index fdd85b0ed3bf9fbac400a46a350da93f74e5d9a4..1d4080d795448bc8e74f527f22750a80208cd8be 100644 GIT binary patch delta 4502 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 self::AXIS_LABELS_NEXT_TO, 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, + 'textRotation' => null, ]; /** @@ -136,7 +137,8 @@ class Axis extends Properties ?string $minimum = null, ?string $maximum = null, ?string $majorUnit = null, - ?string $minorUnit = null + ?string $minorUnit = null, + ?string $textRotation = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -148,6 +150,7 @@ class Axis extends Properties $this->setAxisOption('maximum', $maximum); $this->setAxisOption('major_unit', $majorUnit); $this->setAxisOption('minor_unit', $minorUnit); + $this->setAxisOption('textRotation', $textRotation); } /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index eb425646..d49a5238 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -1279,5 +1279,15 @@ class Chart if (isset($chartDetail->minorUnit)) { $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); } + if (isset($chartDetail->txPr)) { + $children = $chartDetail->txPr->children($this->aNamespace); + if (isset($children->bodyPr)) { + /** @var string */ + $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string'); + if (is_numeric($textRotation)) { + $whichAxis->setAxisOption('textRotation', (string) Properties::xmlToAngle($textRotation)); + } + } + } } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index bfcf91a2..d9d96da6 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -573,6 +573,23 @@ class Chart extends WriterPart $objWriter->endElement(); } + $textRotation = $yAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } + $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $yAxis->getFillColorObject()); $this->writeEffects($objWriter, $yAxis); @@ -748,6 +765,23 @@ class Chart extends WriterPart $objWriter->endElement(); } + $textRotation = $xAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } + $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $xAxis->getFillColorObject()); $this->writeLineStyles($objWriter, $xAxis); diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index b655afc4..77b0a9b2 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -391,6 +391,9 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + self::assertEquals(45, $xAxis->getAxisOptionsProperty('textRotation')); + $plotArea = $chart->getPlotArea(); self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); From 69991111e05fca3ff7398e1e7fca9ebed33efec6 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 18 Jul 2022 21:50:48 +0200 Subject: [PATCH 05/11] 1.24.1 - 2022-07-18 ### Added - Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) ### Changed - Nothing ### Deprecated - Nothing ### Removed - Nothing ### Fixed - Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) - Additional Chart fixes - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) - VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f53aeda2..3356440d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## Unreleased - TBD +## 1.24.1 - 2022-07-18 ### Added -- Nothing +- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) ### Changed @@ -25,7 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing +- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) +- Additional Chart fixes + - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) + - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) + - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) +- VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) ## 1.24.0 - 2022-07-09 From 48d531c476681ba8316dd1702697f422db7a21f3 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 18 Jul 2022 22:13:48 +0200 Subject: [PATCH 06/11] Reset ChangeLog ready for next release --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3356440d..a2fd0cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## Unreleased - TBD + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + ## 1.24.1 - 2022-07-18 ### Added From 93b0de0414bda8aafac23ed28e811c64908feee2 Mon Sep 17 00:00:00 2001 From: Jonathan Goode Date: Wed, 20 Jul 2022 11:36:40 +0100 Subject: [PATCH 07/11] Typo --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c3a74ede..e2e66a4a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -34,7 +34,7 @@ If this is an issue with reading a specific spreadsheet file, then it may be app - [ ] Writer - [ ] Styles - [ ] Data Validations -- [ ] Formula Calulations +- [ ] Formula Calculations - [ ] Charts - [ ] AutoFilter - [ ] Form Elements From abc1d3db70c51d59ad60ae493bd58e7985392598 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 23 Jul 2022 07:46:32 -0700 Subject: [PATCH 08/11] Big Memory Leak in One Test (#2958) * Big Memory Leak in One Test Many tests leak a little by not issuing disconnectWorksheets at the end. CellMatcherTest leaks a lot by opening a spreadsheet many times. Phpunit reported a high watermark of 390MB; fixing CellMatcherTest brings that figure down to 242MB. I have also changed it to use a formal assertion in many cases where markTestSkipped was (IMO inappropriately) used. * Another Leak Issue2301Test lacks a disconnect. Adding one reduces HWM from 242MB to 224. --- .../Reader/Xlsx/Issue2301Test.php | 2 + .../ConditionalFormatting/CellMatcherTest.php | 105 +++++++----------- 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php index 4484b9e0..7a5829f7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php @@ -24,5 +24,7 @@ class Issue2301Test extends \PHPUnit\Framework\TestCase self::assertSame('Arial CE', $font->getName()); self::assertSame(9.0, $font->getSize()); self::assertSame('protected', $sheet->getCell('BT10')->getStyle()->getProtection()->getHidden()); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); } } diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php index 3c96403b..2c6d0da8 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php @@ -10,15 +10,24 @@ use PHPUnit\Framework\TestCase; class CellMatcherTest extends TestCase { /** - * @var Spreadsheet + * @var ?Spreadsheet */ protected $spreadsheet; - protected function setUp(): void + protected function loadSpreadsheet(): Spreadsheet { $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); - $this->spreadsheet = $reader->load($filename); + + return $reader->load($filename); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } /** @@ -26,16 +35,13 @@ class CellMatcherTest extends TestCase */ public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "{$cellAddress} is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -82,16 +88,13 @@ class CellMatcherTest extends TestCase */ public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -128,16 +131,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -167,16 +167,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -216,16 +213,13 @@ class CellMatcherTest extends TestCase */ public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -329,16 +323,13 @@ class CellMatcherTest extends TestCase */ public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -365,16 +356,13 @@ class CellMatcherTest extends TestCase */ public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -400,16 +388,13 @@ class CellMatcherTest extends TestCase */ public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -447,16 +432,13 @@ class CellMatcherTest extends TestCase */ public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::AssertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -486,16 +468,13 @@ class CellMatcherTest extends TestCase */ public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); From b8456105dd5ea44210b5f33dcac3ab732af4444d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 23 Jul 2022 08:06:13 -0700 Subject: [PATCH 09/11] Charts - Gradients, Transparency, Hidden Axes (#2950) Fix #2257. Fix #2929. Fix #2935 (probably in a way that will not satisfy the requester). 2257 and 2929 requested changes that ultimately affect the same section of code, so it's appropriate to deal with them together. 2257 requests the ability to make the chart background transparent (so that the Excel gridlines are visible beneath the chart), and the ability to hide an Axis. 2929 requests the ability to set a gradient background on the chart. --- samples/Chart/33_Chart_create_scatter3.php | 192 ++++++++++++++++++ samples/Chart/33_Chart_create_scatter4.php | 130 ++++++++++++ .../templates/32readwriteScatterChart10.xlsx | Bin 0 -> 12363 bytes .../templates/32readwriteScatterChart9.xlsx | Bin 0 -> 12554 bytes src/PhpSpreadsheet/Chart/Axis.php | 5 +- src/PhpSpreadsheet/Chart/Chart.php | 15 ++ src/PhpSpreadsheet/Chart/ChartColor.php | 32 ++- src/PhpSpreadsheet/Chart/DataSeries.php | 2 +- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 2 +- src/PhpSpreadsheet/Chart/PlotArea.php | 62 ++++++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 51 +++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 56 ++++- .../Chart/Charts32ScatterTest.php | 85 ++++++++ 13 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 samples/Chart/33_Chart_create_scatter3.php create mode 100644 samples/Chart/33_Chart_create_scatter4.php create mode 100644 samples/templates/32readwriteScatterChart10.xlsx create mode 100644 samples/templates/32readwriteScatterChart9.xlsx diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php new file mode 100644 index 00000000..b4fc97b1 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -0,0 +1,192 @@ +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), +]; + +// series 1 +// marker details +$dataSeriesValues[0] + ->setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); +$xAxis->setAxisOption('hidden', '1'); + +$yAxis = new Axis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$yAxis->setAxisOption('hidden', '1'); + +// 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_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setNoFill(true); +// 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 + null, //$yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); +$chart->setNoFill(true); + +// 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); + +// 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/Chart/33_Chart_create_scatter4.php b/samples/Chart/33_Chart_create_scatter4.php new file mode 100644 index 00000000..d42933b6 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter4.php @@ -0,0 +1,130 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// 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), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 +]; +// Set the X-Axis Labels +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// 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 +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), +]; + +// 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 +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); + +$pos1 = 0; // pos = 0% (extreme low side or lower left corner) +$brightness1 = 0; // 0% +$gsColor1 = new ChartColor(); +$gsColor1->setColorProperties('FF0000', 75, 'srgbClr', $brightness1); // red +$gradientStop1 = [$pos1, $gsColor1]; + +$pos2 = 0.5; // pos = 50% (middle) +$brightness2 = 0.5; // 50% +$gsColor2 = new ChartColor(); +$gsColor2->setColorProperties('FFFF00', 50, 'srgbClr', $brightness2); // yellow +$gradientStop2 = [$pos2, $gsColor2]; + +$pos3 = 1.0; // pos = 100% (extreme high side or upper right corner) +$brightness3 = 0.5; // 50% +$gsColor3 = new ChartColor(); +$gsColor3->setColorProperties('00B050', 50, 'srgbClr', $brightness3); // green +$gradientStop3 = [$pos3, $gsColor3]; + +$gradientFillStops = [ + $gradientStop1, + $gradientStop2, + $gradientStop3, +]; +$gradientFillAngle = 315.0; // 45deg above horiz + +$plotArea->setGradientFillProperties($gradientFillStops, $gradientFillAngle); + +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter 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 +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('H20'); + +// 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); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteScatterChart10.xlsx b/samples/templates/32readwriteScatterChart10.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7d5ac0d280f328c2685e6de590a78a2e30a9cb00 GIT binary patch literal 12363 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 PL;~ysi6~3|^RNF0pR-I- literal 0 HcmV?d00001 diff --git a/samples/templates/32readwriteScatterChart9.xlsx b/samples/templates/32readwriteScatterChart9.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3a0b51b2f85429405f5d07484620113e44ffb455 GIT binary patch literal 12554 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

KR-z20Xg!(?ezsbIY$dW zrc}%URBH?l^bwEt>IawW%z8s~=zU&nRoNims0v&j!m{!5*-$onTpK1T(R7bKdvSW_ zk9mKFQ=u*#L)MePtczXl4@-iW7A#&FTT(x2+8PwzF2dv{3K+{& zz*t84Yb+bu+y5_=fie8&krDUlXC))#4E!1?@Z3$N~l`A$wJe8TBbZ_#wV zX@rqcIuv&Ks-)v+MVc_1T5qb5SmwD>n6j{Q#9-0CQ!hoqCzjvR3n{IIvJ3FAnTqF| z{4VnzT3Fx6V_rVQ$XUnbvg@NM6TiOaI-xey{9&t2CcEH+%%g!E0*z|I+A9PeioHCe zR)(|%z(KJS#a_uWo_({Kv97;N@wSA}8rh}DieHpe*#~t(JUC{VYsTmk9TQ(-!)TQ- zq&!H4$hoGdVU#uwrP(eKFDZ6X3W^p(dZf*Ew^u9h9Rzq>TsFsu;wV`1sBTFF9|tYL zW1ocDGtiL{$j+z+7{hw2e$*&jBY!cBjt!>K3jGM_yFHEZSo=i5-_T|c9&{%1hN093 z5mjb=v_7aL=hUO~^A+V0-ovZWo2)8fU$n`swWFOK(&yOkjppnW00eSJ4>CBggFK5(#n{ETk+%G29UK;;> zPUp8N2nZqEFXR6*weu3^<*deUBtwLM{}cZ)x$zR^We@o`3Nmoh2DZ(rN2!JasM!VSvP%&@UkNL8^IUv7s9{g)?bCzOO%&IsNX2lz_Qd& z-SV;|^%CKw%>9i}Oa2St|0jB10=`s^zX7W${_fa+(UC6!U#hX+fEAQ~`1n_C_R{+A z+UK_|2#7Zo2*`gZp_k@=-){bDE=Btn^FKEqMHxsSi+)ajB7^J$iHN}P^SA#8ZMLTE literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 70ff1b31..bd7082ff 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -61,6 +61,7 @@ class Axis extends Properties 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, 'textRotation' => null, + 'hidden' => null, ]; /** @@ -138,7 +139,8 @@ class Axis extends Properties ?string $maximum = null, ?string $majorUnit = null, ?string $minorUnit = null, - ?string $textRotation = null + ?string $textRotation = null, + ?string $hidden = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -151,6 +153,7 @@ class Axis extends Properties $this->setAxisOption('major_unit', $majorUnit); $this->setAxisOption('minor_unit', $minorUnit); $this->setAxisOption('textRotation', $textRotation); + $this->setAxisOption('hidden', $hidden); } /** diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 3f89e6fb..e850f502 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -144,6 +144,9 @@ class Chart /** @var bool */ private $autoTitleDeleted = false; + /** @var bool */ + private $noFill = false; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -747,4 +750,16 @@ class Chart return $this; } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/ChartColor.php b/src/PhpSpreadsheet/Chart/ChartColor.php index 7f87e391..87f31020 100644 --- a/src/PhpSpreadsheet/Chart/ChartColor.php +++ b/src/PhpSpreadsheet/Chart/ChartColor.php @@ -24,15 +24,18 @@ class ChartColor /** @var ?int */ private $alpha; + /** @var ?int */ + private $brightness; + /** * @param string|string[] $value */ - public function __construct($value = '', ?int $alpha = null, ?string $type = null) + public function __construct($value = '', ?int $alpha = null, ?string $type = null, ?int $brightness = null) { if (is_array($value)) { $this->setColorPropertiesArray($value); } else { - $this->setColorProperties($value, $alpha, $type); + $this->setColorProperties($value, $alpha, $type, $brightness); } } @@ -72,10 +75,23 @@ class ChartColor return $this; } + public function getBrightness(): ?int + { + return $this->brightness; + } + + public function setBrightness(?int $brightness): self + { + $this->brightness = $brightness; + + return $this; + } + /** * @param null|float|int|string $alpha + * @param null|float|int|string $brightness */ - public function setColorProperties(?string $color, $alpha = null, ?string $type = null): self + public function setColorProperties(?string $color, $alpha = null, ?string $type = null, $brightness = null): self { if (empty($type) && !empty($color)) { if (substr($color, 0, 1) === '*') { @@ -99,6 +115,11 @@ class ChartColor } elseif (is_numeric($alpha)) { $this->setAlpha((int) $alpha); } + if ($brightness === null) { + $this->setBrightness(null); + } elseif (is_numeric($brightness)) { + $this->setBrightness((int) $brightness); + } return $this; } @@ -108,7 +129,8 @@ class ChartColor return $this->setColorProperties( $color['value'] ?? '', $color['alpha'] ?? null, - $color['type'] ?? null + $color['type'] ?? null, + $color['brightness'] ?? null ); } @@ -133,6 +155,8 @@ class ChartColor $retVal = $this->type; } elseif ($propertyName === 'alpha') { $retVal = $this->alpha; + } elseif ($propertyName === 'brightness') { + $retVal = $this->brightness; } return $retVal; diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 548145e7..5d33e96d 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -94,7 +94,7 @@ class DataSeries private $plotCategory = []; /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index bc0e04d1..cb5fa742 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -536,7 +536,7 @@ class DataSeriesValues extends Properties } /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index 4bd49ece..ccde4bb2 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -6,6 +6,30 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PlotArea { + /** + * No fill in plot area (show Excel gridlines through chart). + * + * @var bool + */ + private $noFill = false; + + /** + * PlotArea Gradient Stop list. + * Each entry is a 2-element array. + * First is position in %. + * Second is ChartColor. + * + * @var array[] + */ + private $gradientFillStops = []; + + /** + * PlotArea Gradient Angle. + * + * @var ?float + */ + private $gradientFillAngle; + /** * PlotArea Layout. * @@ -101,4 +125,42 @@ class PlotArea $plotSeries->refresh($worksheet); } } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self + { + $this->gradientFillStops = $gradientFillStops; + $this->gradientFillAngle = $gradientFillAngle; + + return $this; + } + + /** + * Get gradientFillAngle. + */ + public function getGradientFillAngle(): ?float + { + return $this->gradientFillAngle; + } + + /** + * Get gradientFillStops. + * + * @return array + */ + public function getGradientFillStops() + { + return $this->gradientFillStops; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index d49a5238..12ee0ade 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -73,8 +73,18 @@ class Chart $xAxis = new Axis(); $yAxis = new Axis(); $autoTitleDeleted = null; + $chartNoFill = false; + $gradientArray = []; + $gradientLin = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { + case 'spPr': + $possibleNoFill = $chartElementsC->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $chartNoFill = true; + } + + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetailsC = $chartDetails->children($this->cNamespace); @@ -95,8 +105,29 @@ class Chart $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; $catAxRead = false; + $plotNoFill = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { + case 'spPr': + $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $plotNoFill = true; + } + if (isset($possibleNoFill->gradFill->gsLst)) { + foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + /** @var float */ + $pos = self::getAttribute($gradient, 'pos', 'float'); + $gradientArray[] = [ + $pos / Properties::PERCENTAGE_MULTIPLIER, + new ChartColor($this->readColor($gradient)), + ]; + } + } + if (isset($possibleNoFill->gradFill->lin)) { + $gradientLin = Properties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + } + + break; case 'layout': $plotAreaLayout = $this->chartLayoutDetails($chartDetail); @@ -288,6 +319,12 @@ class Chart } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); $this->setChartAttributes($plotAreaLayout, $plotAttributes); + if ($plotNoFill) { + $plotArea->setNoFill(true); + } + if (!empty($gradientArray)) { + $plotArea->setGradientFillProperties($gradientArray, $gradientLin); + } break; case 'plotVisOnly': @@ -330,6 +367,9 @@ class Chart } } $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if ($chartNoFill) { + $chart->setNoFill(true); + } if (is_bool($autoTitleDeleted)) { $chart->setAutoTitleDeleted($autoTitleDeleted); } @@ -1147,6 +1187,7 @@ class Chart 'type' => null, 'value' => null, 'alpha' => null, + 'brightness' => null, ]; foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { if (isset($colorXml->$type)) { @@ -1159,6 +1200,13 @@ class Chart $result['alpha'] = ChartColor::alphaFromXml($alpha); } } + if (isset($colorXml->$type->lumMod)) { + /** @var string */ + $brightness = self::getAttribute($colorXml->$type->lumMod, 'val', 'string'); + if (is_numeric($brightness)) { + $result['brightness'] = ChartColor::alphaFromXml($brightness); + } + } break; } @@ -1236,6 +1284,9 @@ class Chart if (!isset($whichAxis)) { return; } + if (isset($chartDetail->delete)) { + $whichAxis->setAxisOption('hidden', (string) self::getAttribute($chartDetail->delete, 'val', 'string')); + } if (isset($chartDetail->numFmt)) { $whichAxis->setAxisNumberProperties( (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'), diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index d9d96da6..278b64e7 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -106,11 +106,17 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:chart + if ($chart->getNoFill()) { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + $objWriter->endElement(); // c:spPr + } $this->writePrintSettings($objWriter); - $objWriter->endElement(); + $objWriter->endElement(); // c:chartSpace // Return return $objWriter->getData(); @@ -360,8 +366,35 @@ class Chart extends WriterPart $this->writeSerAxis($objWriter, $id2, $id3); } } + $stops = $plotArea->getGradientFillStops(); + if ($plotArea->getNoFill() || !empty($stops)) { + $objWriter->startElement('c:spPr'); + if ($plotArea->getNoFill()) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + } + if (!empty($stops)) { + $objWriter->startElement('a:gradFill'); + $objWriter->startElement('a:gsLst'); + foreach ($stops as $stop) { + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0])); + $this->writeColor($objWriter, $stop[1], false); + $objWriter->endElement(); // a:gs + } + $objWriter->endElement(); // a:gsLst + $angle = $plotArea->getGradientFillAngle(); + if ($angle !== null) { + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', Properties::angleToXml($angle)); + $objWriter->endElement(); // a:lin + } + $objWriter->endElement(); // a:gradFill + } + $objWriter->endElement(); // c:spPr + } - $objWriter->endElement(); + $objWriter->endElement(); // c:plotArea } private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void @@ -492,7 +525,7 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -682,7 +715,7 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -1612,7 +1645,18 @@ class Chart extends WriterPart if (is_numeric($alpha)) { $objWriter->startElement('a:alpha'); $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); - $objWriter->endElement(); + $objWriter->endElement(); // a:alpha + } + $brightness = $chartColor->getBrightness(); + if (is_numeric($brightness)) { + $brightness = (int) $brightness; + $lumOff = 100 - $brightness; + $objWriter->startElement('a:lumMod'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness)); + $objWriter->endElement(); // a:lumMod + $objWriter->startElement('a:lumOff'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff)); + $objWriter->endElement(); // a:lumOff } $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr if ($solidFill) { diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index 77b0a9b2..fcde7ae3 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -447,4 +448,88 @@ class Charts32ScatterTest extends AbstractFunctional $reloadedSpreadsheet->disconnectWorksheets(); } + + public function testScatter9(): void + { + // gradient testing + $file = self::DIRECTORY . '32readwriteScatterChart9.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('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertFalse($plotArea->getNoFill()); + self::assertEquals(315.0, $plotArea->getGradientFillAngle()); + $stops = $plotArea->getGradientFillStops(); + self::assertCount(3, $stops); + self::assertEquals(0.43808, $stops[0][0]); + self::assertEquals(0, $stops[1][0]); + self::assertEquals(0.91, $stops[2][0]); + $color = $stops[0][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('CDDBEC', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(20, $color->getBrightness()); + $color = $stops[1][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('FFC000', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertNull($color->getBrightness()); + $color = $stops[2][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('00B050', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(4, $color->getBrightness()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter10(): void + { + // nofill for Chart and PlotArea, hidden Axis + $file = self::DIRECTORY . '32readwriteScatterChart10.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('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertTrue($plotArea->getNoFill()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } } From e460c826067102c8d36e8c61fe5b5bf91532259f Mon Sep 17 00:00:00 2001 From: Jonathan Goode Date: Thu, 28 Jul 2022 03:29:02 +0100 Subject: [PATCH 10/11] Fully flatten an array (#2956) * Fully flatten an array * Provide test coverage for CONCAT combined with INDEX/MATCH --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Calculation/Functions.php | 22 +++++------- .../Calculation/ArrayTest.php | 34 +++++++++++++++++++ .../Functions/TextData/ConcatenateTest.php | 25 ++++++++++++++ 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/ArrayTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fd0cc1..7f07303a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing +- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) ## 1.24.1 - 2022-07-18 diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index ddd3e200..b3d6998c 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -573,24 +573,20 @@ class Functions return (array) $array; } - $arrayValues = []; - foreach ($array as $value) { + $flattened = []; + $stack = array_values($array); + + while ($stack) { + $value = array_shift($stack); + if (is_array($value)) { - foreach ($value as $val) { - if (is_array($val)) { - foreach ($val as $v) { - $arrayValues[] = $v; - } - } else { - $arrayValues[] = $val; - } - } + array_unshift($stack, ...array_values($value)); } else { - $arrayValues[] = $value; + $flattened[] = $value; } } - return $arrayValues; + return $flattened; } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php new file mode 100644 index 00000000..60c336cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php @@ -0,0 +1,34 @@ + [ + 0 => [ + 32 => [ + 'B' => 'PHP', + ], + ], + ], + 1 => [ + 0 => [ + 32 => [ + 'C' => 'Spreadsheet', + ], + ], + ], + ]; + + $values = Functions::flattenArray($array); + + self::assertIsNotArray($values[0]); + self::assertIsNotArray($values[1]); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php index 6c9a871d..31fb94fa 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; +use PhpOffice\PhpSpreadsheet\Spreadsheet; + class ConcatenateTest extends AllSetupTeardown { /** @@ -30,4 +32,27 @@ class ConcatenateTest extends AllSetupTeardown { return require 'tests/data/Calculation/TextData/CONCATENATE.php'; } + + public function testConcatenateWithIndexMatch(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Formula'); + $sheet1->fromArray( + [ + ['Number', 'Formula'], + ['52101293', '=CONCAT(INDEX(Lookup!$B$2, MATCH($A2, Lookup!$A$2, 0)))'], + ] + ); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Lookup'); + $sheet2->fromArray( + [ + ['Lookup', 'Match'], + ['52101293', 'PHP'], + ] + ); + self::assertSame('PHP', $sheet1->getCell('B2')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } } From c0809b0c6cb2f8ad6e1ce359cfc44219ba7c841d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 28 Jul 2022 07:03:26 -0700 Subject: [PATCH 11/11] Fix Spreadsheet Copy, Disable Clone, Improve Coverage (#2951) * Fix Spreadsheet Copy, Disable Clone, Improve Coverage This PR was supposed to be merely to increase coverage in Spreadsheet. However, in doing so, I discovered that neither clone nor copy worked correctly. Neither had been covered in the test suite. Copy not only did not work, it broke the source spreadsheet as well. I tried to debug and got nowhere; I even tried using myclabs/deep-copy which is already in use in the test suite, but it failed as well. However, write and reload ought to work just fine for copy. It can't be used for clone; however, since copy does what clone ought to do, there's no reason why clone needs to be used, so __clone is changed to throw an exception if attempted. One other source change was needed, an obvious bug where an if condition uses 'or' when it should use 'and'. Also, one docblock declaration needed a change. Aside from that, the rest of this PR is test cases, and overall coverage passes 89% for the first time. * Clone is Okay After All But copy wasn't, changing it to just return clone. Perhaps save and reload will be needed instead at some point, but not yet. * An Error I Cannot Reproduce PHP8.1 unit test says error because GdImage can't be serialized. I can't reproduce this error on any of my test systems. I have no idea why GdImage is even involved. Using try/catch to see if it helps. * Weird Failures in Github I thought restoring clone was a good idea. That left me in a state where, after one change, copy/clone no longer worked on Github (unable to reproduce on any of my test systems). After a second change, copy worked but clone didn't, again unable to reproduce. So, reverting to original version - copy does save and reload, clone throws exception. --- phpstan-baseline.neon | 15 -- src/PhpSpreadsheet/Spreadsheet.php | 35 ++- tests/PhpSpreadsheetTests/DefinedNameTest.php | 12 + .../PhpSpreadsheetTests/NamedFormulaTest.php | 6 + tests/PhpSpreadsheetTests/NamedRangeTest.php | 6 + .../Reader/Xlsx/RibbonTest.php | 30 +++ .../SpreadsheetCoverageTest.php | 209 ++++++++++++++++++ tests/PhpSpreadsheetTests/Style/FontTest.php | 17 ++ 8 files changed, 297 insertions(+), 33 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5ef5522f..024f1fbd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2860,11 +2860,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Comparison operation \"\\<\\=\" between int\\ and 1000 is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Parameter \\#1 \\$worksheet of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getIndex\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" count: 1 @@ -2875,21 +2870,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Result of \\|\\| is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet and null will always evaluate to false\\.$#" count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 33b4fe0c..4bb93987 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -3,10 +3,13 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet { @@ -1120,28 +1123,24 @@ class Spreadsheet */ public function copy() { - $copied = clone $this; + $filename = File::temporaryFilename(); + $writer = new XlsxWriter($this); + $writer->setIncludeCharts(true); + $writer->save($filename); - $worksheetCount = count($this->workSheetCollection); - for ($i = 0; $i < $worksheetCount; ++$i) { - $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); - $this->workSheetCollection[$i]->rebindParent($this); - } + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $reloadedSpreadsheet = $reader->load($filename); + unlink($filename); - return $copied; + return $reloadedSpreadsheet; } - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ public function __clone() { - // @phpstan-ignore-next-line - foreach ($this as $key => $val) { - if (is_object($val) || (is_array($val))) { - $this->{$key} = unserialize(serialize($val)); - } - } + throw new Exception( + 'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.' + ); } /** @@ -1562,7 +1561,7 @@ class Spreadsheet * Workbook window is hidden and cannot be shown in the * user interface. * - * @param string $visibility visibility status of the workbook + * @param null|string $visibility visibility status of the workbook */ public function setVisibility($visibility): void { @@ -1596,7 +1595,7 @@ class Spreadsheet */ public function setTabRatio($tabRatio): void { - if ($tabRatio >= 0 || $tabRatio <= 1000) { + if ($tabRatio >= 0 && $tabRatio <= 1000) { $this->tabRatio = (int) $tabRatio; } else { throw new Exception('Tab ratio must be between 0 and 1000.'); diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 43eddc8a..82950880 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -85,6 +85,18 @@ class DefinedNameTest extends TestCase self::assertCount(1, $this->spreadsheet->getDefinedNames()); } + public function testRemoveGlobalDefinedName(): void + { + $this->spreadsheet->addDefinedName( + DefinedName::createInstance('Any', $this->spreadsheet->getActiveSheet(), '=A1') + ); + self::assertCount(1, $this->spreadsheet->getDefinedNames()); + + $this->spreadsheet->removeDefinedName('Any'); + self::assertCount(0, $this->spreadsheet->getDefinedNames()); + $this->spreadsheet->removeDefinedName('Other'); + } + public function testRemoveGlobalDefinedNameWhenDuplicateNames(): void { $this->spreadsheet->addDefinedName( diff --git a/tests/PhpSpreadsheetTests/NamedFormulaTest.php b/tests/PhpSpreadsheetTests/NamedFormulaTest.php index 4c4a6b11..02e9d818 100644 --- a/tests/PhpSpreadsheetTests/NamedFormulaTest.php +++ b/tests/PhpSpreadsheetTests/NamedFormulaTest.php @@ -133,4 +133,10 @@ class NamedFormulaTest extends TestCase $formula->getValue() ); } + + public function testRemoveNonExistentNamedFormula(): void + { + self::assertCount(0, $this->spreadsheet->getNamedFormulae()); + $this->spreadsheet->removeNamedFormula('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/NamedRangeTest.php b/tests/PhpSpreadsheetTests/NamedRangeTest.php index c72b7b73..402e7eba 100644 --- a/tests/PhpSpreadsheetTests/NamedRangeTest.php +++ b/tests/PhpSpreadsheetTests/NamedRangeTest.php @@ -133,4 +133,10 @@ class NamedRangeTest extends TestCase $range->getValue() ); } + + public function testRemoveNonExistentNamedRange(): void + { + self::assertCount(0, $this->spreadsheet->getNamedRanges()); + $this->spreadsheet->removeNamedRange('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php index 197ad47f..ab304e7b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -44,4 +44,34 @@ class RibbonTest extends AbstractFunctional self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); $reloadedSpreadsheet->disconnectWorksheets(); } + + /** + * Same as above but discard macros. + */ + public function testDiscardMacros(): void + { + $filename = 'tests/data/Reader/XLSX/ribbon.donotopen.zip'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen($data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + $spreadsheet->discardMacros(); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertNull($reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php new file mode 100644 index 00000000..584c53fe --- /dev/null +++ b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php @@ -0,0 +1,209 @@ +getProperties(); + $properties->setCreator('Anyone'); + $properties->setTitle('Description'); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($properties, $spreadsheet2->getProperties()); + $properties2 = clone $properties; + $spreadsheet2->setProperties($properties2); + self::assertEquals($properties, $spreadsheet2->getProperties()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testDocumentSecurity(): void + { + $spreadsheet = new Spreadsheet(); + $security = $spreadsheet->getSecurity(); + $security->setLockRevision(true); + $revisionsPassword = 'revpasswd'; + $security->setRevisionsPassword($revisionsPassword); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($security, $spreadsheet2->getSecurity()); + $security2 = clone $security; + $spreadsheet2->setSecurity($security2); + self::assertEquals($security, $spreadsheet2->getSecurity()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testCellXfCollection(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $collection = $spreadsheet->getCellXfCollection(); + self::assertCount(4, $collection); + $font1Style = $collection[1]; + self::assertTrue($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font1', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + + $spreadsheet->removeCellXfByIndex(1); + self::assertFalse($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font2', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $spreadsheet->removeCellXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveDefaultStyle(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('No default style found for this workbook'); + // Removing default style probably should be disallowed. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellXfByIndex(0); + $style = $spreadsheet->getDefaultStyle(); + $spreadsheet->disconnectWorksheets(); + } + + public function testCellStyleXF(): void + { + $spreadsheet = new Spreadsheet(); + $collection = $spreadsheet->getCellStyleXfCollection(); + self::assertCount(1, $collection); + $styleXf = $collection[0]; + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByIndex(0)); + $hash = $styleXf->getHashCode(); + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByHashCode($hash)); + self::assertFalse($spreadsheet->getCellStyleXfByHashCode($hash . 'x')); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellStyleXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellStyleXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellStyleXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidFirstSheetIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('First sheet index must be a positive integer.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setFirstSheetIndex(-1); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidVisibility(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Invalid visibility value.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); + self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility(null); + self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility('badvalue'); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidTabRatio(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Tab ratio must be between 0 and 1000.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setTabRatio(2000); + $spreadsheet->disconnectWorksheets(); + } + + public function testCopy(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $sheet->getCell('A1')->setValue('this is a1'); + $sheet->getCell('A2')->setValue('this is a2'); + $sheet->getCell('A3')->setValue('this is a3'); + $sheet->getCell('B1')->setValue('this is b1'); + $sheet->getCell('B2')->setValue('this is b2'); + $copied = $spreadsheet->copy(); + $copysheet = $copied->getActiveSheet(); + $copysheet->getStyle('A2')->getFont()->setName('font12'); + $copysheet->getCell('A2')->setValue('this was a2'); + + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $sheet->getCell('A1')->getValue()); + self::assertSame('this is a2', $sheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $sheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $sheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $sheet->getCell('B2')->getValue()); + + self::assertSame('font1', $copysheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font12', $copysheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $copysheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $copysheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $copysheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $copysheet->getCell('A1')->getValue()); + self::assertSame('this was a2', $copysheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $copysheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $copysheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $copysheet->getCell('B2')->getValue()); + + $spreadsheet->disconnectWorksheets(); + $copied->disconnectWorksheets(); + } + + public function testClone(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'); + $spreadsheet = new Spreadsheet(); + $clone = clone $spreadsheet; + $spreadsheet->disconnectWorksheets(); + $clone->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/FontTest.php b/tests/PhpSpreadsheetTests/Style/FontTest.php index 02814afa..6cd4d950 100644 --- a/tests/PhpSpreadsheetTests/Style/FontTest.php +++ b/tests/PhpSpreadsheetTests/Style/FontTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Style; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; use PHPUnit\Framework\TestCase; class FontTest extends TestCase @@ -88,4 +89,20 @@ class FontTest extends TestCase self::assertEquals('Calibri', $font->getName(), 'Null string changed to default'); $spreadsheet->disconnectWorksheets(); } + + public function testUnderlineHash(): void + { + $font1 = new Font(); + $font2 = new Font(); + $font2aHash = $font2->getHashCode(); + self::assertSame($font1->getHashCode(), $font2aHash); + $font2->setUnderlineColor( + [ + 'type' => 'srgbClr', + 'value' => 'FF0000', + ] + ); + $font2bHash = $font2->getHashCode(); + self::assertNotEquals($font1->getHashCode(), $font2bHash); + } }