From bc9ca28deb98b7eaee38f874e7cf0448c5ec6cc1 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 13 Oct 2023 23:41:08 -0700 Subject: [PATCH 01/20] Use DateTime::format Rather than cal_days_in_month in Sample (#3764) Fix #3760. That problem actually was easily fixed, by enabling calendar extension in user's environment. Astonishingly, however, this is the only use of calendar in the entire project. Since the same functionality is available in DateTime, which is used throughout the project, use that instead to eliminate the dependency on calendar. --- samples/Chart/33_Chart_create_line_dateaxis.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php index e5296d1de5..d7b674f6d1 100644 --- a/samples/Chart/33_Chart_create_line_dateaxis.php +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -361,8 +361,8 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array $lastYr = (int) $lastDate->format('Y'); $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); $qtrEndMonth = 3 + (($qtr - 1) * 3); - $lastDOM = cal_days_in_month(CAL_GREGORIAN, $qtrEndMonth, $lastYr); $qtrEndMonth = sprintf('%02d', $qtrEndMonth); + $lastDOM = DateTime::createFromFormat('Y-m-d', "$lastYr-$qtrEndMonth-01")->format('t'); $qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM"; $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr); From 1282f3d67cf7ed69abd50185595ced97f8f06ffc Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:02:44 -0700 Subject: [PATCH 02/20] Performance Improvements for Csv Reader (#3769) * Performance Improvements for Csv Reader Investigating issue #381, a means was suggested to duplicated a problem, but no problem occurred ... except for performance. This involved a spreadsheet with a large number of cells, definitely not PhpSpreadsheet's strong point; even so, the program (entirely available in the issue) took a disastrous two or so hours to complete on my system. Looking at the Csv Reader code, several opportunities to cache results and avoid function calls jumped out, none of which seem to materially add to the maintenance burden of the program. Testing these changes resulted in a run time of about 20 minutes, still hardly a thing of beauty, but a huge improvement over the original and therefore worth proceeding with. * Redo CsvIssue2232Test Test cases included duplicates, and didn't account for some things (e.g. French locale will treat both 'vrai' and 'true' as true). * Additional Optimization --- src/PhpSpreadsheet/Reader/Csv.php | 55 +++++++++++++------ .../Reader/Csv/CsvIssue2232Test.php | 11 ++-- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index d5a565a55c..a93bbcf4c7 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -9,7 +9,6 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Csv extends BaseReader @@ -102,6 +101,14 @@ class Csv extends BaseReader /** @var bool */ private $sheetNameIsFileName = false; + private string $getTrue = 'true'; + + private string $getFalse = 'false'; + + private string $thousandsSeparator = ','; + + private string $decimalSeparator = '.'; + /** * Create a new CSV Reader instance. */ @@ -234,13 +241,14 @@ public function listWorksheetInfo(string $filename): array $worksheetInfo[0]['lastColumnIndex'] = 0; $worksheetInfo[0]['totalRows'] = 0; $worksheetInfo[0]['totalColumns'] = 0; + $delimiter = $this->delimiter ?? ''; // Loop through each line of the file in turn - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); while (is_array($rowData)) { ++$worksheetInfo[0]['totalRows']; $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1); - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); } $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); @@ -386,15 +394,24 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo $outRow = 0; // Loop through each line of the file in turn - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $delimiter = $this->delimiter ?? ''; + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); $valueBinder = Cell::getValueBinder(); $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion(); + $this->getTrue = Calculation::getTRUE(); + $this->getFalse = Calculation::getFALSE(); + $this->thousandsSeparator = StringHelper::getThousandsSeparator(); + $this->decimalSeparator = StringHelper::getDecimalSeparator(); while (is_array($rowData)) { $noOutputYet = true; $columnLetter = 'A'; foreach ($rowData as $rowDatum) { - $this->convertBoolean($rowDatum, $preserveBooleanString); - $numberFormatMask = $this->convertFormattedNumber($rowDatum); + if ($preserveBooleanString) { + $rowDatum = $rowDatum ?? ''; + } else { + $this->convertBoolean($rowDatum); + } + $numberFormatMask = $this->castFormattedNumberToNumeric ? $this->convertFormattedNumber($rowDatum) : ''; if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($this->contiguous) { if ($noOutputYet) { @@ -405,15 +422,17 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo $outRow = $currentRow; } // Set basic styling for the value (Note that this could be overloaded by styling in a value binder) - $sheet->getCell($columnLetter . $outRow)->getStyle() - ->getNumberFormat() - ->setFormatCode($numberFormatMask); + if ($numberFormatMask !== '') { + $sheet->getStyle($columnLetter . $outRow) + ->getNumberFormat() + ->setFormatCode($numberFormatMask); + } // Set cell value $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum); } ++$columnLetter; } - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); ++$currentRow; } @@ -429,12 +448,12 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo /** * Convert string true/false to boolean, and null to null-string. */ - private function convertBoolean(mixed &$rowDatum, bool $preserveBooleanString): void + private function convertBoolean(mixed &$rowDatum): void { - if (is_string($rowDatum) && !$preserveBooleanString) { - if (strcasecmp(Calculation::getTRUE(), $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { + if (is_string($rowDatum)) { + if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { $rowDatum = true; - } elseif (strcasecmp(Calculation::getFALSE(), $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { + } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { $rowDatum = false; } } else { @@ -447,18 +466,18 @@ private function convertBoolean(mixed &$rowDatum, bool $preserveBooleanString): */ private function convertFormattedNumber(mixed &$rowDatum): string { - $numberFormatMask = NumberFormat::FORMAT_GENERAL; + $numberFormatMask = ''; if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) { $numeric = str_replace( - [StringHelper::getThousandsSeparator(), StringHelper::getDecimalSeparator()], + [$this->thousandsSeparator, $this->decimalSeparator], ['', '.'], $rowDatum ); if (is_numeric($numeric)) { - $decimalPos = strpos($rowDatum, StringHelper::getDecimalSeparator()); + $decimalPos = strpos($rowDatum, $this->decimalSeparator); if ($this->preserveNumericFormatting === true) { - $numberFormatMask = (str_contains($rowDatum, StringHelper::getThousandsSeparator())) + $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator)) ? '#,##0' : '0'; if ($decimalPos !== false) { $decimals = strlen($rowDatum) - $decimalPos - 1; diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php index ae305d42f0..191aa9d84b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php @@ -65,7 +65,7 @@ public static function providerIssue2232(): array /** * @dataProvider providerIssue2232locale */ - public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $preserveBoolString, mixed $b4Value, mixed $b5Value): void + public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $preserveBoolString, mixed $b2Value, mixed $b3Value, mixed $b4Value, mixed $b5Value): void { if ($useStringBinder) { $binder = new StringValueBinder(); @@ -81,6 +81,8 @@ public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $ $filename = 'tests/data/Reader/CSV/issue.2232.csv'; $spreadsheet = $reader->load($filename); $sheet = $spreadsheet->getActiveSheet(); + self::assertSame($b2Value, $sheet->getCell('B2')->getValue()); + self::assertSame($b3Value, $sheet->getCell('B3')->getValue()); self::assertSame($b4Value, $sheet->getCell('B4')->getValue()); self::assertSame($b5Value, $sheet->getCell('B5')->getValue()); $spreadsheet->disconnectWorksheets(); @@ -89,10 +91,9 @@ public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $ public static function providerIssue2232locale(): array { return [ - [true, true, 'Faux', 'Vrai'], - [true, true, 'Faux', 'Vrai'], - [false, false, false, true], - [false, false, false, true], + 'string binder preserve boolean string' => [true, true, 'FaLSe', 'tRUE', 'Faux', 'Vrai'], + 'string binder convert boolean string' => [true, false, false, true, false, true], + 'default binder' => [false, null, false, true, false, true], ]; } } From dcccd63ee376805653899c1b6903c6a99d52bd07 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:06:36 -0700 Subject: [PATCH 03/20] Theme File Missing But Referenced in Spreadsheet (#3772) * Theme File Missing But Referenced in Spreadsheet Fix #3770. A rels file points to a non-existent theme file in the spreadsheet. In other similar cases (e.g. PR #3771), Excel opens such a spreadsheet, but with an error pop-up. Not so with this file; it just opens the spreadsheet without the pop-up. PhpSpreadsheet will now account for this unusual situation as well. * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Xlsx.php | 3 ++ .../Reader/Xlsx/Issue3770Test.php | 47 ++++++++++++++++++ tests/data/Reader/XLSX/issue.3770.xlsx | Bin 0 -> 11946 bytes 4 files changed, 51 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3770Test.php create mode 100644 tests/data/Reader/XLSX/issue.3770.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 240454ef75..05dda2d384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Break Some Circular References. [PR #3716](https://github.com/PHPOffice/PhpSpreadsheet/pull/3716) [PR #3707](https://github.com/PHPOffice/PhpSpreadsheet/pull/3707) - Missing Font Index in Some Xls. [PR #3734](https://github.com/PHPOffice/PhpSpreadsheet/pull/3734) - Load Tables even with READ_DATA_ONLY. [PR #3726](https://github.com/PHPOffice/PhpSpreadsheet/pull/3726) +- Theme File Missing but Referenced in Spreadsheet. [Issue #3770](https://github.com/PHPOffice/PhpSpreadsheet/issues/3770) [PR #3772](https://github.com/PHPOffice/PhpSpreadsheet/pull/3772) ## 1.29.0 - 2023-06-15 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 3c2b4f244a..99fb3937d5 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -427,6 +427,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } switch ($rel['Type']) { case "$xmlNamespaceBase/theme": + if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) { + break; // issue3770 + } $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; $themeOrderAdditional = count($themeOrderArray); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3770Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3770Test.php new file mode 100644 index 0000000000..469c02e513 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3770Test.php @@ -0,0 +1,47 @@ +load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + // Assert anything to confirm read succeeded + self::assertSame('Универсальный передаточный документ', $sheet->getCell('A1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.3770.xlsx b/tests/data/Reader/XLSX/issue.3770.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..415f6691d3457ca28443ecbf32a38d9ea4fa07cf GIT binary patch literal 11946 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 literal 0 HcmV?d00001 From 64a1d1ab02aaca914e348019ff49db8ecc9b99e6 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:35:02 -0700 Subject: [PATCH 04/20] Slk Shared Formulas (#3776) * Slk Shared Formulas Fix #2267. The Slk format has a way to express a "shared formula", but the Slk reader does not yet understand it. Thanks to @SheetJSDev for documenting the problem and pointing the way towards a solution. It has taken a long time to get there. Part of the problem is that I have not been successful in getting Excel to use this type of construction when saving a Slk file. So I have resorted to saving a Slk file where shared formulas *could* be used, and then editing it by hand to actually use them. It would not surprise me in the least to have neglected one or more possible ways to specify a shared formula; but, at least the issue as documented is resolved, and if new issues arise, we'll probably be in better shape to deal with them. * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Slk.php | 32 ++++++- .../Reader/Slk/SlkSharedFormulasTest.php | 38 ++++++++ tests/data/Reader/Slk/issue.2267c.slk | 89 +++++++++++++++++++ 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Slk/SlkSharedFormulasTest.php create mode 100644 tests/data/Reader/Slk/issue.2267c.slk diff --git a/CHANGELOG.md b/CHANGELOG.md index 05dda2d384..407abdd093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Missing Font Index in Some Xls. [PR #3734](https://github.com/PHPOffice/PhpSpreadsheet/pull/3734) - Load Tables even with READ_DATA_ONLY. [PR #3726](https://github.com/PHPOffice/PhpSpreadsheet/pull/3726) - Theme File Missing but Referenced in Spreadsheet. [Issue #3770](https://github.com/PHPOffice/PhpSpreadsheet/issues/3770) [PR #3772](https://github.com/PHPOffice/PhpSpreadsheet/pull/3772) +- Slk Shared Formulas. [Issue #2267](https://github.com/PHPOffice/PhpSpreadsheet/issues/2267) [PR #3776](https://github.com/PHPOffice/PhpSpreadsheet/pull/3776) ## 1.29.0 - 2023-06-15 diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php index 3b026fe277..275d0971b9 100644 --- a/src/PhpSpreadsheet/Reader/Slk.php +++ b/src/PhpSpreadsheet/Reader/Slk.php @@ -5,6 +5,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; +use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Border; @@ -269,14 +270,14 @@ private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, strin $hasCalculatedValue = false; $tryNumeric = false; $cellDataFormula = $cellData = ''; + $sharedColumn = $sharedRow = -1; + $sharedFormula = false; foreach ($rowData as $rowDatum) { switch ($rowDatum[0]) { - case 'C': case 'X': $column = substr($rowDatum, 1); break; - case 'R': case 'Y': $row = substr($rowDatum, 1); @@ -298,9 +299,36 @@ private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, strin ->getText() ->createText($comment); + break; + case 'C': + $sharedColumn = (int) substr($rowDatum, 1); + + break; + case 'R': + $sharedRow = (int) substr($rowDatum, 1); + + break; + case 'S': + $sharedFormula = true; + break; } } + if ($sharedFormula === true && $sharedRow >= 0 && $sharedColumn >= 0) { + $thisCoordinate = Coordinate::stringFromColumnIndex((int) $column) . $row; + $sharedCoordinate = Coordinate::stringFromColumnIndex($sharedColumn) . $sharedRow; + $formula = $spreadsheet->getActiveSheet()->getCell($sharedCoordinate)->getValue(); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($formula); + $referenceHelper = ReferenceHelper::getInstance(); + $newFormula = $referenceHelper->updateFormulaReferences($formula, 'A1', (int) $column - $sharedColumn, (int) $row - $sharedRow, '', true, false); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($newFormula); + //$calc = $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->getCalculatedValue(); + //$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($calc); + $cellData = Calculation::unwrapResult($cellData); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($cellData, $tryNumeric); + + return; + } $columnLetter = Coordinate::stringFromColumnIndex((int) $column); $cellData = Calculation::unwrapResult($cellData); diff --git a/tests/PhpSpreadsheetTests/Reader/Slk/SlkSharedFormulasTest.php b/tests/PhpSpreadsheetTests/Reader/Slk/SlkSharedFormulasTest.php new file mode 100644 index 0000000000..15af9f8db8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Slk/SlkSharedFormulasTest.php @@ -0,0 +1,38 @@ +load($testbook); + $sheet = $spreadsheet->getActiveSheet(); + $range = 'A1:' . $sheet->getHighestDataColumn() . $sheet->getHighestDataRow(); + $values = $sheet->RangeToArray($range, null, false, false, false, false); // just get values, don't calculate + $expected = [ + [1, 10, 100, 101, 102], + ['=A1+1', '=B1+1', '=C1+1', '=D1+1', '=E1+1'], + ['=A2+1', '=B2+1', '=C2+1', '=D2+1', '=E2+1'], + ['=A3+1', '=B3+1', '=C3+1', '=D3+1', '=E3+1'], + ['=A4+1', '=B4+1', '=C4+1', '=D4+1', '=E4+1'], + ]; + self::assertSame($expected, $values); + $calcValues = $sheet->RangeToArray($range, null, true, false, false, false); // get calculated values + $expectedCalc = [ + [1, 10, 100, 101, 102], + [2, 11, 101, 102, 103], + [3, 12, 102, 103, 104], + [4, 13, 103, 104, 105], + [5, 14, 104, 105, 106], + ]; + self::assertSame($expectedCalc, $calcValues); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/Slk/issue.2267c.slk b/tests/data/Reader/Slk/issue.2267c.slk new file mode 100644 index 0000000000..e069d6c06e --- /dev/null +++ b/tests/data/Reader/Slk/issue.2267c.slk @@ -0,0 +1,89 @@ +ID;PWXL;N;E +P;PGeneral +P;P0 +P;P0.00 +P;P#,##0 +P;P#,##0.00 +P;P#,##0_);;\(#,##0\) +P;P#,##0_);;[Red]\(#,##0\) +P;P#,##0.00_);;\(#,##0.00\) +P;P#,##0.00_);;[Red]\(#,##0.00\) +P;P"$"#,##0_);;\("$"#,##0\) +P;P"$"#,##0_);;[Red]\("$"#,##0\) +P;P"$"#,##0.00_);;\("$"#,##0.00\) +P;P"$"#,##0.00_);;[Red]\("$"#,##0.00\) +P;P0% +P;P0.00% +P;P0.00E+00 +P;P##0.0E+0 +P;P#\ ?/? +P;P#\ ??/?? +P;Pyyyy/mm/dd +P;Pdd/mmm/yy +P;Pdd/mmm +P;Pmmm/yy +P;Ph:mm\ AM/PM +P;Ph:mm:ss\ AM/PM +P;Ph:mm +P;Ph:mm:ss +P;Pyyyy/mm/dd\ h:mm +P;Pmm:ss +P;Pmm:ss.0 +P;P@ +P;P[h]:mm:ss +P;P_("$"* #,##0_);;_("$"* \(#,##0\);;_("$"* "-"_);;_(@_) +P;P_(* #,##0_);;_(* \(#,##0\);;_(* "-"_);;_(@_) +P;P_("$"* #,##0.00_);;_("$"* \(#,##0.00\);;_("$"* "-"??_);;_(@_) +P;P_(* #,##0.00_);;_(* \(#,##0.00\);;_(* "-"??_);;_(@_) +P;FCalibri;M220;L9 +P;FCalibri;M220;L9 +P;FCalibri;M220;L9 +P;FCalibri;M220;L9 +P;ECalibri;M220;L9 +P;ECalibri Light;M360;L55 +P;ECalibri;M300;SB;L55 +P;ECalibri;M260;SB;L55 +P;ECalibri;M220;SB;L55 +P;ECalibri;M220;L18 +P;ECalibri;M220;L21 +P;ECalibri;M220;L61 +P;ECalibri;M220;L63 +P;ECalibri;M220;SB;L64 +P;ECalibri;M220;SB;L53 +P;ECalibri;M220;L53 +P;ECalibri;M220;SB;L10 +P;ECalibri;M220;L11 +P;ECalibri;M220;SI;L24 +P;ECalibri;M220;SB;L9 +P;ECalibri;M220;L10 +P;ESegoe UI;M200;L9 +F;P0;DG0G8;M320 +B;Y6;X5;D0 0 5 4 +O;D;V0;K47;G100 0.001 +F;W1 16384 10 +C;Y1;X1;K1 +C;X2;K10 +C;X3;K100 +C;X4;K101 +C;X5;K102 +C;Y2;X1;K2;ER[-1]C+1 +C;X2;K11;ER[-1]C+1 +C;X3;K101;S;R2;C1 +C;X4;K102;S;R2;C1 +C;X5;K103;S;R2;C1 +C;Y3;X1;K3;S;R2;C1 +C;X2;K12;ER[-1]C+1 +C;X3;K102;S;R2;C1 +C;X4;K103;S;R2;C1 +C;X5;K104;S;R2;C1 +C;Y4;X1;K4;S;R2;C1 +C;X2;K13;ER[-1]C+1 +C;X3;K103;S;R2;C1 +C;X4;K104;S;R2;C1 +C;X5;K105;S;R2;C1 +C;Y5;X1;K5;S;R2;C1 +C;X2;K14;ER[-1]C+1 +C;X3;K104;S;R2;C1 +C;X4;K105;S;R2;C1 +C;X5;K106;S;R2;C1 +E From 98b5c77424b8d1fa822461999684df125f66be75 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 31 Oct 2023 09:08:15 -0700 Subject: [PATCH 05/20] Anticipate Dependabot November 2023 (#3780) --- composer.lock | 51 +++++++++---------- .../Calculation/Calculation.php | 2 +- .../Shared/OLE/ChainedBlockStream.php | 3 +- src/PhpSpreadsheet/Writer/Xls.php | 10 ++-- 4 files changed, 30 insertions(+), 36 deletions(-) diff --git a/composer.lock b/composer.lock index 562eaac4ea..e2e3341b20 100644 --- a/composer.lock +++ b/composer.lock @@ -1136,16 +1136,16 @@ "packages-dev": [ { "name": "composer/pcre", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", "shasum": "" }, "require": { @@ -1187,7 +1187,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.0" + "source": "https://github.com/composer/pcre/tree/3.1.1" }, "funding": [ { @@ -1203,7 +1203,7 @@ "type": "tidelift" } ], - "time": "2022-11-17T09:50:14+00:00" + "time": "2023-10-11T07:11:09+00:00" }, { "name": "composer/semver", @@ -1564,16 +1564,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.34.0", + "version": "v3.37.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7c7a4ad2ed8fe50df3e25528218b13d383608f23" + "reference": "c3fe76976081ab871aa654e872da588077e19679" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7c7a4ad2ed8fe50df3e25528218b13d383608f23", - "reference": "7c7a4ad2ed8fe50df3e25528218b13d383608f23", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c3fe76976081ab871aa654e872da588077e19679", + "reference": "c3fe76976081ab871aa654e872da588077e19679", "shasum": "" }, "require": { @@ -1594,9 +1594,6 @@ "symfony/process": "^5.4 || ^6.0", "symfony/stopwatch": "^5.4 || ^6.0" }, - "conflict": { - "stevebauman/unfinalize": "*" - }, "require-dev": { "facile-it/paraunit": "^1.3 || ^2.0", "justinrainbow/json-schema": "^5.2", @@ -1609,8 +1606,6 @@ "phpspec/prophecy": "^1.16", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "phpunitgoodpractices/polyfill": "^1.6", - "phpunitgoodpractices/traits": "^1.9.2", "symfony/phpunit-bridge": "^6.2.3", "symfony/yaml": "^5.4 || ^6.0" }, @@ -1650,7 +1645,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.34.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.37.1" }, "funding": [ { @@ -1658,7 +1653,7 @@ "type": "github" } ], - "time": "2023-09-29T15:34:26+00:00" + "time": "2023-10-29T20:51:23+00:00" }, { "name": "masterminds/html5", @@ -2376,16 +2371,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.36", + "version": "1.10.40", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "ffa3089511121a672e62969404e4fddc753f9b15" + "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ffa3089511121a672e62969404e4fddc753f9b15", - "reference": "ffa3089511121a672e62969404e4fddc753f9b15", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/93c84b5bf7669920d823631e39904d69b9c7dc5d", + "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d", "shasum": "" }, "require": { @@ -2434,20 +2429,20 @@ "type": "tidelift" } ], - "time": "2023-09-29T14:07:45+00:00" + "time": "2023-10-30T14:48:31+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.14", + "version": "1.3.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "614acc10c522e319639bf38b0698a4a566665f04" + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/614acc10c522e319639bf38b0698a4a566665f04", - "reference": "614acc10c522e319639bf38b0698a4a566665f04", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", "shasum": "" }, "require": { @@ -2484,9 +2479,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.14" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15" }, - "time": "2023-08-25T09:46:39+00:00" + "time": "2023-10-09T18:58:39+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index ad13a500de..d8d0557399 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4409,7 +4409,7 @@ private function internalParseFormula($formula, ?Cell $cell = null): bool|array [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); if ($rangeWS2 !== '') { $rangeWS2 .= '!'; - } else { // @phpstan-ignore-line + } else { $rangeWS2 = $rangeWS1; } diff --git a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php index 3c4e03c5a4..f95c9025fd 100644 --- a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php +++ b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php @@ -161,8 +161,7 @@ public function stream_seek($offset, $whence): bool // @codingStandardsIgnoreLin $this->pos = $offset; } elseif ($whence == SEEK_CUR && -$offset <= $this->pos) { $this->pos += $offset; - // @phpstan-ignore-next-line - } elseif ($whence == SEEK_END && -$offset <= count($this->data)) { + } elseif ($whence == SEEK_END && -$offset <= count($this->data)) { // @phpstan-ignore-line $this->pos = strlen($this->data) + $offset; } else { return false; diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index b2f2bc48b1..1b96579e03 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -747,12 +747,12 @@ private function writeDocumentSummaryInformation(): string $dataSection_Content .= $dataProp['data']['data']; $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']); - /* Condition below can never be true - } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) - $dataSection_Content .= $dataProp['data']['data']; + /* Condition below can never be true + } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + $dataSection_Content .= $dataProp['data']['data']; - $dataSection_Content_Offset += 4 + 8; - */ + $dataSection_Content_Offset += 4 + 8; + */ } else { $dataSection_Content .= $dataProp['data']['data']; From ef3890aad34e3b7ce059de8536134896638b3663 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:07:11 -0800 Subject: [PATCH 06/20] Integrate Exceptions Better with PhpStorm (#3765) Fix #3754. Because `PhpSpreadsheetException` extends `Exception`, PhpStorm warns that calls to `setValueExplicit` and `setValue`, among others, do not handle the exception, because PhpStorm treats `Exception`, by default, as "checked". On the other hand, PhpStorm treats `RunTimeException` as "unchecked" so won't flag such calls. It is reasonable to let `PhpSpreadsheetException` extend `RuntTimeException`, and will eliminate the problem, without having to wrap code in all-but-useless try-catch blocks (e.g. for `setValue`, the code would raise an exception only if you try to set the cell's value to an unstringable object). --- src/PhpSpreadsheet/Cell/Cell.php | 78 ++++++++++++++--------- src/PhpSpreadsheet/Exception.php | 4 +- src/PhpSpreadsheet/Writer/Ods/Content.php | 4 +- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 5b8ba19834..8ebc115a88 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -3,9 +3,10 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Collection\Cells; -use PhpOffice\PhpSpreadsheet\Exception; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -76,13 +77,13 @@ class Cell implements Stringable /** * Update the cell into the cell collection. * - * @return $this + * @throws SpreadsheetException */ public function updateInCollection(): self { $parent = $this->parent; if ($parent === null) { - throw new Exception('Cannot update when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot update when cell is not bound to a worksheet'); } $parent->update($this); @@ -101,6 +102,8 @@ public function attach(Cells $parent): void /** * Create a new Cell. + * + * @throws SpreadsheetException */ public function __construct(mixed $value, ?string $dataType, Worksheet $worksheet) { @@ -117,19 +120,21 @@ public function __construct(mixed $value, ?string $dataType, Worksheet $workshee } $this->dataType = $dataType; } elseif (self::getValueBinder()->bindValue($this, $value) === false) { - throw new Exception('Value could not be bound to cell.'); + throw new SpreadsheetException('Value could not be bound to cell.'); } $this->ignoredErrors = new IgnoredErrors(); } /** * Get cell coordinate column. + * + * @throws SpreadsheetException */ public function getColumn(): string { $parent = $this->parent; if ($parent === null) { - throw new Exception('Cannot get column when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get column when cell is not bound to a worksheet'); } return $parent->getCurrentColumn(); @@ -137,12 +142,14 @@ public function getColumn(): string /** * Get cell coordinate row. + * + * @throws SpreadsheetException */ public function getRow(): int { $parent = $this->parent; if ($parent === null) { - throw new Exception('Cannot get row when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get row when cell is not bound to a worksheet'); } return $parent->getCurrentRow(); @@ -151,9 +158,9 @@ public function getRow(): int /** * Get cell coordinate. * - * @return string + * @throws SpreadsheetException */ - public function getCoordinate() + public function getCoordinate(): string { $parent = $this->parent; if ($parent !== null) { @@ -162,7 +169,7 @@ public function getCoordinate() $coordinate = null; } if ($coordinate === null) { - throw new Exception('Coordinate no longer exists'); + throw new SpreadsheetException('Coordinate no longer exists'); } return $coordinate; @@ -216,15 +223,13 @@ protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self * @param mixed $value Value * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder * - * @throws Exception - * - * @return $this + * @throws SpreadsheetException */ public function setValue(mixed $value, ?IValueBinder $binder = null): self { $binder ??= self::getValueBinder(); if (!$binder->bindValue($this, $value)) { - throw new Exception('Value could not be bound to cell.'); + throw new SpreadsheetException('Value could not be bound to cell.'); } return $this; @@ -241,9 +246,9 @@ public function setValue(mixed $value, ?IValueBinder $binder = null): self * If you do mismatch value and datatype, then the value you enter may be changed to match the datatype * that you specify. * - * @return Cell + * @throws SpreadsheetException */ - public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING) + public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self { $oldValue = $this->value; @@ -265,7 +270,7 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE break; case DataType::TYPE_NUMERIC: if (is_string($value) && !is_numeric($value)) { - throw new Exception('Invalid numeric value for datatype Numeric'); + throw new SpreadsheetException('Invalid numeric value for datatype Numeric'); } $this->value = 0 + $value; @@ -288,7 +293,7 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE break; default: - throw new Exception('Invalid datatype: ' . $dataType); + throw new SpreadsheetException('Invalid datatype: ' . $dataType); } // set the datatype @@ -313,11 +318,12 @@ public static function getCalculateDateTimeType(): int return self::$calculateDateTimeType; } + /** @throws CalculationException*/ public static function setCalculateDateTimeType(int $calculateDateTimeType): void { self::$calculateDateTimeType = match ($calculateDateTimeType) { self::CALCULATE_DATE_TIME_ASIS, self::CALCULATE_DATE_TIME_FLOAT, self::CALCULATE_TIME_FLOAT => $calculateDateTimeType, - default => throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception("Invalid value $calculateDateTimeType for calculated date time type"), + default => throw new CalculationException("Invalid value $calculateDateTimeType for calculated date time type"), }; } @@ -348,9 +354,9 @@ private function convertDateTimeInt(mixed $result) * * @param bool $resetLog Whether the calculation engine logger should be reset or not * - * @return mixed + * @throws CalculationException */ - public function getCalculatedValue(bool $resetLog = true) + public function getCalculatedValue(bool $resetLog = true): mixed { if ($this->dataType === DataType::TYPE_FORMULA) { try { @@ -368,14 +374,14 @@ public function getCalculatedValue(bool $resetLog = true) $result = array_shift($result); } } - } catch (Exception $ex) { + } catch (SpreadsheetException $ex) { if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) { return $this->calculatedValue; // Fallback for calculations referencing external files. } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) { return ExcelError::NAME(); } - throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception( + throw new CalculationException( $this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(), $ex->getCode(), $ex @@ -453,11 +459,13 @@ public function isFormula(): bool /** * Does this cell contain Data validation rules? + * + * @throws SpreadsheetException */ public function hasDataValidation(): bool { if (!isset($this->parent)) { - throw new Exception('Cannot check for data validation when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot check for data validation when cell is not bound to a worksheet'); } return $this->getWorksheet()->dataValidationExists($this->getCoordinate()); @@ -465,11 +473,13 @@ public function hasDataValidation(): bool /** * Get Data validation rules. + * + * @throws SpreadsheetException */ public function getDataValidation(): DataValidation { if (!isset($this->parent)) { - throw new Exception('Cannot get data validation for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get data validation for cell that is not bound to a worksheet'); } return $this->getWorksheet()->getDataValidation($this->getCoordinate()); @@ -477,11 +487,13 @@ public function getDataValidation(): DataValidation /** * Set Data validation rules. + * + * @throws SpreadsheetException */ public function setDataValidation(?DataValidation $dataValidation = null): self { if (!isset($this->parent)) { - throw new Exception('Cannot set data validation for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot set data validation for cell that is not bound to a worksheet'); } $this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation); @@ -501,11 +513,13 @@ public function hasValidValue(): bool /** * Does this cell contain a Hyperlink? + * + * @throws SpreadsheetException */ public function hasHyperlink(): bool { if (!isset($this->parent)) { - throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot check for hyperlink when cell is not bound to a worksheet'); } return $this->getWorksheet()->hyperlinkExists($this->getCoordinate()); @@ -513,11 +527,13 @@ public function hasHyperlink(): bool /** * Get Hyperlink. + * + * @throws SpreadsheetException */ public function getHyperlink(): Hyperlink { if (!isset($this->parent)) { - throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet'); } return $this->getWorksheet()->getHyperlink($this->getCoordinate()); @@ -525,11 +541,13 @@ public function getHyperlink(): Hyperlink /** * Set Hyperlink. + * + * @throws SpreadsheetException */ public function setHyperlink(?Hyperlink $hyperlink = null): self { if (!isset($this->parent)) { - throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet'); } $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink); @@ -549,6 +567,8 @@ public function getParent() /** * Get parent worksheet. + * + * @throws SpreadsheetException */ public function getWorksheet(): Worksheet { @@ -560,7 +580,7 @@ public function getWorksheet(): Worksheet } if ($worksheet === null) { - throw new Exception('Worksheet no longer exists'); + throw new SpreadsheetException('Worksheet no longer exists'); } return $worksheet; diff --git a/src/PhpSpreadsheet/Exception.php b/src/PhpSpreadsheet/Exception.php index 9c5ab30ee0..349158078a 100644 --- a/src/PhpSpreadsheet/Exception.php +++ b/src/PhpSpreadsheet/Exception.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet; -class Exception extends \Exception +use RuntimeException; + +class Exception extends RuntimeException { } diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index f08c90e86d..fb840cbf7f 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -9,7 +10,6 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -use PhpOffice\PhpSpreadsheet\Writer\Exception; use PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Comment; use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Style; @@ -225,7 +225,7 @@ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void if ($this->getParentWriter()->getPreCalculateFormulas()) { try { $formulaValue = $cell->getCalculatedValue(); - } catch (Exception) { + } catch (CalculationException $e) { // don't do anything } } From 656a7164e1a969437d02b7abeb3c3460ff5b07c0 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:15:38 -0800 Subject: [PATCH 07/20] Address Some Chart Problems (#3771) * Address Some Chart Problems Fix #3767. The basic problem in that issue was user error, but it exposed a couple of problems in code which are being addressed. When a spreadsheet with charts is loaded without the includeCharts option and then saved, a corrupt spreadsheet is created. I believe this has been the case for quite some time. Nothing in the test suite covers this scenario. It is, in fact, a difficult thing to test, since the problem is exposed only when the file is opened through Excel. The specific problem is that a rels file generated by the output writer continues to refer to the drawing file which described the chart, but that file is not included (and not needed) in the output spreadsheet. The resolution is kludgey. The information that the file will not be needed is not available when the rels file is written. But, when it comes time to write the drawing file, it is known whether the rels file included it. So, if nothing else has caused the file to be generated, it is written out as a basically empty xml file after all. This solves the problem, but I will continue to search for a less kludgey solution. This solution is, at least, testable; if a different solution is applied later on, the test being introduced here is likely to break so a new one will be needed. When the provided spreadsheet is loaded with the includeCharts option and then saved, an error is exposed in processing the Chart Title caption when it doesn't exist. The change to Writer/Xlsx/Chart is simple and clearly justifiable. What is peculiar is that the error does not arise with release 1.29, but does arise with master. It is not at all clear to me what has changed since the release to expose the error - the code in question certainly hasn't changed. It is difficult to isolate changes because of the extensive number of changes following on the elimination of Php7.4 as a supported platform. The provided spreadsheet is unusual in at least two senses. When opened in Excel, it will show a clearly default value for the chart title, namely 'Chart Title'. I cannot find anything in the xml corresponding to that text. Since I have no idea why Excel is using that title, I will not try to duplicate its behavior, so that loading and saving the provided spreadsheet will omit the chart title. I will continue to investigate. The other sense in which it is unusual is that it includes some style files in the same directory as the chart. I doubt that PhpSpreadsheet looks at these. The styling after load and save seems to mostly match the original, although there is at least one color in the graph which does not match. I imagine it would be pretty complicated to formally support these files. * Unused Assignment --- src/PhpSpreadsheet/Writer/Xlsx.php | 3 + src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 4 +- .../Reader/Xlsx/Issue3767Test.php | 88 ++++++++++++++++++ tests/data/Reader/XLSX/issue.3767.xlsx | Bin 0 -> 15637 bytes 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php create mode 100644 tests/data/Reader/XLSX/issue.3767.xlsx diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 00c6752037..c18bc85862 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -410,6 +410,9 @@ public function save($filename, int $flags = 0): void } } } + if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']) && !isset($zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'])) { + $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = ''; + } // Add comment relationship parts $legacy = $unparsedLoadedData['sheets'][$this->spreadSheet->getSheet($i)->getCodeName()]['legacyDrawing'] ?? null; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 9bf65ea5ef..f27156507a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -169,8 +169,8 @@ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void $objWriter->endElement(); $caption = $title->getCaption(); - if ((is_array($caption)) && (count($caption) > 0)) { - $caption = $caption[0]; + if (is_array($caption)) { + $caption = $caption[0] ?? ''; } $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php new file mode 100644 index 0000000000..9f8c3266c9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php @@ -0,0 +1,88 @@ +tempfile !== '') { + unlink($this->tempfile); + $this->tempfile = ''; + } + } + + public function readCharts(XlsxReader $reader): void + { + $reader->setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testReadWithoutCharts(): void + { + $reader = new XlsxReader(); + //$this->readCharts($reader); // Commented out - don't want to read charts. + $spreadsheet = $reader->load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(0, $charts); + $this->tempfile = File::temporaryFileName(); + $writer = new XlsxWriter($spreadsheet); + $this->writeCharts($writer); + $writer->save($this->tempfile); + $spreadsheet->disconnectWorksheets(); + $file = 'zip://'; + $file .= $this->tempfile; + $file .= '#xl/worksheets/_rels/sheet1.xml.rels'; + $data = (string) file_get_contents($file); + // PhpSpreadsheet still generates this target even though charts aren't included + self::assertStringContainsString('Target="../drawings/drawing1.xml"', $data); + $file = 'zip://'; + $file .= $this->tempfile; + $file .= '#xl/drawings/drawing1.xml'; + $data = file_get_contents($file); + self::assertSame('', $data); // fake file because rels needs it + } + + public function testReadWithCharts(): void + { + $reader = new XlsxReader(); + $this->readCharts($reader); + $spreadsheet = $reader->load(self::$testbook); + $xsheet = $spreadsheet->getActiveSheet(); + $xcharts = $xsheet->getChartCollection(); + self::assertCount(1, $xcharts); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $xsheet->getChartCollection(); + self::assertCount(1, $charts); + // In Excel, a default title ('Chart Title') is shown. + // I can't find that anywhere in the Xml. + self::assertSame('', $charts[0]?->getTitle()?->getCaptionText()); + // Just test anything on the chart. + self::assertSame($sheet->getCell('B2')->getValue(), $charts[0]->getPlotArea()?->getPlotGroup()[0]->getPlotValues()[0]->getDataValues()[0]); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.3767.xlsx b/tests/data/Reader/XLSX/issue.3767.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bde09e6a665b9e85ad53552e84b8685461a2fd88 GIT binary patch literal 15637 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

hlkPeum2L&NYwSO__WBO1A+tdOvczC^y^ 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 Date: Fri, 27 Oct 2023 00:00:49 +0300 Subject: [PATCH 08/20] Test against php 8.3 in not experimental mode --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c01a7bdd7..c7d02f4734 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,7 @@ jobs: - '8.0' - '8.1' - '8.2' + - '8.3' include: - php-version: 'nightly' From f9eb35d8d1fb3c899ca01af1e230fb0140ed11ae Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 13 Nov 2023 06:53:03 -0800 Subject: [PATCH 09/20] Two Problems with Html Chart Rendering - Minor Break (#3787) * Two Problems with Html Chart Rendering - Minor Break Several problems are noted in #3783. This PR addresses those problems which make the rendering unsatisfactory (see following paragraphs). It does not address some items where the rendering is IMO satisfactory although it doesn't match Excel. In particular, the use of a different color palette and the rotation of charts are not addressed. I will leave the issue open for now because of those. As for the items which are addressed, in some cases the Html was omitting a chart altogether. This is because it had been extending the column range for charts only when it decided that extending the row range was needed. The code is changed to now extend the column range whenever the chart begins beyond the current column range of the sheet. Also, the rendering always produced a fixed-size image, so saving as Html could result in charts overlaying each other or other parts of the spreadsheet. New properties `renderedWidth` and `renderedHeight` are added to Chart, along with setters and getters. Writer/Html is changed to set these values using the chart's top left and bottom right cells to try to determine the actual size that is needed. Users can also set these properties outside of Writer/Html if they wish. Thanks to @f1mishutka for determining the source of this problem and suggesting an approach to resolving it. Because the size of the rendered image in Html/Pdf is changed, this could be considered a breaking change. To restore the prior behavior, do the following for all charts before saving as Html: ```php $chart->setRenderedWidth(640.0); $chart->setRenderedHeight(480.0); ``` * Update CHANGELOG.md --- CHANGELOG.md | 6 +- samples/templates/36writeMultiple1.xlsx | Bin 0 -> 21837 bytes src/PhpSpreadsheet/Chart/Chart.php | 34 ++++++++++ .../Chart/Renderer/JpGraphRendererBase.php | 23 +++++-- src/PhpSpreadsheet/Shared/Drawing.php | 2 +- src/PhpSpreadsheet/Writer/Html.php | 61 +++++++++++++++--- 6 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 samples/templates/36writeMultiple1.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 407abdd093..6decabdc31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Drop support for PHP 7.4, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support [PR #3713](https://github.com/PHPOffice/PhpSpreadsheet/pull/3713) - RLM Added to NumberFormatter Currency. This happens depending on release of ICU which Php is using (it does not yet happen with any official release). PhpSpreadsheet will continue to use the value returned by Php, but a method is added to keep the result unchanged from release to release. [Issue #3571](https://github.com/PHPOffice/PhpSpreadsheet/issues/3571) [PR #3640](https://github.com/PHPOffice/PhpSpreadsheet/pull/3640) - `toFormattedString` will now always return a string. This was introduced with 1.28.0, but was not properly documented at the time. This can affect the results of `toArray`, `namedRangeToArray`, and `rangeToArray`. [PR #3304](https://github.com/PHPOffice/PhpSpreadsheet/pull/3304) +- Value of constants FORMAT_CURRENCY_EUR and FORMAT_CURRENCY_USD was changed in 1.28.0, but was not properly documented at the time. [Issue #3577](https://github.com/PHPOffice/PhpSpreadsheet/issues/3577) +- Html Writer will attempt to use Chart coordinates to determine image size. [Issue #3783](https://github.com/PHPOffice/PhpSpreadsheet/issues/3783) [PR #3787](https://github.com/PHPOffice/PhpSpreadsheet/pull/3787) ### Deprecated @@ -69,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Load Tables even with READ_DATA_ONLY. [PR #3726](https://github.com/PHPOffice/PhpSpreadsheet/pull/3726) - Theme File Missing but Referenced in Spreadsheet. [Issue #3770](https://github.com/PHPOffice/PhpSpreadsheet/issues/3770) [PR #3772](https://github.com/PHPOffice/PhpSpreadsheet/pull/3772) - Slk Shared Formulas. [Issue #2267](https://github.com/PHPOffice/PhpSpreadsheet/issues/2267) [PR #3776](https://github.com/PHPOffice/PhpSpreadsheet/pull/3776) +- Html omitting some charts. [Issue #3767](https://github.com/PHPOffice/PhpSpreadsheet/issues/3767) [PR #3771](https://github.com/PHPOffice/PhpSpreadsheet/pull/3771) ## 1.29.0 - 2023-06-15 @@ -148,10 +151,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Improved support for locale settings and currency codes when matching formatted strings to numerics in the Calculation Engine [PR #3373](https://github.com/PHPOffice/PhpSpreadsheet/pull/3373) and [PR #3374](https://github.com/PHPOffice/PhpSpreadsheet/pull/3374) - Improved support for locale settings and matching in the Advanced Value Binder [PR #3376](https://github.com/PHPOffice/PhpSpreadsheet/pull/3376) - `toFormattedString` will now always return a string. This can affect the results of `toArray`, `namedRangeToArray`, and `rangeToArray`. [PR #3304](https://github.com/PHPOffice/PhpSpreadsheet/pull/3304) +- Value of constants FORMAT_CURRENCY_EUR and FORMAT_CURRENCY_USD is changed. [Issue #3577](https://github.com/PHPOffice/PhpSpreadsheet/issues/3577) [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377) ### Deprecated -- Rationalisation of Pre-defined Currency Format Masks +- Rationalisation of Pre-defined Currency Format Masks [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377) ### Removed diff --git a/samples/templates/36writeMultiple1.xlsx b/samples/templates/36writeMultiple1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ddbd5124a25ad61278b5ab4fbd51c5794abc852d GIT binary patch literal 21837 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 KyF}R^|NS3$AaOSU literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index e078b80a6d..b86b19cdd0 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -132,6 +132,16 @@ class Chart private ChartColor $fillColor; + /** + * Rendered width in pixels. + */ + private ?float $renderedWidth = null; + + /** + * Rendered height in pixels. + */ + private ?float $renderedHeight = null; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -791,4 +801,28 @@ public function getFillColor(): ChartColor { return $this->fillColor; } + + public function setRenderedWidth(?float $width): self + { + $this->renderedWidth = $width; + + return $this; + } + + public function getRenderedWidth(): ?float + { + return $this->renderedWidth; + } + + public function setRenderedHeight(?float $height): self + { + $this->renderedHeight = $height; + + return $this; + } + + public function getRenderedHeight(): ?float + { + return $this->renderedHeight; + } } diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php index 40ce338d93..27cf3b165d 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -26,9 +26,9 @@ */ abstract class JpGraphRendererBase implements IRenderer { - private static $width = 640; + private const DEFAULT_WIDTH = 640.0; - private static $height = 480; + private const DEFAULT_HEIGHT = 480.0; private static $colourSet = [ 'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1', @@ -70,6 +70,16 @@ public function __construct(Chart $chart) ]; } + private function getGraphWidth(): float + { + return $this->chart->getRenderedWidth() ?? self::DEFAULT_WIDTH; + } + + private function getGraphHeight(): float + { + return $this->chart->getRenderedHeight() ?? self::DEFAULT_HEIGHT; + } + /** * This method should be overriden in descendants to do real JpGraph library initialization. */ @@ -221,7 +231,7 @@ private function renderLegend(): void private function renderCartesianPlotArea(string $type = 'textlin'): void { - $this->graph = new Graph(self::$width, self::$height); + $this->graph = new Graph($this->getGraphWidth(), $this->getGraphHeight()); $this->graph->SetScale($type); $this->renderTitle(); @@ -258,14 +268,14 @@ private function renderCartesianPlotArea(string $type = 'textlin'): void private function renderPiePlotArea(): void { - $this->graph = new PieGraph(self::$width, self::$height); + $this->graph = new PieGraph($this->getGraphWidth(), $this->getGraphHeight()); $this->renderTitle(); } private function renderRadarPlotArea(): void { - $this->graph = new RadarGraph(self::$width, self::$height); + $this->graph = new RadarGraph($this->getGraphWidth(), $this->getGraphHeight()); $this->graph->SetScale('lin'); $this->renderTitle(); @@ -460,7 +470,6 @@ private function renderPlotScatter(int $groupID, bool $bubble): void $dataValuesY[$k] = $k; } } - //var_dump($dataValuesY, $dataValuesX, $bubbleSize); $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); if ($scatterStyle == 'lineMarker') { @@ -468,7 +477,7 @@ private function renderPlotScatter(int $groupID, bool $bubble): void $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); } elseif ($scatterStyle == 'smoothMarker') { $spline = new Spline($dataValuesY, $dataValuesX); - [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); + [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * $this->getGraphWidth() / 20); $lplot = new LinePlot($splineDataX, $splineDataY); $lplot->SetColor(self::$colourSet[self::$plotColour]); diff --git a/src/PhpSpreadsheet/Shared/Drawing.php b/src/PhpSpreadsheet/Shared/Drawing.php index edaad86153..f7ce022c51 100644 --- a/src/PhpSpreadsheet/Shared/Drawing.php +++ b/src/PhpSpreadsheet/Shared/Drawing.php @@ -110,7 +110,7 @@ public static function pixelsToPoints($pixelValue): float /** * Convert points to pixels. * - * @param int $pointValue Value in points + * @param float|int $pointValue Value in points * * @return int Value in pixels */ diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 2b38abfc4e..ecb99219d1 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -31,6 +31,10 @@ class Html extends BaseWriter { + private const DEFAULT_CELL_WIDTH_POINTS = 42; + + private const DEFAULT_CELL_WIDTH_PIXELS = 56; + /** * Spreadsheet object. */ @@ -580,9 +584,9 @@ private function extendRowsForCharts(Worksheet $worksheet, int $row): array $chartCol = Coordinate::columnIndexFromString($chartTL[0]); if ($chartTL[1] > $rowMax) { $rowMax = $chartTL[1]; - if ($chartCol > Coordinate::columnIndexFromString($colMax)) { - $colMax = $chartTL[0]; - } + } + if ($chartCol > Coordinate::columnIndexFromString($colMax)) { + $colMax = $chartTL[0]; } } } @@ -601,9 +605,9 @@ private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): s $imageCol = Coordinate::columnIndexFromString($imageTL[0]); if ($imageTL[1] > $rowMax) { $rowMax = $imageTL[1]; - if ($imageCol > Coordinate::columnIndexFromString($colMax)) { - $colMax = $imageTL[0]; - } + } + if ($imageCol > Coordinate::columnIndexFromString($colMax)) { + $colMax = $imageTL[0]; } } @@ -745,7 +749,15 @@ private function writeChartInCell(Worksheet $worksheet, string $coordinates): st $chartCoordinates = $chart->getTopLeftPosition(); if ($chartCoordinates['cell'] == $coordinates) { $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; - if (!$chart->render($chartFileName)) { + $renderedWidth = $chart->getRenderedWidth(); + $renderedHeight = $chart->getRenderedHeight(); + if ($renderedWidth === null || $renderedHeight === null) { + $this->adjustRendererPositions($chart, $worksheet); + } + $renderSuccessful = $chart->render($chartFileName); + $chart->setRenderedWidth($renderedWidth); + $chart->setRenderedHeight($renderedHeight); + if (!$renderSuccessful) { return ''; } @@ -770,6 +782,37 @@ private function writeChartInCell(Worksheet $worksheet, string $coordinates): st return $html; } + private function adjustRendererPositions(Chart $chart, Worksheet $sheet): void + { + $topLeft = $chart->getTopLeftPosition(); + $bottomRight = $chart->getBottomRightPosition(); + $tlCell = $topLeft['cell']; + $brCell = $bottomRight['cell']; + if ($tlCell !== '' && $brCell !== '') { + $tlCoordinate = Coordinate::indexesFromString($tlCell); + $brCoordinate = Coordinate::indexesFromString($brCell); + $totalHeight = 0.0; + $totalWidth = 0.0; + $defaultRowHeight = $sheet->getDefaultRowDimension()->getRowHeight(); + $defaultRowHeight = SharedDrawing::pointsToPixels(($defaultRowHeight >= 0) ? $defaultRowHeight : SharedFont::getDefaultRowHeightByFont($this->defaultFont)); + if ($tlCoordinate[1] <= $brCoordinate[1] && $tlCoordinate[0] <= $brCoordinate[0]) { + for ($row = $tlCoordinate[1]; $row <= $brCoordinate[1]; ++$row) { + $height = $sheet->getRowDimension($row)->getRowHeight('pt'); + $totalHeight += ($height >= 0) ? $height : $defaultRowHeight; + } + $rightEdge = $brCoordinate[2]; + ++$rightEdge; + for ($column = $tlCoordinate[2]; $column !== $rightEdge; ++$column) { + $width = $sheet->getColumnDimension($column)->getWidth(); + $width = ($width < 0) ? self::DEFAULT_CELL_WIDTH_PIXELS : SharedDrawing::cellDimensionToPixels($sheet->getColumnDimension($column)->getWidth(), $this->defaultFont); + $totalWidth += $width; + } + $chart->setRenderedWidth($totalWidth); + $chart->setRenderedHeight($totalHeight); + } + } + } + /** * Generate CSS styles. * @@ -844,8 +887,8 @@ private function buildCssPerSheet(Worksheet $sheet, array &$css): void $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1; $column = -1; while ($column++ < $highestColumnIndex) { - $this->columnWidths[$sheetIndex][$column] = 42; // approximation - $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt'; + $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt'; } // col elements, loop through columnDimensions and set width From 276f7813d5ac3905b010b609f8d31334589ed35b Mon Sep 17 00:00:00 2001 From: Adrian <54487341+AdrianBatista@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:56:22 -0300 Subject: [PATCH 10/20] check if coordinate is inside range (#3779) * check if coordinate is inside range * Added coordinateIsInsideRange tests * fix coordinateIsInsideRange error throwing * fix coordinateIsInsideRange error throwing * add support to worksheet name * validateReferenceAndGetData type * change validate and add tests data * fix tests data reference * fix absolute reference error * fix boundaries to get range * fix scrutinizer erros * Additional Test Cases --------- Co-authored-by: oleibman <10341515+oleibman@users.noreply.github.com> --- src/PhpSpreadsheet/Cell/Coordinate.php | 86 +++++++++++++++++++ .../Cell/CoordinateTest.php | 35 ++++++++ tests/data/Cell/CoordinateIsInsideRange.php | 33 +++++++ .../Cell/CoordinateIsInsideRangeException.php | 8 ++ 4 files changed, 162 insertions(+) create mode 100644 tests/data/Cell/CoordinateIsInsideRange.php create mode 100644 tests/data/Cell/CoordinateIsInsideRangeException.php diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 564d280a2e..81ad64d4f9 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -14,6 +14,7 @@ abstract class Coordinate { public const A1_COORDINATE_REGEX = '/^(?\$?[A-Z]{1,3})(?\$?\d{1,7})$/i'; + public const FULL_REFERENCE_REGEX = '/^(?:(?[^!]*)!)?(?(?[$]?[A-Z]{1,3}[$]?\d{1,7})(?:\:(?[$]?[A-Z]{1,3}[$]?\d{1,7}))?)$/i'; /** * Default range variable constant. @@ -258,6 +259,91 @@ public static function getRangeBoundaries(string $range) ]; } + /** + * Check if cell or range reference is valid and return an array with type of reference (cell or range), worksheet (if it was given) + * and the coordinate or the first coordinate and second coordinate if it is a range. + * + * @param string $reference Coordinate or Range (e.g. A1:A1, B2, B:C, 2:3) + * + * @return array reference data + */ + private static function validateReferenceAndGetData($reference): array + { + $data = []; + preg_match(self::FULL_REFERENCE_REGEX, $reference, $matches); + if (count($matches) === 0) { + return ['type' => 'invalid']; + } + + if (isset($matches['secondCoordinate'])) { + $data['type'] = 'range'; + $data['firstCoordinate'] = str_replace('$', '', $matches['firstCoordinate']); + $data['secondCoordinate'] = str_replace('$', '', $matches['secondCoordinate']); + } else { + $data['type'] = 'coordinate'; + $data['coordinate'] = str_replace('$', '', $matches['firstCoordinate']); + } + + $worksheet = $matches['worksheet']; + if ($worksheet !== '') { + if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") { + $worksheet = substr($worksheet, 1, -1); + } + $data['worksheet'] = strtolower($worksheet); + } + $data['localReference'] = str_replace('$', '', $matches['localReference']); + + return $data; + } + + /** + * Check if coordinate is inside a range. + * + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) + * @param string $coordinate Cell coordinate (e.g. A1) + * + * @return bool true if coordinate is inside range + */ + public static function coordinateIsInsideRange(string $range, string $coordinate): bool + { + $rangeData = self::validateReferenceAndGetData($range); + if ($rangeData['type'] === 'invalid') { + throw new Exception('First argument needs to be a range'); + } + + $coordinateData = self::validateReferenceAndGetData($coordinate); + if ($coordinateData['type'] === 'invalid') { + throw new Exception('Second argument needs to be a single coordinate'); + } + + if (isset($coordinateData['worksheet']) && !isset($rangeData['worksheet'])) { + return false; + } + if (!isset($coordinateData['worksheet']) && isset($rangeData['worksheet'])) { + return false; + } + + if (isset($coordinateData['worksheet'], $rangeData['worksheet'])) { + if ($coordinateData['worksheet'] !== $rangeData['worksheet']) { + return false; + } + } + + $boundaries = self::rangeBoundaries($rangeData['localReference']); + $coordinates = self::indexesFromString($coordinateData['localReference']); + + $columnIsInside = $boundaries[0][0] <= $coordinates[0] && $coordinates[0] <= $boundaries[1][0]; + if (!$columnIsInside) { + return false; + } + $rowIsInside = $boundaries[0][1] <= $coordinates[1] && $coordinates[1] <= $boundaries[1][1]; + if (!$rowIsInside) { + return false; + } + + return true; + } + /** * Column index from string. * diff --git a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php index 1f1d85ee35..e1a527496f 100644 --- a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php @@ -300,6 +300,41 @@ public static function providerGetRangeBoundaries(): array return require 'tests/data/CellGetRangeBoundaries.php'; } + /** + * @dataProvider providerCoordinateIsInsideRange + */ + public static function testCoordinateIsInsideRange(bool $expectedResult, string $range, string $coordinate): void + { + $result = Coordinate::coordinateIsInsideRange($range, $coordinate); + self::assertEquals($result, $expectedResult); + } + + public static function providerCoordinateIsInsideRange(): array + { + return require 'tests/data/Cell/CoordinateIsInsideRange.php'; + } + + /** + * @dataProvider providerCoordinateIsInsideRangeException + */ + public static function testCoordinateIsInsideRangeException(string $expectedResult, string $range, string $coordinate): void + { + try { + Coordinate::coordinateIsInsideRange($range, $coordinate); + } catch (\Exception $e) { + self::assertInstanceOf(Exception::class, $e); + self::assertEquals($e->getMessage(), $expectedResult); + + return; + } + self::fail('An expected exception has not been raised.'); + } + + public static function providerCoordinateIsInsideRangeException(): array + { + return require 'tests/data/Cell/CoordinateIsInsideRangeException.php'; + } + /** * @dataProvider providerExtractAllCellReferencesInRange */ diff --git a/tests/data/Cell/CoordinateIsInsideRange.php b/tests/data/Cell/CoordinateIsInsideRange.php new file mode 100644 index 0000000000..c71c712307 --- /dev/null +++ b/tests/data/Cell/CoordinateIsInsideRange.php @@ -0,0 +1,33 @@ + [true, 'Sheet!A1:E20', 'sheet!B4'], + 'apostrophes 1st sheetname not 2nd' => [true, '\'Sheet\'!A1:E20', 'sheet!B4'], + 'apostrophes 2nd sheetname not 1st' => [true, 'Sheet!A1:E20', '\'sheet\'!B4'], + [false, 'Sheet!A1:E20', 'Sheet!F36'], + [true, 'Sheet!$A$1:$E$20', 'Sheet!$B$4'], + [false, 'Sheet!$A$1:$E$20', 'Sheet!$F$36'], + [false, 'Sheet!A1:E20', 'B4'], + [false, 'Sheet!A1:E20', 'F36'], + [false, 'Sheet!$A$1:$E$20', '$B$4'], + [false, 'Sheet!$A$1:$E$20', '$F$36'], + [false, 'A1:E20', 'Sheet!B4'], + [false, 'A1:E20', 'Sheet!F36'], + [false, '$A$1:$E$20', 'Sheet!$B$4'], + [false, '$A$1:$E$20', 'Sheet!$F$36'], + [true, '\'Sheet space\'!A1:E20', '\'Sheet space\'!B4'], + [false, '\'Sheet space\'!A1:E20', '\'Sheet space\'!F36'], + [true, '\'Sheet space\'!$A$1:$E$20', '\'Sheet space\'!$B$4'], + [false, '\'Sheet space\'!$A$1:$E$20', '\'Sheet space\'!$F$36'], +]; diff --git a/tests/data/Cell/CoordinateIsInsideRangeException.php b/tests/data/Cell/CoordinateIsInsideRangeException.php new file mode 100644 index 0000000000..2ec8ca56c2 --- /dev/null +++ b/tests/data/Cell/CoordinateIsInsideRangeException.php @@ -0,0 +1,8 @@ + Date: Sat, 18 Nov 2023 15:24:18 +0100 Subject: [PATCH 11/20] Use case insentive comparison to get sheet name (#3791) --- src/PhpSpreadsheet/Spreadsheet.php | 2 +- tests/PhpSpreadsheetTests/SpreadsheetTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index fa8cbcca7a..bc1a103df6 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -701,7 +701,7 @@ public function getSheetByName($worksheetName) { $worksheetCount = count($this->workSheetCollection); for ($i = 0; $i < $worksheetCount; ++$i) { - if ($this->workSheetCollection[$i]->getTitle() === trim($worksheetName, "'")) { + if (strcasecmp($this->workSheetCollection[$i]->getTitle(), trim($worksheetName, "'")) === 0) { return $this->workSheetCollection[$i]; } } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php index fe4e986439..12fe4abcb1 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -70,11 +70,22 @@ public function testAddSheetDuplicateTitle(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage("Workbook already contains a worksheet named 'someSheet2'. Rename this worksheet first."); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); $spreadsheet->addSheet($sheet); } + public function testAddSheetDuplicateTitleWithDifferentCase(): void + { + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(Exception::class); + $this->expectExceptionMessage("Workbook already contains a worksheet named 'SomeSheet2'. Rename this worksheet first."); + $sheet = new Worksheet(); + $sheet->setTitle('SomeSheet2'); + $spreadsheet->addSheet($sheet); + } + public function testAddSheetNoAdjustActive(): void { $spreadsheet = $this->getSpreadsheet(); @@ -101,6 +112,7 @@ public function testRemoveSheetIndexTooHigh(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('You tried to remove a sheet by the out of bounds index: 4. The actual number of sheets is 3.'); $spreadsheet->removeSheetByIndex(4); } @@ -126,6 +138,7 @@ public function testGetSheetIndexTooHigh(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Your requested sheet index: 4 is out of bounds. The actual number of sheets is 3.'); $spreadsheet->getSheet(4); } @@ -133,6 +146,7 @@ public function testGetIndexNonExistent(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Sheet does not exist.'); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); $spreadsheet->getIndex($sheet); @@ -178,6 +192,7 @@ public function testSetActiveSheetIndexTooHigh(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('You tried to set a sheet active by the out of bounds index: 4. The actual number of sheets is 3.'); $spreadsheet->setActiveSheetIndex(4); } @@ -185,6 +200,7 @@ public function testSetActiveSheetNoSuchName(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Workbook does not contain sheet:unknown'); $spreadsheet->setActiveSheetIndexByName('unknown'); } @@ -213,6 +229,7 @@ public function testAddExternal(): void public function testAddExternalDuplicateName(): void { $this->expectException(Exception::class); + $this->expectExceptionMessage("Workbook already contains a worksheet named 'someSheet1'. Rename the external sheet first."); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->createSheet()->setTitle('someSheet1'); $sheet->getCell('A1')->setValue(1); From 5a60ba45abfb975695d4353d34ee1debfe0ef2d2 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:06:12 -0800 Subject: [PATCH 12/20] Sheet Background Images (#3795) * Sheet Background Images Fix #1649, a 3-year-old issue long marked "stale". Excel supports background images on sheets; now PhpSpreadsheet will as well. Support is limited to Xlsx (read and write) and Html (write only). As far as I can tell, Excel Xml and Gnumeric do not support this, nor, of course, do Csv and Slk; Excel Xls does, but, as usual, how to handle it in BIFF format is a mystery; LibreOffice ODS supports it differently than Excel, and this is just another of many ODS style properties not currently supported by PhpSpreadsheet. * Update CHANGELOG.md --- CHANGELOG.md | 3 ++ src/PhpSpreadsheet/Reader/Xlsx.php | 22 ++++++++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 43 +++++++++++++++ src/PhpSpreadsheet/Writer/Html.php | 5 ++ src/PhpSpreadsheet/Writer/Xlsx.php | 2 +- .../Writer/Xlsx/ContentTypes.php | 7 +++ src/PhpSpreadsheet/Writer/Xlsx/Rels.php | 16 +++++- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 44 ++++++++++++--- .../Writer/Html/BackgroundImageTest.php | 31 +++++++++++ .../Writer/Xlsx/BackgroundImageTest.php | 50 ++++++++++++++++++ tests/data/Writer/XLSX/backgroundtest.png | Bin 0 -> 429 bytes 11 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php create mode 100644 tests/data/Writer/XLSX/backgroundtest.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 6decabdc31..274981073e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Support for Conditional Formatting Color Scale. [PR #3738](https://github.com/PHPOffice/PhpSpreadsheet/pull/3738) - Support Additional Tags in Helper/Html. [Issue #3751](https://github.com/PHPOffice/PhpSpreadsheet/issues/3751) [PR #3752](https://github.com/PHPOffice/PhpSpreadsheet/pull/3752) - Writer ODS : Write Border Style for cells [Issue #3690](https://github.com/PHPOffice/PhpSpreadsheet/issues/3690) [PR #3693](https://github.com/PHPOffice/PhpSpreadsheet/pull/3693) +- Sheet Background Images [Issue #1649](https://github.com/PHPOffice/PhpSpreadsheet/issues/1649) [PR #3795](https://github.com/PHPOffice/PhpSpreadsheet/pull/3795) +- Check if Coordinate is Inside Range [PR #3779](https://github.com/PHPOffice/PhpSpreadsheet/pull/3779) ### Changed @@ -72,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Theme File Missing but Referenced in Spreadsheet. [Issue #3770](https://github.com/PHPOffice/PhpSpreadsheet/issues/3770) [PR #3772](https://github.com/PHPOffice/PhpSpreadsheet/pull/3772) - Slk Shared Formulas. [Issue #2267](https://github.com/PHPOffice/PhpSpreadsheet/issues/2267) [PR #3776](https://github.com/PHPOffice/PhpSpreadsheet/pull/3776) - Html omitting some charts. [Issue #3767](https://github.com/PHPOffice/PhpSpreadsheet/issues/3767) [PR #3771](https://github.com/PHPOffice/PhpSpreadsheet/pull/3771) +- Case Insensitive Comparison for Sheet Names [PR #3791](https://github.com/PHPOffice/PhpSpreadsheet/pull/3791) ## 1.29.0 - 2023-06-15 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 99fb3937d5..73ac43afd8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -973,6 +973,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet if ($this->readDataOnly === false) { $this->readAutoFilter($xmlSheetNS, $docSheet); + $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'); } $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS); @@ -2201,6 +2202,27 @@ private function readAutoFilter( } } + private function readBackgroundImage( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet, + string $relsName + ): void { + if ($xmlSheet && $xmlSheet->picture) { + $id = (string) self::getArrayItem(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id'); + $rels = $this->loadZip($relsName); + foreach ($rels->Relationship as $rel) { + $attrs = $rel->attributes() ?? []; + $rid = (string) ($attrs['Id'] ?? ''); + $target = (string) ($attrs['Target'] ?? ''); + if ($rid === $id && substr($target, 0, 2) === '..') { + $target = 'xl' . substr($target, 2); + $content = $this->getFromZipArchive($this->zip, $target); + $docSheet->setBackgroundImage($content); + } + } + } + } + private function readTables( SimpleXMLElement $xmlSheet, Worksheet $docSheet, diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 8ed1d22eb7..89c5458525 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3879,4 +3879,47 @@ private function getXfIndex(string $coordinate): ?int return $xfIndex; } + + private string $backgroundImage = ''; + + private string $backgroundMime = ''; + + private string $backgroundExtension = ''; + + public function getBackgroundImage(): string + { + return $this->backgroundImage; + } + + public function getBackgroundMime(): string + { + return $this->backgroundMime; + } + + public function getBackgroundExtension(): string + { + return $this->backgroundExtension; + } + + /** + * Set background image. + * Used on read/write for Xlsx. + * Used on write for Html. + * + * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents + */ + public function setBackgroundImage(string $backgroundImage): self + { + $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => '']; + $mime = $imageArray['mime']; + if ($mime !== '') { + $extension = explode('/', $mime); + $extension = $extension[1]; + $this->backgroundImage = $backgroundImage; + $this->backgroundMime = $mime; + $this->backgroundExtension = $extension; + } + + return $this; + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index ecb99219d1..b4547637cf 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -878,6 +878,11 @@ private function buildCssPerSheet(Worksheet $sheet, array &$css): void $css["table.sheet$sheetIndex"]['page-break-inside'] = 'avoid'; $css["table.sheet$sheetIndex"]['break-inside'] = 'avoid'; } + $picture = $sheet->getBackgroundImage(); + if ($picture !== '') { + $base64 = base64_encode($picture); + $css["table.sheet$sheetIndex"]['background-image'] = 'url(data:' . $sheet->getBackgroundMime() . ';base64,' . $base64 . ')'; + } // Build styles // Calculate column widths diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index c18bc85862..e127ccac90 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -365,7 +365,7 @@ public function save($filename, int $flags = 0): void // Add worksheet relationships (drawings, ...) for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { // Add relationships - $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1); + $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1, $zipContent); // Add unparsedLoadedData $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php index 8faa3ffc2d..3357e657f3 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -186,6 +186,13 @@ public function writeContentTypes(Spreadsheet $spreadsheet, $includeCharts = fal } } } + + $bgImage = $spreadsheet->getSheet($i)->getBackgroundImage(); + $mimeType = $spreadsheet->getSheet($i)->getBackgroundMime(); + $extension = $spreadsheet->getSheet($i)->getBackgroundExtension(); + if ($bgImage !== '' && !isset($aMediaContentTypes[$mimeType])) { + $this->writeDefaultContentType($objWriter, $extension, $mimeType); + } } // unparsed defaults diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 977dc9abf6..dce5882ce6 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -169,7 +169,7 @@ public function writeWorkbookRelationships(Spreadsheet $spreadsheet) * * @return string XML Output */ - public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1) + public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1, array &$zipContent = []) { // Create XML writer $objWriter = null; @@ -221,6 +221,20 @@ public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\ ); } + $backgroundImage = $worksheet->getBackgroundImage(); + if ($backgroundImage !== '') { + $rId = 'Bg'; + $uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999)); + $relPath = "../media/$uniqueName." . $worksheet->getBackgroundExtension(); + $this->writeRelationship( + $objWriter, + $rId, + Namespaces::IMAGE, + $relPath + ); + $zipContent["xl/media/$uniqueName." . $worksheet->getBackgroundExtension()] = $backgroundImage; + } + // Write hyperlink relationships? $i = 1; foreach ($worksheet->getHyperlinkCollection() as $hyperlink) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 7efaaf9396..de9cc49e7e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -139,6 +139,9 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, array $string // IgnoredErrors $this->writeIgnoredErrors($objWriter); + // BackgroundImage must come after ignored, before table + $this->writeBackgroundImage($objWriter, $worksheet); + // Table $this->writeTable($objWriter, $worksheet); @@ -1042,6 +1045,9 @@ private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $ private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void { $tableCount = $worksheet->getTableCollection()->count(); + if ($tableCount === 0) { + return; + } $objWriter->startElement('tableParts'); $objWriter->writeAttribute('count', (string) $tableCount); @@ -1055,6 +1061,18 @@ private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $works $objWriter->endElement(); } + /** + * Write Background Image. + */ + private function writeBackgroundImage(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void + { + if ($worksheet->getBackgroundImage() !== '') { + $objWriter->startElement('picture'); + $objWriter->writeAttribute('r:id', 'rIdBg'); + $objWriter->endElement(); + } + } + /** * Write PageSetup. */ @@ -1098,19 +1116,31 @@ private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $w private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void { // headerFooter + $headerFooter = $worksheet->getHeaderFooter(); + $oddHeader = $headerFooter->getOddHeader(); + $oddFooter = $headerFooter->getOddFooter(); + $evenHeader = $headerFooter->getEvenHeader(); + $evenFooter = $headerFooter->getEvenFooter(); + $firstHeader = $headerFooter->getFirstHeader(); + $firstFooter = $headerFooter->getFirstFooter(); + if ("$oddHeader$oddFooter$evenHeader$evenFooter$firstHeader$firstFooter" === '') { + return; + } + $objWriter->startElement('headerFooter'); $objWriter->writeAttribute('differentOddEven', ($worksheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false')); $objWriter->writeAttribute('differentFirst', ($worksheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false')); $objWriter->writeAttribute('scaleWithDoc', ($worksheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false')); $objWriter->writeAttribute('alignWithMargins', ($worksheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false')); - $objWriter->writeElement('oddHeader', $worksheet->getHeaderFooter()->getOddHeader()); - $objWriter->writeElement('oddFooter', $worksheet->getHeaderFooter()->getOddFooter()); - $objWriter->writeElement('evenHeader', $worksheet->getHeaderFooter()->getEvenHeader()); - $objWriter->writeElement('evenFooter', $worksheet->getHeaderFooter()->getEvenFooter()); - $objWriter->writeElement('firstHeader', $worksheet->getHeaderFooter()->getFirstHeader()); - $objWriter->writeElement('firstFooter', $worksheet->getHeaderFooter()->getFirstFooter()); - $objWriter->endElement(); + self::writeElementIf($objWriter, $oddHeader !== '', 'oddHeader', $oddHeader); + self::writeElementIf($objWriter, $oddFooter !== '', 'oddFooter', $oddFooter); + self::writeElementIf($objWriter, $evenHeader !== '', 'evenHeader', $evenHeader); + self::writeElementIf($objWriter, $evenFooter !== '', 'evenFooter', $evenFooter); + self::writeElementIf($objWriter, $firstHeader !== '', 'firstHeader', $firstHeader); + self::writeElementIf($objWriter, $firstFooter !== '', 'firstFooter', $firstFooter); + + $objWriter->endElement(); // headerFooter } /** diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php new file mode 100644 index 0000000000..df73c9cd86 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php @@ -0,0 +1,31 @@ +getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue(2); + $sheet->getCell('A2')->setValue(3); + $sheet->getCell('B2')->setValue(4); + $imageFile = 'tests/data/Writer/XLSX/backgroundtest.png'; + $image = (string) file_get_contents($imageFile); + $sheet->setBackgroundImage($image); + self::assertSame('image/png', $sheet->getBackgroundMime()); + self::assertSame('png', $sheet->getBackgroundExtension()); + $writer = new Html($spreadsheet); + $header = $writer->generateHTMLHeader(true); + self::assertStringContainsString('table.sheet0 { background-image:url(data:image/png;base64,', $header); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php new file mode 100644 index 0000000000..73a32db3ae --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php @@ -0,0 +1,50 @@ +getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue(2); + $sheet->getCell('A2')->setValue(3); + $sheet->getCell('B2')->setValue(4); + $imageFile = 'tests/data/Writer/XLSX/backgroundtest.png'; + $image = (string) file_get_contents($imageFile); + $sheet->setBackgroundImage($image); + self::assertSame('image/png', $sheet->getBackgroundMime()); + self::assertSame('png', $sheet->getBackgroundExtension()); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame($image, $reloadedWorksheet->getBackgroundImage()); + self::assertSame('image/png', $reloadedWorksheet->getBackgroundMime()); + self::assertSame('png', $reloadedWorksheet->getBackgroundExtension()); + self::assertSame(2, $reloadedWorksheet->getCell('B1')->getValue()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testInvalidImage(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $imageFile = __FILE__; + $image = (string) file_get_contents($imageFile); + self::assertNotSame('', $image); + $sheet->setBackgroundImage($image); + self::assertSame('', $sheet->getBackgroundImage()); + self::assertSame('', $sheet->getBackgroundMime()); + self::assertSame('', $sheet->getBackgroundExtension()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Writer/XLSX/backgroundtest.png b/tests/data/Writer/XLSX/backgroundtest.png new file mode 100644 index 0000000000000000000000000000000000000000..3f14e4b3f0b1b3132884e84de4efc5a18278246e GIT binary patch literal 429 zcmeAS@N?(olHy`uVBq!ia0vp^DIm_i+ALso$kF{Uk zIkRO=oSDD++jq-fNRLZ~gQ7H5+GdG%fSakEn?$7t_E4E736{zL literal 0 HcmV?d00001 From 7fbf5c494fd2a56c78f9cab410cb06eae474e9b1 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 30 Nov 2023 07:31:42 -0800 Subject: [PATCH 13/20] Anticipate Dependabot (#3805) These changes will be suggested tomorrow. It's better to take care of them in advance. --- composer.lock | 92 +++++++++---------- phpstan-baseline.neon | 25 ----- .../Calculation/Calculation.php | 2 +- .../Calculation/FormulaParser.php | 24 ++--- src/PhpSpreadsheet/Worksheet/Validations.php | 8 +- tests/bootstrap.php | 2 +- 6 files changed, 64 insertions(+), 89 deletions(-) diff --git a/composer.lock b/composer.lock index e2e3341b20..a3d742130f 100644 --- a/composer.lock +++ b/composer.lock @@ -1358,12 +1358,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "b49fdb59dd34284bd347435a959f2093894d1ac8" + "reference": "eda4086d152f545894f94018cb208f38dc251172" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/b49fdb59dd34284bd347435a959f2093894d1ac8", - "reference": "b49fdb59dd34284bd347435a959f2093894d1ac8", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/eda4086d152f545894f94018cb208f38dc251172", + "reference": "eda4086d152f545894f94018cb208f38dc251172", "shasum": "" }, "require": { @@ -1428,7 +1428,7 @@ "issues": "https://github.com/PHPCSStandards/composer-installer/issues", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-09-22T07:48:18+00:00" + "time": "2023-11-26T02:51:26+00:00" }, { "name": "doctrine/instantiator", @@ -1564,50 +1564,50 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.37.1", + "version": "v3.40.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "c3fe76976081ab871aa654e872da588077e19679" + "reference": "27d2b3265b5d550ec411b4319967ae7cfddfb2e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c3fe76976081ab871aa654e872da588077e19679", - "reference": "c3fe76976081ab871aa654e872da588077e19679", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/27d2b3265b5d550ec411b4319967ae7cfddfb2e0", + "reference": "27d2b3265b5d550ec411b4319967ae7cfddfb2e0", "shasum": "" }, "require": { - "composer/semver": "^3.3", + "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", "ext-json": "*", "ext-tokenizer": "*", "php": "^7.4 || ^8.0", "sebastian/diff": "^4.0 || ^5.0", - "symfony/console": "^5.4 || ^6.0", - "symfony/event-dispatcher": "^5.4 || ^6.0", - "symfony/filesystem": "^5.4 || ^6.0", - "symfony/finder": "^5.4 || ^6.0", - "symfony/options-resolver": "^5.4 || ^6.0", - "symfony/polyfill-mbstring": "^1.27", - "symfony/polyfill-php80": "^1.27", - "symfony/polyfill-php81": "^1.27", - "symfony/process": "^5.4 || ^6.0", - "symfony/stopwatch": "^5.4 || ^6.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3 || ^2.0", "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^2.0", + "keradus/cli-executor": "^2.1", "mikey179/vfsstream": "^1.6.11", - "php-coveralls/php-coveralls": "^2.5.3", + "php-coveralls/php-coveralls": "^2.7", "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy": "^1.16", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", + "phpspec/prophecy": "^1.17", "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "symfony/phpunit-bridge": "^6.2.3", - "symfony/yaml": "^5.4 || ^6.0" + "phpunit/phpunit": "^9.6", + "symfony/phpunit-bridge": "^6.3.8 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1645,7 +1645,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.37.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.40.0" }, "funding": [ { @@ -1653,7 +1653,7 @@ "type": "github" } ], - "time": "2023-10-29T20:51:23+00:00" + "time": "2023-11-26T09:25:53+00:00" }, { "name": "masterminds/html5", @@ -1770,16 +1770,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.2.0", + "version": "v8.2.2", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "170a236a588d177c2aa7447ce490a030ca68e6f4" + "reference": "596a87b876d7793be7be060a8ac13424de120dd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/170a236a588d177c2aa7447ce490a030ca68e6f4", - "reference": "170a236a588d177c2aa7447ce490a030ca68e6f4", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/596a87b876d7793be7be060a8ac13424de120dd5", + "reference": "596a87b876d7793be7be060a8ac13424de120dd5", "shasum": "" }, "require": { @@ -1789,7 +1789,7 @@ "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", "myclabs/deep-copy": "^1.7", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "psr/http-message": "^1.0 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "setasign/fpdi": "^2.1" @@ -1847,7 +1847,7 @@ "type": "custom" } ], - "time": "2023-09-01T11:44:52+00:00" + "time": "2023-11-07T13:52:14+00:00" }, { "name": "mpdf/psr-http-message-shim", @@ -2371,16 +2371,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.40", + "version": "1.10.46", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d" + "reference": "90d3d25c5b98b8068916bbf08ce42d5cb6c54e70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/93c84b5bf7669920d823631e39904d69b9c7dc5d", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/90d3d25c5b98b8068916bbf08ce42d5cb6c54e70", + "reference": "90d3d25c5b98b8068916bbf08ce42d5cb6c54e70", "shasum": "" }, "require": { @@ -2429,7 +2429,7 @@ "type": "tidelift" } ], - "time": "2023-10-30T14:48:31+00:00" + "time": "2023-11-28T14:57:26+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -5327,16 +5327,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -5365,7 +5365,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -5373,7 +5373,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" } ], "aliases": [], diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c7cbf42ca9..c9762340b2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,31 +1,6 @@ parameters: ignoreErrors: - - - message: "#^Cannot call method getTokenSubType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 4 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - - message: "#^Cannot call method getTokenType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 8 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - message: "#^Binary operation \"/\" between float and array\\|float\\|int\\|string results in an error\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php - - - - message: "#^Offset 2 does not exist on array\\{int, int, int, int\\}\\|array\\{int, int\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Validations.php - - - - message: "#^Offset 3 does not exist on array\\{int, int, int, int\\}\\|array\\{int, int\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Validations.php - - - - message: "#^Parameter \\#2 \\$value of function ini_set expects string, int given\\.$#" - count: 1 - path: tests/bootstrap.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index d8d0557399..34062a5319 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4715,7 +4715,7 @@ private function processTokenStack(mixed $tokens, $cellID = null, ?Cell $cell = if (($operand2Data = $stack->pop()) === null) { return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } - if (($operand1Data = $stack->pop()) === null) { + if (($operand1Data = $stack->pop()) === null) { // @phpstan-ignore-line return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } diff --git a/src/PhpSpreadsheet/Calculation/FormulaParser.php b/src/PhpSpreadsheet/Calculation/FormulaParser.php index cffce179c6..0be1ca8ac9 100644 --- a/src/PhpSpreadsheet/Calculation/FormulaParser.php +++ b/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -530,12 +530,12 @@ private function parseToTokens(): void if ($i == 0) { $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX); } elseif ( - (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) ) { $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); } else { @@ -551,12 +551,12 @@ private function parseToTokens(): void if ($i == 0) { continue; } elseif ( - (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) ) { $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); } else { diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index d6e5582809..9ce7652690 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -81,14 +81,14 @@ public static function validateCellRange(AddressRange|array|string $cellRange): if (is_array($cellRange)) { switch (count($cellRange)) { - case 2: + case 4: $from = [$cellRange[0], $cellRange[1]]; - $to = [$cellRange[0], $cellRange[1]]; + $to = [$cellRange[2], $cellRange[3]]; break; - case 4: + case 2: $from = [$cellRange[0], $cellRange[1]]; - $to = [$cellRange[2], $cellRange[3]]; + $to = [$cellRange[0], $cellRange[1]]; break; default: diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4cd23357de..5b48e70f9b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -32,6 +32,6 @@ function phpunit10ErrorHandler(int $errno, string $errstr, string $filename, int } if (!method_exists(\PHPUnit\Framework\TestCase::class, 'setOutputCallback')) { - ini_set('error_reporting', E_ALL); + ini_set('error_reporting', (string) E_ALL); set_error_handler('phpunit10ErrorHandler'); } From 009e00981151275ea1cdd521e730c9cbe497cee2 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:01:56 -0800 Subject: [PATCH 14/20] Chart Dynamic Title and Special Font Properties (#3800) * Chart Dynamic Title and Special Font Properties Fix #3797. Excel allows a Chart Title to be a formula, albeit a very rigidly limited one. It can only be a reference to a single cell, and the worksheet name must be specified, and the column and row must be absolute. Methods are added to Chart/Title to accommodate this (and styling for it). This will be handled for input/output for Xlsx, and for output for Html. The sample file which was submitted with this issue demonstrated that something else was missing. When setting the font for a chart title in Excel, you can specify all-caps or small-caps, options not available for most cell formatting. These are now added. The sample file also fell into the category of spreadsheets which lose one or more charts when converted to Html. I have redone the "extend rows and charts" logic in Html Writer. It is now clearer (I hope) and more efficient, and hopefully this problem will not arise again. * Scrutinizer 50/50 One false positive, one correct "unused parameter". --- samples/Chart/32_Chart_read_write.php | 2 +- samples/Chart/37_Chart_dynamic_title.php | 83 ++++++++++ samples/templates/37dynamictitle.xlsx | Bin 0 -> 10153 bytes src/PhpSpreadsheet/Chart/Title.php | 58 ++++++- src/PhpSpreadsheet/Helper/Sample.php | 19 ++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 21 ++- src/PhpSpreadsheet/Settings.php | 5 + src/PhpSpreadsheet/Style/Font.php | 31 ++++ src/PhpSpreadsheet/Writer/Html.php | 150 +++++++----------- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 69 +++++++- .../Chart/ChartsDynamicTitleTest.php | 141 ++++++++++++++++ 11 files changed, 480 insertions(+), 99 deletions(-) create mode 100644 samples/Chart/37_Chart_dynamic_title.php create mode 100644 samples/templates/37dynamictitle.xlsx create mode 100644 tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php diff --git a/samples/Chart/32_Chart_read_write.php b/samples/Chart/32_Chart_read_write.php index a1f2f54681..42944c0ccb 100644 --- a/samples/Chart/32_Chart_read_write.php +++ b/samples/Chart/32_Chart_read_write.php @@ -42,7 +42,7 @@ foreach ($chartNames as $i => $chartName) { $chart = $worksheet->getChartByName($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } diff --git a/samples/Chart/37_Chart_dynamic_title.php b/samples/Chart/37_Chart_dynamic_title.php new file mode 100644 index 0000000000..b8b801faae --- /dev/null +++ b/samples/Chart/37_Chart_dynamic_title.php @@ -0,0 +1,83 @@ +log('File ' . $inputFileNameShort . ' does not exist'); + + continue; + } + $reader = IOFactory::createReader($inputFileType); + $reader->setIncludeCharts(true); + $callStartTime = microtime(true); + $spreadsheet = $reader->load($inputFileName); + $helper->logRead($inputFileType, $inputFileName, $callStartTime); + + $helper->log('Iterate worksheets looking at the charts'); + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $sheetName = $worksheet->getTitle(); + $worksheet->getCell('A1')->setValue('Changed Title'); + $helper->log('Worksheet: ' . $sheetName); + + $chartNames = $worksheet->getChartNames(); + if (empty($chartNames)) { + $helper->log(' There are no charts in this worksheet'); + } else { + natsort($chartNames); + foreach ($chartNames as $i => $chartName) { + $chart = $worksheet->getChartByName($chartName); + if ($chart->getTitle() !== null) { + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; + } else { + $caption = 'Untitled'; + } + $helper->log(' ' . $chartName . ' - ' . $caption); + $indentation = str_repeat(' ', strlen($chartName) + 3); + $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + if ($groupCount == 1) { + $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $helper->log($indentation . ' ' . $chartType); + $helper->renderChart($chart, __FILE__, $spreadsheet); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = 'Multiple Plot ' . array_pop($chartTypes); + $helper->log($indentation . ' ' . $chartType); + $helper->renderChart($chart, __FILE__); + } elseif (count($chartTypes) == 0) { + $helper->log($indentation . ' *** Type not yet implemented'); + } else { + $helper->log($indentation . ' Combination Chart'); + $helper->renderChart($chart, __FILE__); + } + } + } + } + } + + $callStartTime = microtime(true); + $helper->write($spreadsheet, $inputFileName, ['Xlsx'], true); + + Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); + $callStartTime = microtime(true); + $helper->write($spreadsheet, $inputFileName, ['Html'], true); + + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); +} diff --git a/samples/templates/37dynamictitle.xlsx b/samples/templates/37dynamictitle.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6a5215e861439d5ae656790c90964948d86bd559 GIT binary patch literal 10153 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≀|Sh z9@FqykNv8VrEWgYy z_jB45cnj}oZPFv0zlL!>9kV}Y;Nik=LTT_qx-Y_Q3Bxp*cU&6npcj2YslKilH2LUK zJII03a?!k0VA;e@1Sxtq|E!s=D}ngMyF$??FZ)Jr$At#l?oGwb9Rqc0JtKnmcZa!9 zu!32Ev{&Hz^Xqc0Em?Zfr;!(06*N4-?2k`+e1zC(ah@oDQyT#ni0dsducF}bG^DTk zCQFxnlK84_wcT=pB&42cz5ggw;WZ5`Q!0Ew{ptX|^VJ>QxA=ueqhYuv?@O*C*u_3X z2)p`c;RwiP3S~b+ib?Qg;i;E$6|)2Pv7_85WgS+>Diz>Wg-_8=QY=5iQXPk{j%T8vyQf{bYS0?$&>kE#u$r}f z6TBjs059tiWYFQF?Xm}V;lb6iXnj5_LcP2Ceq~Jcv1sIw)VDolD0;o>PhO;?4YlUIV(suGL zAIHOT1FPA)aJVE{$@LL)bf*`&be5AV(mN_rDO7PzyhA$?>6}0}%n-)9&(6d}<4*>0 zj=~5N+}C6UM5ZW>{2)if#;{^(Il0y1%}TsiD@VQEL{zdYn7Qy4Sey>ugd8^Do=pL8 zC11mrOBw*Fb+;Ox#_$Htz7!Hi^RonU`UxVvOl^cwqxL)nUSe{Jq!$OIBUzyEc9h{F zD#N@Wpu%s=^si?}#|%0a1gg)G+BR?iA5VV^D(@W`CB|q7zN9@ozGbx6FA0@UhYqW+ zA5ug225X}lQV{3{$u%V)MG$>H7QXdwRhTcWEHiX2!XGR>M7nH79kmU$rJ&iKwz#Nm zTs-1O8!Zdf_d?S+*R7@L4Y^(n-E!@^P?ML5{<82GO!SfS!y1r(Y?c~AWw3ke^J+r z8rVgXyN*vl@uq-jvmQh}?gPf0E_>sg{yVQtWdvkMK=vm~NEknY2nHth#!6214iF+_ z?_~OiPW@k<0J3ibqT-bmyGXG^mgR3T&<>djI3wfhWvBKmBdJF$yu}*9;UPeg-S4fn zi&n3Mt`N5tVw~-0H?!Zr5$fRkybX+_lz3#Zgjmu*<@thTRTi6fwAKL9I~4bFuy~o7 zjkBP%q}A9E4Yf(mj5+ zvuljhwD!6BKslrw6yXt z8rHG(enW5?rlI0#hp0Fi9+4GcQqk?EYpaNQvYCgRRL1ZOT@Gc@6OWy&`=`rrM9foU z-S^?zl1MqYnpmrklyI)ro&N{13f>3xNi-z(a3Hl2^Y_>@ba41@==}(~Kc0+ud20wa z2t7o(!;rj`RG>9QsveBDv&<<7pxoDHh%r{y2BwurU(8n6to2%Y>11xMZA|Blho_4f z*E}Z2KSbur-e80&^08cKGicsl+)zv=qtm3{CBoiA?6+I*!Kiv8Bti-EmI@^duJm+%2eNs|zDm(am2%=l<5t`Is8HRCWdet8To2#jFCr^4v@Bg=WCVmMOm`KQJFoh} zSir_o9V)lTApA1gRu!QqM1t25)yoKI8b_9@luc;cmloT!&Y+@_ISW(6q$tw@#=w=L zz(#~=G}|&caXKBH`ga3;sKd(~^>9PqX@k*=49X%Td1bURZKST};PYwqeZ0Hy{gsX= z@gjV9rWK5xskT~4;kG(Q3;@#6&fR5XC|57a)bs7bw^UcN3|EL8xQLb=wh}A+oSJqL zE(f_qKjyV~X$hhTnXQgDaxa9GwX4IfNsUGtsg@#|UWZiq(TCq~3|sIgsFy7e4=SJ5 zjWcx&Rc<)7z1`S3yBC^)72dD8h1B>z7yuMB6GXH5_p3htS+f7U|K;M3lI-6B{(e2- zKZZZv(U83O?Xto{!@q02zZ-f$dh7qQ^7|0yq5k&^sSI+P>46&f(D#0$7Y_j*HjjP*OhDuh2*9sa(nHgSor+(k?{I#a{-ba45aHno@y1!7iAR^A+o%)OTK1BID+xUe60K6xL^wu77kB8=eFBrd@Q{%846l7)lh S0RVsw`FjsZr5%bNU;ht_z<}rg literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Title.php b/src/PhpSpreadsheet/Chart/Title.php index d8e6e7497f..378987446e 100644 --- a/src/PhpSpreadsheet/Chart/Title.php +++ b/src/PhpSpreadsheet/Chart/Title.php @@ -3,9 +3,16 @@ namespace PhpOffice\PhpSpreadsheet\Chart; use PhpOffice\PhpSpreadsheet\RichText\RichText; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; class Title { + public const TITLE_CELL_REFERENCE + = '/^(.*)!' // beginning of string, everything up to ! is match[1] + . '[$]([A-Z]{1,3})' // absolute column string match[2] + . '[$](\d{1,7})$/i'; // absolute row string match[3] + /** * Title Caption. * @@ -25,6 +32,10 @@ class Title */ private ?Layout $layout; + private string $cellReference = ''; + + private ?Font $font = null; + /** * Create a new Title. * @@ -48,8 +59,14 @@ public function getCaption() return $this->caption; } - public function getCaptionText(): string + public function getCaptionText(?Spreadsheet $spreadsheet = null): string { + if ($spreadsheet !== null) { + $caption = $this->getCalculatedTitle($spreadsheet); + if ($caption !== null) { + return $caption; + } + } $caption = $this->caption; if (is_string($caption)) { return $caption; @@ -100,13 +117,50 @@ public function getOverlay() * * @param bool $overlay */ - public function setOverlay($overlay): void + public function setOverlay($overlay): static { $this->overlay = $overlay; + + return $this; } public function getLayout(): ?Layout { return $this->layout; } + + public function setCellReference(string $cellReference): self + { + $this->cellReference = $cellReference; + + return $this; + } + + public function getCellReference(): string + { + return $this->cellReference; + } + + public function getCalculatedTitle(?Spreadsheet $spreadsheet): ?string + { + preg_match(self::TITLE_CELL_REFERENCE, $this->cellReference, $matches); + if (count($matches) === 0 || $spreadsheet === null) { + return null; + } + $sheetName = preg_replace("/^'(.*)'$/", '$1', $matches[1]) ?? ''; + + return $spreadsheet->getSheetByName($sheetName)?->getCell($matches[2] . $matches[3])?->getFormattedValue(); + } + + public function getFont(): ?Font + { + return $this->font; + } + + public function setFont(?Font $font): self + { + $this->font = $font; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 3d53f1e053..cb23cf597c 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -198,7 +198,7 @@ public function log(string $message): void * * @codeCoverageIgnore */ - public function renderChart(Chart $chart, string $fileName): void + public function renderChart(Chart $chart, string $fileName, ?Spreadsheet $spreadsheet = null): void { if ($this->isCli() === true) { return; @@ -206,17 +206,32 @@ public function renderChart(Chart $chart, string $fileName): void Settings::setChartRenderer(MtJpGraphRenderer::class); $fileName = $this->getFilename($fileName, 'png'); + $title = $chart->getTitle(); + $caption = null; + if ($title !== null) { + $calculatedTitle = $title->getCalculatedTitle($spreadsheet); + if ($calculatedTitle !== null) { + $caption = $title->getCaption(); + $title->setCaption($calculatedTitle); + } + } try { $chart->render($fileName); $this->log('Rendered image: ' . $fileName); - $imageData = file_get_contents($fileName); + $imageData = @file_get_contents($fileName); if ($imageData !== false) { echo '

'; + } else { + $this->log('Unable to open chart' . PHP_EOL); } } catch (Throwable $e) { $this->log('Error rendering chart: ' . $e->getMessage() . PHP_EOL); } + if (isset($title, $caption)) { + $title->setCaption($caption); + } + Settings::unsetChartRenderer(); } public function titles(string $category, string $functionName, ?string $description = null): void diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 789d46ee78..32737f01a6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -497,6 +497,8 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title $caption = []; $titleLayout = null; $titleOverlay = false; + $titleFormula = null; + $titleFont = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($titleDetailKey) { @@ -517,6 +519,9 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title $caption[] = (string) $pt->v; } } + if (isset($chartDetail->strRef->f)) { + $titleFormula = (string) $chartDetail->strRef->f; + } } break; @@ -527,11 +532,24 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title case 'layout': $titleLayout = $this->chartLayoutDetails($chartDetail); + break; + case 'txPr': + if (isset($chartDetail->children($this->aNamespace)->p)) { + $titleFont = $this->parseFont($chartDetail->children($this->aNamespace)->p); + } + break; } } + $title = new Title($caption, $titleLayout, (bool) $titleOverlay); + if (!empty($titleFormula)) { + $title->setCellReference($titleFormula); + } + if ($titleFont !== null) { + $title->setFont($titleFont); + } - return new Title($caption, $titleLayout, (bool) $titleOverlay); + return $title; } private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout @@ -1185,6 +1203,7 @@ private function parseFont(SimpleXMLElement $titleDetailPart): ?Font $fontArray['italic'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i'); $fontArray['underscore'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u'); $fontArray['strikethrough'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike'); + $fontArray['cap'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'cap'); if (isset($titleDetailPart->pPr->defRPr->latin)) { $fontArray['latin'] = self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface'); diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 9f2ef0cd89..f7daa45750 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -78,6 +78,11 @@ public static function setChartRenderer(string $rendererClassName): void self::$chartRenderer = $rendererClassName; } + public static function unsetChartRenderer(): void + { + self::$chartRenderer = null; + } + /** * Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use. * diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 09a24d10ab..ea26b8ce64 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -13,6 +13,13 @@ class Font extends Supervisor const UNDERLINE_SINGLE = 'single'; const UNDERLINE_SINGLEACCOUNTING = 'singleAccounting'; + const CAP_ALL = 'all'; + const CAP_SMALL = 'small'; + const CAP_NONE = 'none'; + private const VALID_CAPS = [self::CAP_ALL, self::CAP_SMALL, self::CAP_NONE]; + + protected ?string $cap = null; + /** * Font Name. * @@ -236,6 +243,9 @@ public function applyFromArray(array $styleArray): static if (isset($styleArray['scheme'])) { $this->setScheme($styleArray['scheme']); } + if (isset($styleArray['cap'])) { + $this->setCap($styleArray['cap']); + } } return $this; @@ -795,6 +805,7 @@ public function getHashCode() $this->hashChartColor($this->chartColor), $this->hashChartColor($this->underlineColor), (string) $this->baseLine, + (string) $this->cap, ] ) . __CLASS__ @@ -806,6 +817,7 @@ protected function exportArray1(): array $exportedArray = []; $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'cap', $this->getCap()); $this->exportArray2($exportedArray, 'chartColor', $this->getChartColor()); $this->exportArray2($exportedArray, 'color', $this->getColor()); $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); @@ -847,4 +859,23 @@ public function setScheme(string $scheme): self return $this; } + + /** + * Set capitalization attribute. If not one of the permitted + * values (all, small, or none), set it to null. + * This will be honored only for the font for chart titles. + * None is distinguished from null because null will inherit + * the current value, whereas 'none' will override it. + */ + public function setCap(string $cap): self + { + $this->cap = in_array($cap, self::VALID_CAPS, true) ? $cap : null; + + return $this; + } + + public function getCap(): ?string + { + return $this->cap; + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index b4547637cf..5df0525f11 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -23,6 +23,7 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; @@ -150,6 +151,12 @@ class Html extends BaseWriter */ private $editHtmlCallback; + /** @var BaseDrawing[] */ + private $sheetDrawings; + + /** @var Chart[] */ + private $sheetCharts; + /** * Create a new HTML. */ @@ -479,11 +486,14 @@ public function generateSheetData(): string foreach ($sheets as $sheet) { // Write table header $html .= $this->generateTableHeader($sheet); + $this->sheetCharts = []; + $this->sheetDrawings = []; // Get worksheet dimension [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); [$minCol, $minRow] = Coordinate::indexesFromString($min); [$maxCol, $maxRow] = Coordinate::indexesFromString($max); + $this->extendRowsAndColumns($sheet, $maxCol, $maxRow); [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); @@ -510,8 +520,6 @@ public function generateSheetData(): string $html .= $endTag; } - --$row; - $html .= $this->extendRowsForChartsAndImages($sheet, $row); // Write table footer $html .= $this->generateTableFooter(); @@ -563,78 +571,33 @@ public function generateNavigation(): string return $html; } - /** - * Extend Row if chart is placed after nominal end of row. - * This code should be exercised by sample: - * Chart/32_Chart_read_write_PDF.php. - * - * @param int $row Row to check for charts - */ - private function extendRowsForCharts(Worksheet $worksheet, int $row): array + private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void { - $rowMax = $row; - $colMax = 'A'; - $anyfound = false; if ($this->includeCharts) { foreach ($worksheet->getChartCollection() as $chart) { if ($chart instanceof Chart) { - $anyfound = true; $chartCoordinates = $chart->getTopLeftPosition(); - $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']); - $chartCol = Coordinate::columnIndexFromString($chartTL[0]); + $this->sheetCharts[$chartCoordinates['cell']] = $chart; + $chartTL = Coordinate::indexesFromString($chartCoordinates['cell']); if ($chartTL[1] > $rowMax) { $rowMax = $chartTL[1]; } - if ($chartCol > Coordinate::columnIndexFromString($colMax)) { + if ($chartTL[0] > $colMax) { $colMax = $chartTL[0]; } } } } - - return [$rowMax, $colMax, $anyfound]; - } - - private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string - { - [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row); - foreach ($worksheet->getDrawingCollection() as $drawing) { - $anyfound = true; - $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates()); - $imageCol = Coordinate::columnIndexFromString($imageTL[0]); + $imageTL = Coordinate::indexesFromString($drawing->getCoordinates()); + $this->sheetDrawings[$drawing->getCoordinates()] = $drawing; if ($imageTL[1] > $rowMax) { $rowMax = $imageTL[1]; } - if ($imageCol > Coordinate::columnIndexFromString($colMax)) { + if ($imageTL[0] > $colMax) { $colMax = $imageTL[0]; } } - - // Don't extend rows if not needed - if ($row === $rowMax || !$anyfound) { - return ''; - } - - $html = ''; - ++$colMax; - ++$row; - while ($row <= $rowMax) { - $html .= ''; - for ($col = 'A'; $col != $colMax; ++$col) { - $htmlx = $this->writeImageInCell($worksheet, $col . $row); - $htmlx .= $this->includeCharts ? $this->writeChartInCell($worksheet, $col . $row) : ''; - if ($htmlx) { - $html .= "$htmlx"; - } else { - $html .= ""; - } - } - ++$row; - $html .= '' . PHP_EOL; - } - - return $html; } /** @@ -658,19 +621,16 @@ public static function winFileToUrl($filename, bool $mpdf = false) /** * Generate image tag in cell. * - * @param Worksheet $worksheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet * @param string $coordinates Cell coordinates */ - private function writeImageInCell(Worksheet $worksheet, string $coordinates): string + private function writeImageInCell(string $coordinates): string { // Construct HTML $html = ''; // Write images - foreach ($worksheet->getDrawingCollection() as $drawing) { - if ($drawing->getCoordinates() != $coordinates) { - continue; - } + $drawing = $this->sheetDrawings[$coordinates] ?? null; + if ($drawing !== null) { $filedesc = $drawing->getDescription(); $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image'; if ($drawing instanceof Drawing) { @@ -744,37 +704,47 @@ private function writeChartInCell(Worksheet $worksheet, string $coordinates): st $html = ''; // Write charts - foreach ($worksheet->getChartCollection() as $chart) { - if ($chart instanceof Chart) { - $chartCoordinates = $chart->getTopLeftPosition(); - if ($chartCoordinates['cell'] == $coordinates) { - $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; - $renderedWidth = $chart->getRenderedWidth(); - $renderedHeight = $chart->getRenderedHeight(); - if ($renderedWidth === null || $renderedHeight === null) { - $this->adjustRendererPositions($chart, $worksheet); - } - $renderSuccessful = $chart->render($chartFileName); - $chart->setRenderedWidth($renderedWidth); - $chart->setRenderedHeight($renderedHeight); - if (!$renderSuccessful) { - return ''; - } + $chart = $this->sheetCharts[$coordinates] ?? null; + if ($chart !== null) { + $chartCoordinates = $chart->getTopLeftPosition(); + $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; + $renderedWidth = $chart->getRenderedWidth(); + $renderedHeight = $chart->getRenderedHeight(); + if ($renderedWidth === null || $renderedHeight === null) { + $this->adjustRendererPositions($chart, $worksheet); + } + $title = $chart->getTitle(); + $caption = null; + $filedesc = ''; + if ($title !== null) { + $calculatedTitle = $title->getCalculatedTitle($worksheet->getParent()); + if ($calculatedTitle !== null) { + $caption = $title->getCaption(); + $title->setCaption($calculatedTitle); + } + $filedesc = $title->getCaptionText($worksheet->getParent()); + } + $renderSuccessful = $chart->render($chartFileName); + $chart->setRenderedWidth($renderedWidth); + $chart->setRenderedHeight($renderedHeight); + if (isset($title, $caption)) { + $title->setCaption($caption); + } + if (!$renderSuccessful) { + return ''; + } - $html .= PHP_EOL; - $imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => '']; - $filedesc = $chart->getTitle(); - $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; - $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; - $picture = file_get_contents($chartFileName); - if ($picture !== false) { - $base64 = base64_encode($picture); - $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + $html .= PHP_EOL; + $imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => '']; - $html .= '' . $filedesc . '' . PHP_EOL; - } - unlink($chartFileName); - } + $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; + $picture = file_get_contents($chartFileName); + unlink($chartFileName); + if ($picture !== false) { + $base64 = base64_encode($picture); + $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + + $html .= '' . $filedesc . '' . PHP_EOL; } } @@ -1444,7 +1414,7 @@ private function generateRowSpans(string $html, int $rowSpan, int $colSpan): str private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void { // Image? - $htmlx = $this->writeImageInCell($worksheet, $coordinate); + $htmlx = $this->writeImageInCell($coordinate); // Chart? $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); // Column start diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index f27156507a..a3f27d1b93 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -99,9 +99,11 @@ public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, mixed $ $objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly()); $objWriter->endElement(); - $objWriter->startElement('c:dispBlanksAs'); - $objWriter->writeAttribute('val', $chart->getDisplayBlanksAs()); - $objWriter->endElement(); + if ($chart->getDisplayBlanksAs() !== '') { + $objWriter->startElement('c:dispBlanksAs'); + $objWriter->writeAttribute('val', $chart->getDisplayBlanksAs()); + $objWriter->endElement(); + } $objWriter->startElement('c:showDLblsOverMax'); $objWriter->writeAttribute('val', '0'); @@ -151,6 +153,9 @@ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void if ($title === null) { return; } + if ($this->writeCalculatedTitle($objWriter, $title)) { + return; + } $objWriter->startElement('c:title'); $objWriter->startElement('c:tx'); @@ -187,6 +192,60 @@ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void $objWriter->endElement(); } + /** + * Write Calculated Chart Title. + */ + private function writeCalculatedTitle(XMLWriter $objWriter, Title $title): bool + { + $calc = $title->getCalculatedTitle($this->getParentWriter()->getSpreadsheet()); + if (empty($calc)) { + return false; + } + + $objWriter->startElement('c:title'); + $objWriter->startElement('c:tx'); + $objWriter->startElement('c:strRef'); + $objWriter->writeElement('c:f', $title->getCellReference()); + $objWriter->startElement('c:strCache'); + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', '1'); + $objWriter->endElement(); // c:ptCount + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', '0'); + $objWriter->writeElement('c:v', $calc); + $objWriter->endElement(); // c:pt + + $objWriter->endElement(); // c:strCache + $objWriter->endElement(); // c:strRef + $objWriter->endElement(); // c:tx + + $this->writeLayout($objWriter, $title->getLayout()); + + $objWriter->startElement('c:overlay'); + $objWriter->writeAttribute('val', ($title->getOverlay()) ? '1' : '0'); + $objWriter->endElement(); // c:overlay + // c:spPr + + // c:txPr + $labelFont = $title->getFont(); + if ($labelFont !== null) { + $objWriter->startElement('c:txPr'); + + $objWriter->startElement('a:bodyPr'); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $this->writeLabelFont($objWriter, $labelFont, null); + + $objWriter->endElement(); // c:txPr + } + + $objWriter->endElement(); // c:title + + return true; + } + /** * Write Chart Legend. */ @@ -1804,6 +1863,10 @@ private function writeLabelFont(XMLWriter $objWriter, ?Font $labelFont, ?Propert if ($labelFont->getItalic() === true) { $objWriter->writeAttribute('i', '1'); } + $cap = $labelFont->getCap(); + if ($cap !== null) { + $objWriter->writeAttribute('cap', $cap); + } $fontColor = $labelFont->getChartColor(); if ($fontColor !== null) { $this->writeColor($objWriter, $fontColor); diff --git a/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php b/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php new file mode 100644 index 0000000000..2546d60de6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php @@ -0,0 +1,141 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testDynamicTitle(): void + { + // based on samples/templates/issue.3797.2007.xlsx + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Only Sheet'); + $sheet->fromArray( + [ + ['Some Title'], + [], + [null, null, 'Data'], + [null, 'L1', 1.3], + [null, 'L2', 1.3], + [null, 'L3', 2.3], + [null, 'L4', 1.6], + [null, 'L5', 1.5], + [null, 'L6', 1.4], + [null, 'L7', 2.2], + [null, 'L8', 1.8], + [null, 'L9', 1.1], + [null, 'L10', 1.8], + [null, 'L11', 1.6], + [null, 'L12', 2.7], + [null, 'L13', 2.2], + [null, 'L14', 1.3], + ] + ); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4', null, 1), // 2010 + ]; + // Set the X-Axis Labels + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4:$B$17'), + ]; + // Set the Data values for each data series we want to plot + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, '\'Only Sheet\'!$C$4:$C$17'), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + DataSeries::GROUPING_STANDARD, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + $title = new Title(); + $title->setCellReference('\'Only Sheet\'!$A$1'); + $font = new Font(); + $font->setCap(Font::CAP_ALL); + $title->setFont($font); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + null, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null, // yAxisLabel + null, // xAxis + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('G7'); + $chart->setBottomRightPosition('N21'); + // Add the chart to the worksheet + $sheet->addChart($chart); + $sheet->setSelectedCells('D1'); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $rsheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $original = $chart2->getTitle()?->getCalculatedTitle($reloadedSpreadsheet); + self::assertSame('Some Title', $original); + $rsheet->getCell('A1')->setValue('Changed Title'); + self::assertNotNull($chart2->getTitle()); + self::assertSame('Changed Title', $chart2->getTitle()->getCalculatedTitle($reloadedSpreadsheet)); + self::assertSame(Font::CAP_ALL, $chart2->getTitle()->getFont()?->getCap()); + + $writer = new Html($reloadedSpreadsheet); + Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); + $writer->setIncludeCharts(true); + $content = $writer->generateHtmlAll(); + self::assertStringContainsString('alt="Changed Title"', $content); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} From 9fcfa4b7ec773f3868951ad47ce6e17c05c6dd0d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:46:43 -0800 Subject: [PATCH 15/20] Xlsx Support Flipping of Image (#3801) * Xlsx Support Flipping of Image Fix #731. Opened over 5 years ago, probably the second oldest problem I've worked on. Images attached to an Xlsx spreadsheet can be rotated, which is supported by PhpSpreadsheet. They can also be flipped along their horizontal and/or vertical axes, and that has not been supported. This PR adds that support. * Update CHANGELOG.md --- CHANGELOG.md | 2 + src/PhpSpreadsheet/Reader/Xlsx.php | 4 ++ src/PhpSpreadsheet/Worksheet/BaseDrawing.php | 28 +++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Drawing.php | 9 +++++ .../Reader/Xlsx/Issue731Test.php | 38 ++++++++++++++++++ tests/data/Reader/XLSX/issue.731.xlsx | Bin 0 -> 28509 bytes 6 files changed, 81 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue731Test.php create mode 100644 tests/data/Reader/XLSX/issue.731.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 274981073e..bac167f9b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Writer ODS : Write Border Style for cells [Issue #3690](https://github.com/PHPOffice/PhpSpreadsheet/issues/3690) [PR #3693](https://github.com/PHPOffice/PhpSpreadsheet/pull/3693) - Sheet Background Images [Issue #1649](https://github.com/PHPOffice/PhpSpreadsheet/issues/1649) [PR #3795](https://github.com/PHPOffice/PhpSpreadsheet/pull/3795) - Check if Coordinate is Inside Range [PR #3779](https://github.com/PHPOffice/PhpSpreadsheet/pull/3779) +- Flipping Images [Issue #731](https://github.com/PHPOffice/PhpSpreadsheet/issues/731) [PR #3801](https://github.com/PHPOffice/PhpSpreadsheet/pull/3801) +- Chart Dynamic Title and Font Properties [Issue #3797](https://github.com/PHPOffice/PhpSpreadsheet/issues/3797) [PR #3800](https://github.com/PHPOffice/PhpSpreadsheet/pull/3800) ### Changed diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 73ac43afd8..109a593edf 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1424,6 +1424,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy'))); if ($xfrm) { $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); + $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); } if ($outerShdw) { $shadow = $objDrawing->getShadow(); @@ -1518,6 +1520,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cx'))); $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cy'))); $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); + $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); } if ($outerShdw) { $shadow = $objDrawing->getShadow(); diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 2310e64ea2..d59d4b39ae 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -131,6 +131,10 @@ class BaseDrawing implements IComparable */ protected $rotation = 0; + protected bool $flipVertical = false; + + protected bool $flipHorizontal = false; + /** * Shadow. */ @@ -542,4 +546,28 @@ public function setSrcRect($srcRect): self return $this; } + + public function setFlipHorizontal(bool $flipHorizontal): self + { + $this->flipHorizontal = $flipHorizontal; + + return $this; + } + + public function getFlipHorizontal(): bool + { + return $this->flipHorizontal; + } + + public function setFlipVertical(bool $flipVertical): self + { + $this->flipVertical = $flipVertical; + + return $this; + } + + public function getFlipVertical(): bool + { + return $this->flipVertical; + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 6763b508f2..cb8b1c4038 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -294,6 +294,8 @@ public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relati // a:xfrm $objWriter->startElement('a:xfrm'); $objWriter->writeAttribute('rot', (string) SharedDrawing::degreesToAngle($drawing->getRotation())); + self::writeAttributeIf($objWriter, $drawing->getFlipVertical(), 'flipV', '1'); + self::writeAttributeIf($objWriter, $drawing->getFlipHorizontal(), 'flipH', '1'); if ($isTwoCellAnchor) { $objWriter->startElement('a:ext'); $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); @@ -579,4 +581,11 @@ private static function stringEmu(int $pixelValue): string { return (string) SharedDrawing::pixelsToEMU($pixelValue); } + + private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void + { + if ($condition) { + $objWriter->writeAttribute($attr, $val); + } + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue731Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue731Test.php new file mode 100644 index 0000000000..929320c55c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue731Test.php @@ -0,0 +1,38 @@ +load(self::$testbook); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $reloadedSheet = $reloadedSpreadsheet->getActiveSheet(); + $expected = [ + [0, false, false], + [90, false, false], + [270, false, false], + [0, false, true], + [0, true, false], + [20, false, false], + [20, false, true], + [0, true, true], + ]; + $actual = []; + foreach ($reloadedSheet->getDrawingCollection() as $drawing) { + $actual[] = [$drawing->getRotation(), $drawing->getFlipHorizontal(), $drawing->getFlipVertical()]; + } + self::assertSame($expected, $actual); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.731.xlsx b/tests/data/Reader/XLSX/issue.731.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..91f865d4aa1ada4a8ed415c8ebe19fa69d882810 GIT binary patch literal 28509 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 z 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 o)9>oPPgs9dtDF5rZTRo$OHmg7X&n7gL Date: Fri, 1 Dec 2023 07:26:36 -0800 Subject: [PATCH 16/20] Bump phpunit/phpunit from 9.6.13 to 9.6.14 (#3806) Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 9.6.13 to 9.6.14. - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/9.6.14/ChangeLog-9.6.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/9.6.13...9.6.14) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index a3d742130f..1560eafc97 100644 --- a/composer.lock +++ b/composer.lock @@ -2804,16 +2804,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "9.6.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "43653e6ad7adc22e7b667dd561bf8fcb74c10cf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43653e6ad7adc22e7b667dd561bf8fcb74c10cf0", + "reference": "43653e6ad7adc22e7b667dd561bf8fcb74c10cf0", "shasum": "" }, "require": { @@ -2887,7 +2887,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.14" }, "funding": [ { @@ -2903,7 +2903,7 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2023-12-01T06:10:48+00:00" }, { "name": "psr/container", @@ -5403,5 +5403,5 @@ "platform-overrides": { "php": "8.0.99" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From df3c6d91221c95032daf6ce1f377ea846d8a5bf6 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:58:53 -0800 Subject: [PATCH 17/20] Prevent Loop in Shared/File::realpath (#3809) Fix #3807. Function attempts to rationalize `..` in filenames in a way that normally works just fine. Reporter notes that at least one of the filenames that will be analyzed when a spreadsheet is read can be maliciously altered in a manner which does not harm Excel when reading the file, but which puts PhpSpreadsheet into a loop. This PR fixes the problem. --- src/PhpSpreadsheet/Shared/File.php | 6 +-- .../Reader/Xlsx/Issue3807Test.php | 36 ++++++++++++++++++ tests/data/Reader/XLSX/issue.3807.xlsx | Bin 0 -> 8952 bytes 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3807Test.php create mode 100644 tests/data/Reader/XLSX/issue.3807.xlsx diff --git a/src/PhpSpreadsheet/Shared/File.php b/src/PhpSpreadsheet/Shared/File.php index 737a6eb591..e5e470e3bc 100644 --- a/src/PhpSpreadsheet/Shared/File.php +++ b/src/PhpSpreadsheet/Shared/File.php @@ -94,9 +94,9 @@ public static function realpath(string $filename): string $pathArray = explode('/', $filename); while (in_array('..', $pathArray) && $pathArray[0] != '..') { $iMax = count($pathArray); - for ($i = 0; $i < $iMax; ++$i) { - if ($pathArray[$i] == '..' && $i > 0) { - unset($pathArray[$i], $pathArray[$i - 1]); + for ($i = 1; $i < $iMax; ++$i) { + if ($pathArray[$i] == '..') { + array_splice($pathArray, $i - 1, 2); break; } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3807Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3807Test.php new file mode 100644 index 0000000000..db98262ebe --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3807Test.php @@ -0,0 +1,36 @@ +load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + // Assert anything to confirm read succeeded + self::assertSame(1, $sheet->getCell('B1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.3807.xlsx b/tests/data/Reader/XLSX/issue.3807.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..54772a628dba34806e5ca391c657e0fb12354039 GIT binary patch literal 8952 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 Date: Tue, 5 Dec 2023 21:10:02 -0800 Subject: [PATCH 18/20] Performance Improvement for Xlsx Reader (#3810) * Performance Improvement for Xlsx Reader Fix #3683. PR #3497 fixed a problem involving formulas and the quotePrefix style attribute. It did so by automatically turning off quotePrefix for any formulas encountered by Xlsx Reader. Under the right circumstances, it turns out that that change can cause a file read to take noticeably more time than previously. This change will turn off quotePrefix only if it is already on, and that appears to eliminate the performance problem while continuing to solve the original problem. * Very Minor Improvement * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ src/PhpSpreadsheet/Reader/Xlsx.php | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bac167f9b7..76f9746d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Slk Shared Formulas. [Issue #2267](https://github.com/PHPOffice/PhpSpreadsheet/issues/2267) [PR #3776](https://github.com/PHPOffice/PhpSpreadsheet/pull/3776) - Html omitting some charts. [Issue #3767](https://github.com/PHPOffice/PhpSpreadsheet/issues/3767) [PR #3771](https://github.com/PHPOffice/PhpSpreadsheet/pull/3771) - Case Insensitive Comparison for Sheet Names [PR #3791](https://github.com/PHPOffice/PhpSpreadsheet/pull/3791) +- Performance improvement for Xlsx Reader. [Issue #3683](https://github.com/PHPOffice/PhpSpreadsheet/issues/3683) [PR #3810](https://github.com/PHPOffice/PhpSpreadsheet/pull/3810) +- Prevent loop in Shared/File. [Issue #3807](https://github.com/PHPOffice/PhpSpreadsheet/issues/3807) [PR #3809](https://github.com/PHPOffice/PhpSpreadsheet/pull/3809) ## 1.29.0 - 2023-06-15 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 109a593edf..d9b60146e7 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -937,10 +937,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $holdSelected = $docSheet->getSelectedCells(); $cAttrS = (int) ($cAttr['s'] ?? 0); // no style index means 0, it seems - $cell->setXfIndex(isset($styles[$cAttrS]) - ? $cAttrS : 0); + $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0; + $cell->setXfIndex($cAttrS); // issue 3495 - if ($cell->getDataType() === DataType::TYPE_FORMULA) { + if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { $cell->getStyle()->setQuotePrefix(false); } $docSheet->setSelectedCells($holdSelected); From 5bcbc7db0c00c1351d4f2a9625e2e6f6240a58a9 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 6 Dec 2023 07:08:14 -0800 Subject: [PATCH 19/20] WIP Avoid a PHP8.4 Deprecation (#3789) Fix #3782. A signature of the ReflectionMethod constructor will be deprecated, and PhpSpreadsheet runs afoul of that change in one place. Php8.4 is not yet available in any form, and I am reluctant to make this change until we see that the issue is real (and that this PR fixes it), so am leaving this PR in draft status till then. I note that one Excel function `PI` is implemented not as a class method in PhpSpreadsheet, but rather as a call to the native Php function `pi`. The ReflectionMethod call is subject to a TypeError in the changed code, but that is already the case. We haven't seen a TypeError because (a) it will arise only if the caller supplies an argument to the function (which must be called with zero arguments), and (b) there are no test cases for that function. The code is slightly cleaned up, and test cases are now added. This is not an important enough problem to rush this PR - the existing code (and the changed code), rather than failing with TypeError, will fail with a CalculationException (wrong number of arguments) before it gets to the TypeError; that is the correct behavior. --- .../Calculation/Calculation.php | 4 +-- .../Calculation/Functions/MathTrig/PiTest.php | 32 +++++++++++++++++++ tests/data/Calculation/MathTrig/PI.php | 8 +++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PiTest.php create mode 100644 tests/data/Calculation/MathTrig/PI.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 34062a5319..960db939c2 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -5072,7 +5072,7 @@ private function processTokenStack(mixed $tokens, $cellID = null, ?Cell $cell = krsort($args); krsort($emptyArguments); - if ($argCount > 0) { + if ($argCount > 0 && is_array($functionCall)) { $args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments); } @@ -5571,7 +5571,7 @@ public function getImplementedFunctionNames(): array private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array { - $reflector = new ReflectionMethod(implode('::', $functionCall)); + $reflector = new ReflectionMethod($functionCall[0], $functionCall[1]); $methodArguments = $reflector->getParameters(); if (count($methodArguments) > 0) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PiTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PiTest.php new file mode 100644 index 0000000000..130f02f980 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PiTest.php @@ -0,0 +1,32 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + if ($number !== null) { + $sheet->getCell('A1')->setValue($number); + } + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=PI()'); + } else { + $sheet->getCell('B1')->setValue('=PI(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public static function providerPI(): array + { + return require 'tests/data/Calculation/MathTrig/PI.php'; + } +} diff --git a/tests/data/Calculation/MathTrig/PI.php b/tests/data/Calculation/MathTrig/PI.php new file mode 100644 index 0000000000..317cd1786c --- /dev/null +++ b/tests/data/Calculation/MathTrig/PI.php @@ -0,0 +1,8 @@ + ['exception', 1], +]; From 29c0162e2aaa79d4227a556ae094b494f4718512 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 6 Dec 2023 09:40:27 -0800 Subject: [PATCH 20/20] Let Phpstan Run on Samples (#3808) * Let Phpstan Run on Samples Phpstan currently analyzes all source and test members. We already run phpcs and php-cs-fixer on samples as well. I would expect that samples are often used as templates for code in userland; it behooves us to be at least as careful with those members as for the others which are already being analyzed. Aside from 1300+ messages `Variable $helper might not be defined.`, which will be suppressed in phpstan.neon.dist, there are really only a few changes needed for sample members, so that part of the code base was already in good shape, and is now even better. No annotations were needed. * Scrutinizer 2 out of 3 1 false positive, now suppressed; fix other 2. * Remove Dead Code * Very Minor Changes * Add infra --- .phpcs.xml.dist | 1 + infra/DocumentGenerator.php | 2 +- infra/LocaleGenerator.php | 35 ++++--- phpstan.neon.dist | 3 + .../Autofilter/10_Autofilter_selection_2.php | 2 +- samples/Basic/02_Types.php | 8 +- samples/Basic/19_Namedrange.php | 3 + samples/Basic/25_In_memory_image.php | 7 +- samples/Basic/39_Dropdown.php | 7 +- .../Engineering/Convert-Online.php | 1 + samples/Chart/32_Chart_read_write.php | 10 +- samples/Chart/32_Chart_read_write_HTML.php | 12 +-- samples/Chart/32_Chart_read_write_PDF.php | 12 +-- .../Chart/33_Chart_create_line_dateaxis.php | 16 ++- .../33_Chart_create_scatter5_trendlines.php | 2 +- samples/Chart/35_Chart_render.php | 8 +- samples/Chart/35_Chart_render33.php | 10 +- samples/Chart/37_Chart_dynamic_title.php | 8 +- .../CrossWorksheetNamedFormula.php | 2 +- samples/DefinedNames/ScopedNamedRange.php | 12 ++- samples/Pdf/21b_Pdf.php | 2 +- samples/Pdf/21c_Pdf.php | 4 +- samples/Pdf/21e_UnusualFont_mpdf.php | 2 +- samples/Pdf/{mpdf2.inc => Mpdf2.php} | 6 +- ...eader_loading_several_named_worksheets.php | 8 +- ...eader_using_a_configurable_read_filter.php | 8 +- ...a_configurable_read_filter_(version_1).php | 9 +- ...a_configurable_read_filter_(version_2).php | 9 +- ...ks_to_split_across_multiple_worksheets.php | 13 +-- .../20_Reader_worksheet_hyperlink_image.php | 7 +- samples/Wizards/NumberFormat/Accounting.php | 4 +- samples/Wizards/NumberFormat/Currency.php | 4 +- samples/Wizards/NumberFormat/Number.php | 4 +- samples/Wizards/NumberFormat/Percentage.php | 4 +- samples/Wizards/NumberFormat/Scientific.php | 4 +- samples/templates/sampleSpreadsheet.php | 12 +-- samples/templates/sampleSpreadsheet2.php | 12 +-- src/PhpSpreadsheet/Cell/DataValidation.php | 8 +- src/PhpSpreadsheet/Chart/Axis.php | 14 +-- src/PhpSpreadsheet/Chart/Chart.php | 10 ++ src/PhpSpreadsheet/Chart/DataSeries.php | 4 +- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 2 +- src/PhpSpreadsheet/Chart/Properties.php | 8 +- src/PhpSpreadsheet/RichText/Run.php | 10 ++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 10 ++ .../Chart/ChartsByNameTest.php | 97 +++++++++++++++++++ .../Chart/PlotAreaTest.php | 48 +++++++++ tests/PhpSpreadsheetTests/RichTextTest.php | 17 ++++ 48 files changed, 367 insertions(+), 134 deletions(-) rename samples/Pdf/{mpdf2.inc => Mpdf2.php} (95%) create mode 100644 tests/PhpSpreadsheetTests/Chart/ChartsByNameTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/PlotAreaTest.php diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 3eafb6ca19..ba381a28f7 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -5,6 +5,7 @@ samples src tests + infra samples/Header.php */tests/Core/*/*Test\.(inc|css|js)$ diff --git a/infra/DocumentGenerator.php b/infra/DocumentGenerator.php index e2c3c86cb9..5a86d17784 100644 --- a/infra/DocumentGenerator.php +++ b/infra/DocumentGenerator.php @@ -52,7 +52,7 @@ private static function tableRow(array $lengths, ?array $values = null): string return rtrim($result, ' '); } - private static function getPhpSpreadsheetFunctionText($functionCall): string + private static function getPhpSpreadsheetFunctionText(mixed $functionCall): string { if (is_string($functionCall)) { return $functionCall; diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index 5e570f7f0f..508a6480e3 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -38,32 +38,32 @@ class LocaleGenerator */ protected $translationBaseFolder; - protected $phpSpreadsheetFunctions; + protected array $phpSpreadsheetFunctions; /** * @var Spreadsheet */ protected $translationSpreadsheet; - protected $verbose; + protected bool $verbose; /** * @var Worksheet */ protected $localeTranslations; - protected $localeLanguageMap = []; + protected array $localeLanguageMap = []; - protected $errorCodeMap = []; + protected array $errorCodeMap = []; /** * @var Worksheet */ private $functionNameTranslations; - protected $functionNameLanguageMap = []; + protected array $functionNameLanguageMap = []; - protected $functionNameMap = []; + protected array $functionNameMap = []; public function __construct( string $translationBaseFolder, @@ -98,7 +98,7 @@ public function generateLocales(): void } } - protected function buildConfigFileForLocale($column, $locale): void + protected function buildConfigFileForLocale(string $column, string $locale): void { $language = $this->localeTranslations->getCell($column . self::ENGLISH_LANGUAGE_NAME_ROW)->getValue(); $localeLanguage = $this->localeTranslations->getCell($column . self::LOCALE_LANGUAGE_NAME_ROW)->getValue(); @@ -124,7 +124,8 @@ protected function buildConfigFileForLocale($column, $locale): void fclose($configFile); } - protected function writeConfigArgumentSeparator($configFile, $column): void + /** @param resource $configFile resource to write to */ + protected function writeConfigArgumentSeparator($configFile, string $column): void { $translationCell = $this->localeTranslations->getCell($column . self::ARGUMENT_SEPARATOR_ROW); $localeValue = $translationCell->getValue(); @@ -136,7 +137,8 @@ protected function writeConfigArgumentSeparator($configFile, $column): void } } - protected function writeConfigCurrencySymbol($configFile, $column): void + /** @param resource $configFile resource to write to */ + protected function writeConfigCurrencySymbol($configFile, string $column): void { $translationCell = $this->localeTranslations->getCell($column . self::CURRENCY_SYMBOL_ROW); $localeValue = $translationCell->getValue(); @@ -151,7 +153,7 @@ protected function writeConfigCurrencySymbol($configFile, $column): void } } - protected function buildFunctionsFileForLocale($column, $locale): void + protected function buildFunctionsFileForLocale(string $column, string $locale): void { $language = $this->functionNameTranslations->getCell($column . self::ENGLISH_LANGUAGE_NAME_ROW)->getValue(); $localeLanguage = $this->functionNameTranslations->getCell($column . self::LOCALE_LANGUAGE_NAME_ROW) @@ -176,6 +178,7 @@ protected function buildFunctionsFileForLocale($column, $locale): void fclose($functionFile); } + /** @return resource used by other methods in this class */ protected function openConfigFile(string $locale, string $language, string $localeLanguage) { $this->log("Building locale {$locale} ($language) configuration"); @@ -185,11 +188,15 @@ protected function openConfigFile(string $locale, string $language, string $loca $this->log("Writing locale configuration to {$configFileName}"); $configFile = fopen($configFileName, 'wb'); + if ($configFile === false) { + throw new Exception('Unable to open $configFileName for write'); + } $this->writeFileHeader($configFile, $localeLanguage, $language, 'locale settings'); return $configFile; } + /** @return resource used by other methods in this class */ protected function openFunctionNameFile(string $locale, string $language, string $localeLanguage) { $this->log("Building locale {$locale} ($language) function names"); @@ -199,6 +206,9 @@ protected function openFunctionNameFile(string $locale, string $language, string $this->log("Writing local function names to {$functionFileName}"); $functionFile = fopen($functionFileName, 'wb'); + if ($functionFile === false) { + throw new Exception('Unable to open $functionFileName for write'); + } $this->writeFileHeader($functionFile, $localeLanguage, $language, 'function name translations'); return $functionFile; @@ -218,6 +228,7 @@ protected function getLocaleFolder(string $locale): string return $localeFolder; } + /** @param resource $localeFile file being written to */ protected function writeFileHeader($localeFile, string $localeLanguage, string $language, string $title): void { fwrite($localeFile, str_repeat('#', 60) . self::EOL); @@ -229,6 +240,7 @@ protected function writeFileHeader($localeFile, string $localeLanguage, string $ fwrite($localeFile, str_repeat('#', 60) . self::EOL . self::EOL); } + /** @param resource $localeFile file being written to */ protected function writeFileSectionHeader($localeFile, string $header): void { fwrite($localeFile, self::EOL . '##' . self::EOL); @@ -245,9 +257,6 @@ protected function openTranslationWorkbook(): void protected function getTranslationSheet(string $sheetName): Worksheet { $worksheet = $this->translationSpreadsheet->setActiveSheetIndexByName($sheetName); - if ($worksheet === null) { - throw new Exception("{$sheetName} Worksheet not found"); - } return $worksheet; } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f6ce123adc..154541708c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,6 +8,8 @@ parameters: paths: - src/ - tests/ + - samples/ + - infra/ excludePaths: - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -21,3 +23,4 @@ parameters: ignoreErrors: # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' + - '~^Variable \$helper might not be defined\.$~' diff --git a/samples/Autofilter/10_Autofilter_selection_2.php b/samples/Autofilter/10_Autofilter_selection_2.php index 4d5d891cff..6b82bcdf52 100644 --- a/samples/Autofilter/10_Autofilter_selection_2.php +++ b/samples/Autofilter/10_Autofilter_selection_2.php @@ -126,7 +126,7 @@ $autoFilter->getColumn('D') ->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER) ->createRule() - ->setRule(Rule::AUTOFILTER_COLUMN_RULE_EQUAL, null, Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE) + ->setRule(Rule::AUTOFILTER_COLUMN_RULE_EQUAL, '', Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE) ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); $helper->log('Add filter on the Date (Column D) to display year to date'); diff --git a/samples/Basic/02_Types.php b/samples/Basic/02_Types.php index 4eaaa8b15f..d28af579d2 100644 --- a/samples/Basic/02_Types.php +++ b/samples/Basic/02_Types.php @@ -109,9 +109,9 @@ $richText->createText('你好 '); $payable = $richText->createTextRun('你 好 吗?'); -$payable->getFont()->setBold(true); -$payable->getFont()->setItalic(true); -$payable->getFont()->setColor(new Color(Color::COLOR_DARKGREEN)); +$payable->getFontOrThrow()->setBold(true); +$payable->getFontOrThrow()->setItalic(true); +$payable->getFontOrThrow()->setColor(new Color(Color::COLOR_DARKGREEN)); $richText->createText(', unless specified otherwise on the invoice.'); @@ -123,7 +123,7 @@ $richText2->createText("black text\n"); $red = $richText2->createTextRun('red text'); -$red->getFont()->setColor(new Color(Color::COLOR_RED)); +$red->getFontOrThrow()->setColor(new Color(Color::COLOR_RED)); $spreadsheet->getActiveSheet() ->getCell('C14') diff --git a/samples/Basic/19_Namedrange.php b/samples/Basic/19_Namedrange.php index 6170fa28e7..5b1a252708 100644 --- a/samples/Basic/19_Namedrange.php +++ b/samples/Basic/19_Namedrange.php @@ -36,6 +36,9 @@ // Rename named ranges $helper->log('Rename named ranges'); +if ($spreadsheet->getNamedRange('PersonName') === null) { + throw new Exception('named range not found'); +} $spreadsheet->getNamedRange('PersonName')->setName('PersonFN'); // Rename worksheet diff --git a/samples/Basic/25_In_memory_image.php b/samples/Basic/25_In_memory_image.php index 1833e683ae..7ba4871b3a 100644 --- a/samples/Basic/25_In_memory_image.php +++ b/samples/Basic/25_In_memory_image.php @@ -27,12 +27,15 @@ // Generate an image $helper->log('Generate an image'); -$gdImage = @imagecreatetruecolor(120, 20); +$gdImage = imagecreatetruecolor(120, 20); if (!$gdImage) { - exit('Cannot Initialize new GD image stream'); + throw new Exception('Cannot Initialize new GD image stream'); } $textColor = imagecolorallocate($gdImage, 255, 255, 255); +if ($textColor === false) { + throw new Exception('imagecolorallocate failed'); +} imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); // Add a drawing to the worksheet diff --git a/samples/Basic/39_Dropdown.php b/samples/Basic/39_Dropdown.php index 0e8ff52b6e..232ed0eda9 100644 --- a/samples/Basic/39_Dropdown.php +++ b/samples/Basic/39_Dropdown.php @@ -20,7 +20,8 @@ ->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.') ->setKeywords('Office PhpSpreadsheet php') ->setCategory('Test result file'); -function transpose($value) + +function transpose(string $value): array { return [$value]; } @@ -30,12 +31,12 @@ function transpose($value) $column = 'F'; // Set data for dropdowns -$continents = glob(__DIR__ . '/data/continents/*'); +$continents = glob(__DIR__ . '/data/continents/*') ?: []; foreach ($continents as $key => $filename) { $continent = pathinfo($filename, PATHINFO_FILENAME); $helper->log("Loading $continent"); $continent = str_replace(' ', '_', $continent); - $countries = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $countries = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $countryCount = count($countries); // Transpose $countries from a row to a column array diff --git a/samples/Calculations/Engineering/Convert-Online.php b/samples/Calculations/Engineering/Convert-Online.php index e20e4c79b6..3d26a3f6ba 100644 --- a/samples/Calculations/Engineering/Convert-Online.php +++ b/samples/Calculations/Engineering/Convert-Online.php @@ -77,6 +77,7 @@ $quantity = $_POST['quantity']; $fromUnit = $_POST['fromUnit']; $toUnit = $_POST['toUnit']; + /** @var float|string */ $result = ConvertUOM::CONVERT($quantity, $fromUnit, $toUnit); echo "{$quantity} {$units[$_POST['category']][$fromUnit]} is {$result} {$units[$_POST['category']][$toUnit]}", PHP_EOL; diff --git a/samples/Chart/32_Chart_read_write.php b/samples/Chart/32_Chart_read_write.php index 42944c0ccb..900c9567c2 100644 --- a/samples/Chart/32_Chart_read_write.php +++ b/samples/Chart/32_Chart_read_write.php @@ -13,7 +13,7 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -40,7 +40,7 @@ } else { natsort($chartNames); foreach ($chartNames as $i => $chartName) { - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { @@ -48,15 +48,15 @@ } $helper->log(' ' . $chartName . ' - ' . $caption); $indentation = str_repeat(' ', strlen($chartName) + 3); - $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); if ($groupCount == 1) { - $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); $helper->log($indentation . ' ' . $chartType); $helper->renderChart($chart, __FILE__); } else { $chartTypes = []; for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); } $chartTypes = array_unique($chartTypes); if (count($chartTypes) == 1) { diff --git a/samples/Chart/32_Chart_read_write_HTML.php b/samples/Chart/32_Chart_read_write_HTML.php index 3cb377873c..0dc5484ce5 100644 --- a/samples/Chart/32_Chart_read_write_HTML.php +++ b/samples/Chart/32_Chart_read_write_HTML.php @@ -18,7 +18,7 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -46,22 +46,22 @@ } else { natsort($chartNames); foreach ($chartNames as $i => $chartName) { - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } $helper->log(' ' . $chartName . ' - ' . $caption); $helper->log(str_repeat(' ', strlen($chartName) + 3)); - $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); if ($groupCount == 1) { - $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); $helper->log(' ' . $chartType); } else { $chartTypes = []; for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); } $chartTypes = array_unique($chartTypes); if (count($chartTypes) == 1) { diff --git a/samples/Chart/32_Chart_read_write_PDF.php b/samples/Chart/32_Chart_read_write_PDF.php index e3d702a9df..0ac41f71f6 100644 --- a/samples/Chart/32_Chart_read_write_PDF.php +++ b/samples/Chart/32_Chart_read_write_PDF.php @@ -20,7 +20,7 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -48,22 +48,22 @@ } else { natsort($chartNames); foreach ($chartNames as $i => $chartName) { - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } $helper->log(' ' . $chartName . ' - ' . $caption); $helper->log(str_repeat(' ', strlen($chartName) + 3)); - $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); if ($groupCount == 1) { - $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); $helper->log(' ' . $chartType); } else { $chartTypes = []; for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); } $chartTypes = array_unique($chartTypes); if (count($chartTypes) == 1) { diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php index d7b674f6d1..c177de79a3 100644 --- a/samples/Chart/33_Chart_create_line_dateaxis.php +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -195,7 +195,7 @@ $chartSheet = $spreadsheet->getSheet(1); $chartSheet->setTitle('Scatter+Line Chart'); -$chartSheet = $spreadsheet->getSheetByName('Scatter+Line Chart'); +$chartSheet = $spreadsheet->getSheetByNameOrThrow('Scatter+Line Chart'); // Add the chart to the worksheet $chartSheet->addChart($chart); @@ -340,11 +340,14 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array { - $dataSheet = $wrkbk->getSheetByName('Data'); + $dataSheet = $wrkbk->getSheetByNameOrThrow('Data'); // start the xaxis at the beginning of the quarter of the first date $startDateStr = $dataSheet->getCell('B2')->getValue(); // yyyy-mm-dd date string $startDate = DateTime::createFromFormat('Y-m-d', $startDateStr); // php date obj + if ($startDate === false) { + throw new Exception("invalid start date $startDateStr on spreadsheet"); + } // get date of first day of the quarter of the start date $startMonth = (int) $startDate->format('n'); // suppress leading zero @@ -357,12 +360,19 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array // end the xaxis at the end of the quarter of the last date $lastDateStr = $dataSheet->getCell([2, $nrows + 1])->getValue(); $lastDate = DateTime::createFromFormat('Y-m-d', $lastDateStr); + if ($lastDate === false) { + throw new Exception("invalid last date $lastDateStr on spreadsheet"); + } $lastMonth = (int) $lastDate->format('n'); $lastYr = (int) $lastDate->format('Y'); $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); $qtrEndMonth = 3 + (($qtr - 1) * 3); $qtrEndMonth = sprintf('%02d', $qtrEndMonth); - $lastDOM = DateTime::createFromFormat('Y-m-d', "$lastYr-$qtrEndMonth-01")->format('t'); + $lastDOMDate = DateTime::createFromFormat('Y-m-d', "$lastYr-$qtrEndMonth-01"); + if ($lastDOMDate === false) { + throw new Exception("invalid last dom date $lastYr-$qtrEndMonth-01 on spreadsheet"); + } + $lastDOM = $lastDOMDate->format('t'); $qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM"; $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr); diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index f58aadaf73..7467c82069 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -169,7 +169,7 @@ $chartSheet = $spreadsheet->getSheet(1); $chartSheet->setTitle('Scatter Chart'); -$chartSheet = $spreadsheet->getSheetByName('Scatter Chart'); +$chartSheet = $spreadsheet->getSheetByNameOrThrow('Scatter Chart'); // Add the chart to the worksheet $chartSheet->addChart($chart); diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index 4c403d993b..57eb691ef8 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -19,11 +19,13 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } if (count($inputFileNames) === 1) { + /** @var string[] */ $unresolvedErrors = []; } else { + /** @var string[] */ $unresolvedErrors = [ // The following spreadsheet was created by 3rd party software, // and doesn't include the data that usually accompanies a chart. @@ -66,9 +68,9 @@ natsort($chartNames); foreach ($chartNames as $j => $chartName) { $i = $renderedCharts + $j; - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } diff --git a/samples/Chart/35_Chart_render33.php b/samples/Chart/35_Chart_render33.php index 97091fceec..f9c7c11ad6 100644 --- a/samples/Chart/35_Chart_render33.php +++ b/samples/Chart/35_Chart_render33.php @@ -10,7 +10,7 @@ Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; -$inputFileNames = $helper->getTemporaryFolder() . '/33_Chart_create_*.xlsx'; +$inputFileNamesString = $helper->getTemporaryFolder() . '/33_Chart_create_*.xlsx'; if ((isset($argc)) && ($argc > 1)) { $inputFileNames = []; @@ -18,11 +18,13 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNamesString) ?: []; } if (count($inputFileNames) === 1) { + /** @var string[] */ $unresolvedErrors = []; } else { + /** @var string[] */ $unresolvedErrors = [ //'33_Chart_create_bar_stacked.xlsx', // fixed with mitoteam/jpgraph 10.3 ]; @@ -62,9 +64,9 @@ natsort($chartNames); foreach ($chartNames as $j => $chartName) { $i = $renderedCharts + $j; - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } diff --git a/samples/Chart/37_Chart_dynamic_title.php b/samples/Chart/37_Chart_dynamic_title.php index b8b801faae..e0df17cd04 100644 --- a/samples/Chart/37_Chart_dynamic_title.php +++ b/samples/Chart/37_Chart_dynamic_title.php @@ -37,7 +37,7 @@ } else { natsort($chartNames); foreach ($chartNames as $i => $chartName) { - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { @@ -45,15 +45,15 @@ } $helper->log(' ' . $chartName . ' - ' . $caption); $indentation = str_repeat(' ', strlen($chartName) + 3); - $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); if ($groupCount == 1) { - $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); $helper->log($indentation . ' ' . $chartType); $helper->renderChart($chart, __FILE__, $spreadsheet); } else { $chartTypes = []; for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); } $chartTypes = array_unique($chartTypes); if (count($chartTypes) == 1) { diff --git a/samples/DefinedNames/CrossWorksheetNamedFormula.php b/samples/DefinedNames/CrossWorksheetNamedFormula.php index 5ce7651628..6c2faec2b9 100644 --- a/samples/DefinedNames/CrossWorksheetNamedFormula.php +++ b/samples/DefinedNames/CrossWorksheetNamedFormula.php @@ -40,7 +40,7 @@ $worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet)); setYearlyData($worksheet, '2020', [], 'GROWTH'); -function setYearlyData(Worksheet $worksheet, string $year, $yearlyData, ?string $title = null): void +function setYearlyData(Worksheet $worksheet, string $year, array $yearlyData, ?string $title = null): void { // Set up some basic data $worksheetTitle = $title ?: $year; diff --git a/samples/DefinedNames/ScopedNamedRange.php b/samples/DefinedNames/ScopedNamedRange.php index aa71454df0..88bc62777e 100644 --- a/samples/DefinedNames/ScopedNamedRange.php +++ b/samples/DefinedNames/ScopedNamedRange.php @@ -60,12 +60,18 @@ ->setCellValue("B{$row}", '=SUM(COLUMN_DATA_VALUES)') ->setCellValue("C{$row}", '=SUM(COLUMN_DATA_VALUES)'); +$range = $spreadsheet->getNamedRange('CHARGE_RATE'); +if ($range === null || $range->getWorksheet() === null) { + throw new Exception('expected named range not found'); +} +$chargeRateCellValue = $spreadsheet + ->getSheetByNameOrThrow($range->getWorksheet()->getTitle()) + ->getCell($range->getCellsInRange()[0])->getValue(); + $helper->log(sprintf( 'Worked %.2f hours at a rate of %s - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), - $chargeRateCellValue = $spreadsheet - ->getSheetByName($spreadsheet->getNamedRange('CHARGE_RATE')->getWorksheet()->getTitle()) - ->getCell($spreadsheet->getNamedRange('CHARGE_RATE')->getCellsInRange()[0])->getValue(), + $chargeRateCellValue, $worksheet->getCell("C{$row}")->getCalculatedValue() )); diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index d7a1cb51e3..38ba4d9c45 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -21,7 +21,7 @@ function replaceBody(string $html): string EOF; - return preg_replace($bodystring, $bodyrepl, $html); + return preg_replace($bodystring, $bodyrepl, $html) ?? ''; } require __DIR__ . '/../Header.php'; diff --git a/samples/Pdf/21c_Pdf.php b/samples/Pdf/21c_Pdf.php index f556dcb2ff..6b6e6ab05f 100644 --- a/samples/Pdf/21c_Pdf.php +++ b/samples/Pdf/21c_Pdf.php @@ -16,7 +16,7 @@ function addHeadersFootersMpdf2000(string $html): string odd-footer-name: html_myFooter2; EOF; - $html = preg_replace('/@page page0 {/', $pagerepl, $html); + $html = preg_replace('/@page page0 {/', $pagerepl, $html) ?? ''; $bodystring = '//'; $simulatedBodyStart = Mpdf::SIMULATED_BODY_START; $bodyrepl = << [ - 'R' => $fontFile, - ], - ]; + 'R' => $fontFile, + ], + ]; return new \Mpdf\Mpdf($config); } diff --git a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php index 66efc3e0fb..0a79dadd9d 100644 --- a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php +++ b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php @@ -4,9 +4,15 @@ require __DIR__ . '/../Header.php'; +/** @return string[] */ +function getDesiredSheetNames(): array +{ + return ['Data Sheet #1', 'Data Sheet #3']; +} + $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$sheetnames = ['Data Sheet #1', 'Data Sheet #3']; +$sheetnames = getDesiredSheetNames(); $helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); diff --git a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php index 437fe6cffa..a8148f33c5 100644 --- a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php +++ b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php @@ -13,13 +13,13 @@ class MyReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; - private $columns = []; + private array $columns = []; - public function __construct($startRow, $endRow, $columns) + public function __construct(int $startRow, int $endRow, array $columns) { $this->startRow = $startRow; $this->endRow = $endRow; diff --git a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php index ec01132618..64c5b5665f 100644 --- a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php +++ b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php @@ -13,17 +13,14 @@ /** Define a Read Filter class implementing IReadFilter */ class ChunkReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; /** * We expect a list of the rows that we want to read to be passed into the constructor. - * - * @param mixed $startRow - * @param mixed $chunkSize */ - public function __construct($startRow, $chunkSize) + public function __construct(int $startRow, int $chunkSize) { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; diff --git a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php index e8c4a65903..5b2a4e48f7 100644 --- a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php +++ b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php @@ -13,17 +13,14 @@ /** Define a Read Filter class implementing IReadFilter */ class ChunkReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; /** * Set the list of rows that we want to read. - * - * @param mixed $startRow - * @param mixed $chunkSize */ - public function setRows($startRow, $chunkSize): void + public function setRows(int $startRow, int $chunkSize): void { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; diff --git a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php index 82544f60a7..3d6b2cb06a 100644 --- a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php +++ b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php @@ -13,17 +13,14 @@ /** Define a Read Filter class implementing IReadFilter */ class ChunkReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; /** * Set the list of rows that we want to read. - * - * @param mixed $startRow - * @param mixed $chunkSize */ - public function setRows($startRow, $chunkSize): void + public function setRows(int $startRow, int $chunkSize): void { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; @@ -51,8 +48,8 @@ public function readCell($columnAddress, $row, $worksheetName = '') // Tell the Reader that we want to use the Read Filter that we've Instantiated // and that we want to store it in contiguous rows/columns -$reader->setReadFilter($chunkFilter) - ->setContiguous(true); +$reader->setReadFilter($chunkFilter); +$reader->setContiguous(true); // Instantiate a new PhpSpreadsheet object manually $spreadsheet = new Spreadsheet(); diff --git a/samples/Reader/20_Reader_worksheet_hyperlink_image.php b/samples/Reader/20_Reader_worksheet_hyperlink_image.php index 2b3f294a6f..19200ee21e 100644 --- a/samples/Reader/20_Reader_worksheet_hyperlink_image.php +++ b/samples/Reader/20_Reader_worksheet_hyperlink_image.php @@ -14,9 +14,12 @@ $gdImage = @imagecreatetruecolor(120, 20); if ($gdImage === false) { - throw new \Exception('imagecreatetruecolor failed'); + throw new Exception('imagecreatetruecolor failed'); } $textColor = imagecolorallocate($gdImage, 255, 255, 255); +if ($textColor === false) { + throw new Exception('imagecolorallocate failed'); +} imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); $baseUrl = 'https://phpspreadsheet.readthedocs.io'; @@ -52,7 +55,7 @@ $helper->log('reloaded Spreadsheet'); foreach ($reloadedSpreadsheet->getActiveSheet()->getDrawingCollection() as $pDrawing) { - $helper->log('Read link: ' . $pDrawing->getHyperlink()->getUrl()); + $helper->log('Read link: ' . ($pDrawing->getHyperlink()?->getUrl() ?? 'none')); } $helper->log('end'); diff --git a/samples/Wizards/NumberFormat/Accounting.php b/samples/Wizards/NumberFormat/Accounting.php index c09c7ca6cc..20737e502b 100644 --- a/samples/Wizards/NumberFormat/Accounting.php +++ b/samples/Wizards/NumberFormat/Accounting.php @@ -83,11 +83,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Accounting($_POST['currency'], $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $wizard = new Wizard\Accounting($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Currency.php b/samples/Wizards/NumberFormat/Currency.php index 87da383607..313db31949 100644 --- a/samples/Wizards/NumberFormat/Currency.php +++ b/samples/Wizards/NumberFormat/Currency.php @@ -83,11 +83,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Currency($_POST['currency'], $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $wizard = new Wizard\Currency($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Number.php b/samples/Wizards/NumberFormat/Number.php index ab2b2b3f3f..274f7904a1 100644 --- a/samples/Wizards/NumberFormat/Number.php +++ b/samples/Wizards/NumberFormat/Number.php @@ -49,11 +49,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Number($_POST['decimals'], isset($_POST['thousands'])); + $wizard = new Wizard\Number((int) $_POST['decimals'], isset($_POST['thousands'])); $mask = $wizard->format(); $example = NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Percentage.php b/samples/Wizards/NumberFormat/Percentage.php index c1838827ba..71b5c69113 100644 --- a/samples/Wizards/NumberFormat/Percentage.php +++ b/samples/Wizards/NumberFormat/Percentage.php @@ -43,11 +43,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Percentage($_POST['decimals']); + $wizard = new Wizard\Percentage((int) $_POST['decimals']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Scientific.php b/samples/Wizards/NumberFormat/Scientific.php index 40d390a5e2..61e635d782 100644 --- a/samples/Wizards/NumberFormat/Scientific.php +++ b/samples/Wizards/NumberFormat/Scientific.php @@ -43,11 +43,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Scientific($_POST['decimals']); + $wizard = new Wizard\Scientific((int) $_POST['decimals']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/templates/sampleSpreadsheet.php b/samples/templates/sampleSpreadsheet.php index 92e88262d3..998f211055 100644 --- a/samples/templates/sampleSpreadsheet.php +++ b/samples/templates/sampleSpreadsheet.php @@ -74,19 +74,19 @@ $spreadsheet->getActiveSheet()->getComment('E11')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('Total amount on the current invoice, excluding VAT.'); $spreadsheet->getActiveSheet()->getComment('E12')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('Total amount of VAT on the current invoice.'); $spreadsheet->getActiveSheet()->getComment('E13')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('Total amount on the current invoice, including VAT.'); $spreadsheet->getActiveSheet()->getComment('E13')->setWidth('100pt'); @@ -100,9 +100,9 @@ $richText->createText('This invoice is '); $payable = $richText->createTextRun('payable within thirty days after the end of the month'); -$payable->getFont()->setBold(true); -$payable->getFont()->setItalic(true); -$payable->getFont()->setColor(new Color(Color::COLOR_DARKGREEN)); +$payable->getFontOrThrow()->setBold(true); +$payable->getFontOrThrow()->setItalic(true); +$payable->getFontOrThrow()->setColor(new Color(Color::COLOR_DARKGREEN)); $richText->createText(', unless specified otherwise on the invoice.'); diff --git a/samples/templates/sampleSpreadsheet2.php b/samples/templates/sampleSpreadsheet2.php index 5a7beda705..76b5e04547 100644 --- a/samples/templates/sampleSpreadsheet2.php +++ b/samples/templates/sampleSpreadsheet2.php @@ -74,19 +74,19 @@ $spreadsheet->getActiveSheet()->getComment('E11')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('Total amount on the current invoice, excluding VAT.'); $spreadsheet->getActiveSheet()->getComment('E12')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('Total amount of VAT on the current invoice.'); $spreadsheet->getActiveSheet()->getComment('E13')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('Total amount on the current invoice, including VAT.'); $spreadsheet->getActiveSheet()->getComment('E13')->setWidth('100pt'); @@ -100,9 +100,9 @@ $richText->createText('This invoice is '); $payable = $richText->createTextRun('payable within thirty days after the end of the month'); -$payable->getFont()->setBold(true); -$payable->getFont()->setItalic(true); -$payable->getFont()->setColor(new Color(Color::COLOR_DARKGREEN)); +$payable->getFontOrThrow()->setBold(true); +$payable->getFontOrThrow()->setItalic(true); +$payable->getFontOrThrow()->setColor(new Color(Color::COLOR_DARKGREEN)); $richText->createText(', unless specified otherwise on the invoice.'); diff --git a/src/PhpSpreadsheet/Cell/DataValidation.php b/src/PhpSpreadsheet/Cell/DataValidation.php index 7f1b363c5d..22b285b307 100644 --- a/src/PhpSpreadsheet/Cell/DataValidation.php +++ b/src/PhpSpreadsheet/Cell/DataValidation.php @@ -140,13 +140,13 @@ public function getFormula1() /** * Set Formula 1. * - * @param string $formula + * @param float|int|string $formula usually string, but can be number (test for equal) * * @return $this */ public function setFormula1($formula): static { - $this->formula1 = $formula; + $this->formula1 = (string) $formula; return $this; } @@ -164,13 +164,13 @@ public function getFormula2() /** * Set Formula 2. * - * @param string $formula + * @param float|int|string $formula usually string, but can be number (test for equal) * * @return $this */ public function setFormula2($formula): static { - $this->formula2 = $formula; + $this->formula2 = (string) $formula; return $this; } diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 7de219184a..3f9249cedd 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -124,10 +124,10 @@ public function getAxisIsNumericFormat(): bool return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric']; } - public function setAxisOption(string $key, ?string $value): void + public function setAxisOption(string $key, null|float|int|string $value): void { if ($value !== null && $value !== '') { - $this->axisOptions[$key] = $value; + $this->axisOptions[$key] = (string) $value; } } @@ -141,11 +141,11 @@ public function setAxisOptionsProperties( ?string $axisOrientation = null, ?string $majorTmt = null, ?string $minorTmt = null, - ?string $minimum = null, - ?string $maximum = null, - ?string $majorUnit = null, - ?string $minorUnit = null, - ?string $textRotation = null, + null|float|int|string $minimum = null, + null|float|int|string $maximum = null, + null|float|int|string $majorUnit = null, + null|float|int|string $minorUnit = null, + null|float|int|string $textRotation = null, ?string $hidden = null, ?string $baseTimeUnit = null, ?string $majorTimeUnit = null, diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index b86b19cdd0..2b8de5d517 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -285,6 +285,16 @@ public function getPlotArea(): ?PlotArea return $this->plotArea; } + public function getPlotAreaOrThrow(): PlotArea + { + $plotArea = $this->getPlotArea(); + if ($plotArea !== null) { + return $plotArea; + } + + throw new Exception('Chart has no PlotArea'); + } + /** * Set Plot Area. */ diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 8bceae071d..6475793457 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -124,7 +124,7 @@ class DataSeries * @param DataSeriesValues[] $plotCategory * @param DataSeriesValues[] $plotValues * @param null|string $plotDirection - * @param bool $smoothLine + * @param null|bool $smoothLine null treated as false * @param null|string $plotStyle */ public function __construct($plotType = null, $plotGrouping = null, array $plotOrder = [], array $plotLabel = [], array $plotCategory = [], array $plotValues = [], $plotDirection = null, $smoothLine = false, $plotStyle = null) @@ -144,7 +144,7 @@ public function __construct($plotType = null, $plotGrouping = null, array $plotO } $this->plotCategory = $plotCategory; - $this->smoothLine = $smoothLine; + $this->smoothLine = (bool) $smoothLine; $this->plotStyle = $plotStyle; if ($plotDirection === null) { diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index a3f5ca0cf0..72bbf81036 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -98,7 +98,7 @@ class DataSeriesValues extends Properties * @param mixed $dataValues * @param null|mixed $marker * @param null|ChartColor|ChartColor[]|string|string[] $fillColor - * @param string $pointSize + * @param int|string $pointSize point size */ public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3') { diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 849f561949..740702d12d 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -795,9 +795,9 @@ public function getLineColorProperty($propertyName) * @param string $capType * @param string $joinType * @param string $headArrowType - * @param string $headArrowSize + * @param null|int|string $headArrowSize index into ARROW_SIZES array * @param string $endArrowType - * @param string $endArrowSize + * @param null|int|string $endArrowSize index into ARROW_SIZES array * @param string $headArrowWidth * @param string $headArrowLength * @param string $endArrowWidth @@ -824,7 +824,7 @@ public function setLineStyleProperties($lineWidth = null, $compoundType = '', $d if ($headArrowType !== '') { $this->lineStyleProperties['arrow']['head']['type'] = $headArrowType; } - if (array_key_exists($headArrowSize, self::ARROW_SIZES)) { + if (isset(self::ARROW_SIZES[$headArrowSize])) { $this->lineStyleProperties['arrow']['head']['size'] = $headArrowSize; $this->lineStyleProperties['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; $this->lineStyleProperties['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; @@ -832,7 +832,7 @@ public function setLineStyleProperties($lineWidth = null, $compoundType = '', $d if ($endArrowType !== '') { $this->lineStyleProperties['arrow']['end']['type'] = $endArrowType; } - if (array_key_exists($endArrowSize, self::ARROW_SIZES)) { + if (isset(self::ARROW_SIZES[$endArrowSize])) { $this->lineStyleProperties['arrow']['end']['size'] = $endArrowSize; $this->lineStyleProperties['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; $this->lineStyleProperties['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; diff --git a/src/PhpSpreadsheet/RichText/Run.php b/src/PhpSpreadsheet/RichText/Run.php index c2b7156e60..05d90579e1 100644 --- a/src/PhpSpreadsheet/RichText/Run.php +++ b/src/PhpSpreadsheet/RichText/Run.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\RichText; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\Style\Font; class Run extends TextElement implements ITextElement @@ -35,6 +36,15 @@ public function getFont() return $this->font; } + public function getFontOrThrow(): Font + { + if ($this->font === null) { + throw new SpreadsheetException('unexpected null font'); + } + + return $this->font; + } + /** * Set font. * diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 89c5458525..35d3ff866d 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -662,6 +662,16 @@ public function getChartByName($chartName) return false; } + public function getChartByNameOrThrow(string $chartName): Chart + { + $chart = $this->getChartByName($chartName); + if ($chart !== false) { + return $chart; + } + + throw new Exception("Sheet does not have a chart named $chartName."); + } + /** * Refresh column dimensions. * diff --git a/tests/PhpSpreadsheetTests/Chart/ChartsByNameTest.php b/tests/PhpSpreadsheetTests/Chart/ChartsByNameTest.php new file mode 100644 index 0000000000..a3583672f8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ChartsByNameTest.php @@ -0,0 +1,97 @@ +getActiveSheet(); + $sheet->setTitle('Only Sheet'); + $sheet->fromArray( + [ + ['Some Title'], + [], + [null, null, 'Data'], + [null, 'L1', 1.3], + [null, 'L2', 1.3], + [null, 'L3', 2.3], + [null, 'L4', 1.6], + [null, 'L5', 1.5], + [null, 'L6', 1.4], + [null, 'L7', 2.2], + [null, 'L8', 1.8], + [null, 'L9', 1.1], + [null, 'L10', 1.8], + [null, 'L11', 1.6], + [null, 'L12', 2.7], + [null, 'L13', 2.2], + [null, 'L14', 1.3], + ] + ); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4', null, 1), // 2010 + ]; + // Set the X-Axis Labels + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4:$B$17'), + ]; + // Set the Data values for each data series we want to plot + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, '\'Only Sheet\'!$C$4:$C$17'), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + DataSeries::GROUPING_STANDARD, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + + // Create the chart + $chart = new Chart( + name: 'namedchart1', + plotArea: $plotArea, + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('G7'); + $chart->setBottomRightPosition('N21'); + // Add the chart to the worksheet + $sheet->addChart($chart); + $sheet->setSelectedCells('D1'); + self::assertSame($chart, $sheet->getChartByName('namedchart1')); + self::assertSame($chart, $sheet->getChartByNameOrThrow('namedchart1')); + self::assertFalse($sheet->getChartByName('namedchart2')); + + try { + $sheet->getChartByNameOrThrow('namedchart2'); + $exceptionRaised = false; + } catch (SpreadsheetException $e) { + self::assertSame('Sheet does not have a chart named namedchart2.', $e->getMessage()); + $exceptionRaised = true; + } + + self::assertTrue($exceptionRaised); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/PlotAreaTest.php b/tests/PhpSpreadsheetTests/Chart/PlotAreaTest.php new file mode 100644 index 0000000000..819e41cec8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/PlotAreaTest.php @@ -0,0 +1,48 @@ +getPlotAreaOrThrow()); + } + + public function testNoPlotArea(): void + { + $chart = new Chart('chart1'); + $this->expectException(ChartException::class); + $this->expectExceptionMessage('Chart has no PlotArea'); + $chart->getPlotAreaOrThrow(); + } +} diff --git a/tests/PhpSpreadsheetTests/RichTextTest.php b/tests/PhpSpreadsheetTests/RichTextTest.php index 3ebd099c0e..513dcf7a9d 100644 --- a/tests/PhpSpreadsheetTests/RichTextTest.php +++ b/tests/PhpSpreadsheetTests/RichTextTest.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\TextElement; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -48,4 +49,20 @@ public function testTextElements(): void self::assertSame([['ABC', '-3.5']], $sheet->toArray()); $spreadsheet->disconnectWorksheets(); } + + public function testNullFont(): void + { + $richText = new RichText(); + $textRun = $richText->createTextRun('hello'); + self::assertNotNull($textRun->getFontOrThrow()); + $textRun->setFont(null); + + try { + $textRun->getFontOrThrow(); + $foundFont = true; + } catch (SpreadsheetException $e) { + $foundFont = false; + } + self::assertFalse($foundFont, 'expected exception not received'); + } }