From 7ed96e0be15ef292bc05283b3f6122fc0f28968b Mon Sep 17 00:00:00 2001 From: Alban Duval Date: Mon, 25 May 2020 21:33:48 +0200 Subject: [PATCH 001/153] Calcualtion - DATEDIF - fix result for Y & YM units (#1466) Bugfix for negative results and too small results 2000-02-02 => 2001-02-01 > DATEDIF with Y unit: 0 year (returned -1 before fix) > DATEDIF with YM unit: 11 months (returned -1 before fix) --- src/PhpSpreadsheet/Calculation/DateTime.php | 30 +++++---------------- tests/data/Calculation/DateTime/DATEDIF.php | 14 +++++++++- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index d08ab543..19860794 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -668,30 +668,19 @@ class DateTime $endMonths = $PHPEndDateObject->format('n'); $endYears = $PHPEndDateObject->format('Y'); + $PHPDiffDateObject = $PHPEndDateObject->diff($PHPStartDateObject); + switch ($unit) { case 'D': $retVal = (int) $difference; break; case 'M': - $retVal = (int) ($endMonths - $startMonths) + ((int) ($endYears - $startYears) * 12); - // We're only interested in full months - if ($endDays < $startDays) { - --$retVal; - } + $retVal = (int) 12 * $PHPDiffDateObject->format('%y') + $PHPDiffDateObject->format('%m'); break; case 'Y': - $retVal = (int) ($endYears - $startYears); - // We're only interested in full months - if ($endMonths < $startMonths) { - --$retVal; - } elseif (($endMonths == $startMonths) && ($endDays < $startDays)) { - // Remove start month - --$retVal; - // Remove end month - --$retVal; - } + $retVal = (int) $PHPDiffDateObject->format('%y'); break; case 'MD': @@ -701,19 +690,12 @@ class DateTime $adjustDays = $PHPEndDateObject->format('j'); $retVal += ($adjustDays - $startDays); } else { - $retVal = $endDays - $startDays; + $retVal = (int) $PHPDiffDateObject->format('%d'); } break; case 'YM': - $retVal = (int) ($endMonths - $startMonths); - if ($retVal < 0) { - $retVal += 12; - } - // We're only interested in full months - if ($endDays < $startDays) { - --$retVal; - } + $retVal = (int) $PHPDiffDateObject->format('%m'); break; case 'YD': diff --git a/tests/data/Calculation/DateTime/DATEDIF.php b/tests/data/Calculation/DateTime/DATEDIF.php index d113d3aa..a6d2d761 100644 --- a/tests/data/Calculation/DateTime/DATEDIF.php +++ b/tests/data/Calculation/DateTime/DATEDIF.php @@ -393,6 +393,10 @@ return [ 1, '19-12-1960', '26-01-2012', 'YM', ], + [ + 11, + '19-12-1960', '26-11-1962', 'YM', + ], [ 38, '19-12-1960', '26-01-2012', 'YD', @@ -402,7 +406,15 @@ return [ '19-12-1960', '26-01-2012', 'MD', ], [ - 50, + 0, + '19-12-1960', '12-12-1961', 'Y', + ], + [ + 1, + '19-12-1960', '12-12-1962', 'Y', + ], + [ + 51, '19-12-1960', '12-12-2012', 'Y', ], [ From b3b0b49b7ccadc899d776c3ff406fa741ff46a4f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 25 May 2020 21:41:02 +0200 Subject: [PATCH 002/153] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22fd8c3d..fe1f159a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Several improvements in HTML writer [#1464](https://github.com/PHPOffice/PhpSpreadsheet/pull/1464) - Fix incorrect behaviour when saving XLSX file with drawings [#1462](https://github.com/PHPOffice/PhpSpreadsheet/pull/1462), - Fix Crash while trying setting a cell the value "123456\n" [#1476](https://github.com/PHPOffice/PhpSpreadsheet/pull/1481) +- Improved DATEDIF() function and reduced errors for Y and YM units [#1466](https://github.com/PHPOffice/PhpSpreadsheet/pull/1466) ### Changed From 8b2bba9bdb6ca5969730462626d7866bac4bb00c Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 29 May 2020 21:53:28 +0200 Subject: [PATCH 003/153] Range operator tests (#1498) * Fix intersection operator when working with named ranges --- .../Calculation/Calculation.php | 10 ++-- .../Calculation/Engine/RangeTest.php | 53 ++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index cdfe7b53..5aa309c5 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3456,10 +3456,8 @@ class Calculation if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) { $opCharacter .= $formula[++$index]; } - // Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match); - if ($opCharacter == '-' && !$expectingOperator) { // Is it a negation instead of a minus? // Put a negation on the stack $stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); @@ -3776,8 +3774,12 @@ class Calculation } // If we're expecting an operator, but only have a space between the previous and next operands (and both are // Cell References) then we have an INTERSECTION operator - if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) && - ($output[count($output) - 1]['type'] == 'Cell Reference')) { + if (($expectingOperator) && + ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) && + ($output[count($output) - 1]['type'] == 'Cell Reference') || + (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', substr($formula, $index), $match)) && + ($output[count($output) - 1]['type'] == 'Named Range' || $output[count($output) - 1]['type'] == 'Value') + )) { while ($stack->count() > 0 && ($o2 = $stack->last()) && isset(self::$operators[$o2['value']]) && diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index 84cac747..d1ad229b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Engine; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -28,7 +29,7 @@ class RangeTest extends TestCase /** * @dataProvider providerRangeEvaluation * - * @param mixed $formula + * @param string $formula * @param int $expectedResult */ public function testRangeEvaluation($formula, $expectedResult): void @@ -51,4 +52,54 @@ class RangeTest extends TestCase ['=SUM(A1:B2 B2:C3)', 5], ]; } + + /** + * @dataProvider providerNamedRangeEvaluation + * + * @param string $group1 + * @param string $group2 + * @param string $formula + * @param int $expectedResult + */ + public function testNamedRangeEvaluation($group1, $group2, $formula, $expectedResult): void + { + $workSheet = $this->spreadSheet->getActiveSheet(); + $this->spreadSheet->addNamedRange(new NamedRange('GROUP1', $workSheet, $group1)); + $this->spreadSheet->addNamedRange(new NamedRange('GROUP2', $workSheet, $group2)); + + $workSheet->setCellValue('E1', $formula); + + $actualRresult = $workSheet->getCell('E1')->getCalculatedValue(); + self::assertSame($expectedResult, $actualRresult); + } + + public function providerNamedRangeEvaluation() + { + return[ + [ + 'A1:B3', + 'A1:C2', + '=SUM(GROUP1,GROUP2)', + 48, + ], + [ + 'A1:B3', + 'A1:C2', + '=SUM(GROUP1 GROUP2)', + 12, + ], + [ + 'A1:B2', + 'B2:C3', + '=SUM(GROUP1,GROUP2)', + 40, + ], + [ + 'A1:B2', + 'B2:C3', + '=SUM(GROUP1 GROUP2)', + 5, + ], + ]; + } } From edc411e6dd0ffbd7faf02aea9458f350d5a41574 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sat, 30 May 2020 21:27:35 -0700 Subject: [PATCH 004/153] Add ability to save edited Html/Pdf We give users the ability to edit Html/Pdf, but it's a little cumbersome to use the edited Html for an Html file, and difficult to use it for a Pdf. I believe we could make it fairly painless in both cases by allowing the user to set a callback to edit the generated Html. This can be accomplished with fewer than a dozen lines of very simple code. I think this would be easier than grabbing the Html in pieces, editing it, and reassembling it. I think it would also be simpler than an alternative I considered, namely the addition of a new method (e.g. saveEditedHtml) to each of the Html and Pdf writers. One edit that users might like to make when editing html is to add fallback fonts, something that is not currently available in PhpSpreadsheet, and might be difficult to add. A natural extension to that idea would be the use of webfonts, something which is guaranteed difficult to add. See samples/Basic/17b_Html for an example of this. None of the PDF writers support webfonts yet. That doesn't mean they won't do so in future, but, for now, samples/Pdf/21a_Pdf is a prosaic example of something you could do with this callback. In fact, this opens the door to letting the user replace the entire body with data of their choosing, effectively allowing PhpSpreadsheet (where you can set things like paper size and orientation) to be used as a front-end to the Pdf processor without the user having to be be overly familiar with the vagaries of the PDF processor. I think this is actually a pretty nice idea. YMMV. See samples/Basic/21b_Pdf for an example. --- docs/topics/reading-and-writing-to-file.md | 33 +++++++++++- samples/Basic/17b_Html.php | 27 ++++++++++ samples/Pdf/21a_Pdf.php | 47 ++++++++++++++++ samples/Pdf/21b_Pdf.php | 51 ++++++++++++++++++ src/PhpSpreadsheet/Writer/Html.php | 21 ++++++++ .../Writer/Html/CallbackTest.php | 53 +++++++++++++++++++ 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 samples/Basic/17b_Html.php create mode 100644 samples/Pdf/21a_Pdf.php create mode 100644 samples/Pdf/21b_Pdf.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 8f92e1f2..abd7c5f3 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -693,7 +693,7 @@ $sty = $writer->generateStyles(false); // do not write $newstyle = << $sty -html { +body { background-color: yellow; } @@ -703,6 +703,31 @@ echo $writer->generateSheetData(); echo $writer->generateHTMLFooter(); ``` +#### Editing HTML During Save Via a Callback + +You can also add a callback function to edit the generated html +before saving. For example, you could add a webfont +(not currently supported for Pdf) as follows: + +``` php +function webfont(string $html): string +{ + $linktag = << + +EOF; + $html = preg_replace('@setEditHtmlCallback('webfont'); +$writer->save($filename); +``` + #### Writing UTF-8 HTML files A HTML file can be marked as UTF-8 by writing a BOM file header. This @@ -841,6 +866,12 @@ $writer->setPreCalculateFormulas(false); $writer->save("05featuredemo.pdf"); ``` +#### Editing Pdf During Save Via a Callback + +You can also add a callback function to edit the html used to +generate the Pdf before saving. +[See under Html](#editing-html-during-save-via-a-callback). + #### Decimal and thousands separators See section `\PhpOffice\PhpSpreadsheet\Writer\Csv` how to control the diff --git a/samples/Basic/17b_Html.php b/samples/Basic/17b_Html.php new file mode 100644 index 00000000..05649314 --- /dev/null +++ b/samples/Basic/17b_Html.php @@ -0,0 +1,27 @@ +getFilename(__FILE__, 'html'); +$writer = new Html($spreadsheet); + +function webfont(string $html): string +{ + $linktag = << + +EOF; + $html = preg_replace('@setEmbedImages(true); +$writer->setEditHtmlCallback('webfont'); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Pdf/21a_Pdf.php b/samples/Pdf/21a_Pdf.php new file mode 100644 index 00000000..c4dc2c48 --- /dev/null +++ b/samples/Pdf/21a_Pdf.php @@ -0,0 +1,47 @@ +log('Hide grid lines'); +$spreadsheet->getActiveSheet()->setShowGridLines(false); + +$helper->log('Set orientation to landscape'); +$spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); + +function yellowBody(string $html): string +{ + $newstyle = << +body { +background-color: yellow; +} + + +EOF; + + return preg_replace('@@', "$newstyle", $html); +} + +$helper->log('Write to Dompdf'); +$writer = new Dompdf($spreadsheet); +$filename = $helper->getFileName('21a_Pdf_dompdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('yellowBody'); +$writer->save($filename); + +$helper->log('Write to Mpdf'); +$writer = new Mpdf($spreadsheet); +$filename = $helper->getFileName('21a_Pdf_mpdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('yellowBody'); +$writer->save($filename); + +$helper->log('Write to Tcpdf'); +$writer = new Tcpdf($spreadsheet); +$filename = $helper->getFileName('21a_Pdf_tcpdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('yellowBody'); +$writer->save($filename); diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php new file mode 100644 index 00000000..142fc344 --- /dev/null +++ b/samples/Pdf/21b_Pdf.php @@ -0,0 +1,51 @@ +.*@ms'; + $bodyrepl = << +

Serif

+

$lorem

+

Sans-Serif

+

$lorem

+

Monospace

+

$lorem

+ +EOF; + + return preg_replace($bodystring, $bodyrepl, $html); +} + +require __DIR__ . '/../Header.php'; +$spreadsheet = require __DIR__ . '/../templates/sampleSpreadsheet.php'; + +$helper->log('Hide grid lines'); +$spreadsheet->getActiveSheet()->setShowGridLines(false); + +$helper->log('Set orientation to landscape'); +$spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); + +$helper->log('Write to Dompdf'); +$writer = new Dompdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_dompdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); + +$helper->log('Write to Mpdf'); +$writer = new Mpdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_mpdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); + +$helper->log('Write to Tcpdf'); +$writer = new Tcpdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_tcpdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index c9f2d7cb..963b02d7 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -131,6 +131,13 @@ class Html extends BaseWriter */ private $generateSheetNavigationBlock = true; + /** + * Callback for editing generated html. + * + * @var callable + */ + protected $editHtmlCallback = ''; + /** * Create a new HTML. */ @@ -190,6 +197,10 @@ class Html extends BaseWriter // Write footer $html .= $this->generateHTMLFooter(); + $cbk = $this->editHtmlCallback; + if ($cbk) { + $html = $cbk($html); + } Calculation::setArrayReturnType($saveArrayReturnType); Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); @@ -197,6 +208,16 @@ class Html extends BaseWriter return $html; } + public function setEditHtmlCallback(callable $cbk): void + { + $this->editHtmlCallback = $cbk; + } + + public function resetEditHtmlCallback(): void + { + $this->editHtmlCallback = ''; + } + const VALIGN_ARR = [ Alignment::VERTICAL_BOTTOM => 'bottom', Alignment::VERTICAL_TOP => 'top', diff --git a/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php new file mode 100644 index 00000000..f712419c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php @@ -0,0 +1,53 @@ + +body { + background-color: yellow; +} + + +EOF; + + return preg_replace('@@', "$newstyle", $html); + } + + public function testSetAndReset(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A1', '1'); + + $writer = new Html($spreadsheet); + $html1 = $writer->generateHTMLall(); + $writer->setEditHtmlCallback([$this, 'yellowBody']); + $html2 = $writer->generateHTMLall(); + $writer->resetEditHtmlCallback(); + $html3 = $writer->generateHTMLall(); + + self::assertFalse(strpos($html1, 'background-color: yellow')); + self::assertNotFalse(strpos($html2, 'background-color: yellow')); + self::assertFalse(strpos($html3, 'background-color: yellow')); + self::assertEquals($html3, $html1); + + $writer->setEditHtmlCallback([$this, 'yellowBody']); + $oufil = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($oufil); + $html4 = file_get_contents($oufil); + unlink($oufil); + self::assertNotFalse(strpos($html4, 'background-color: yellow')); + + $this->writeAndReload($spreadsheet, 'Html'); + } +} From c434e9b13735f82aa76901c52ba5339712fbaf0e Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 31 May 2020 15:58:57 +0900 Subject: [PATCH 005/153] Stricter typing for mergeCells Closes #1494 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Worksheet/Worksheet.php | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1f159a..75eb393c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix incorrect behaviour when saving XLSX file with drawings [#1462](https://github.com/PHPOffice/PhpSpreadsheet/pull/1462), - Fix Crash while trying setting a cell the value "123456\n" [#1476](https://github.com/PHPOffice/PhpSpreadsheet/pull/1481) - Improved DATEDIF() function and reduced errors for Y and YM units [#1466](https://github.com/PHPOffice/PhpSpreadsheet/pull/1466) +- Stricter typing for mergeCells [#1494](https://github.com/PHPOffice/PhpSpreadsheet/pull/1494) ### Changed diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 1ed77e05..e2b0dd87 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -187,7 +187,7 @@ class Worksheet implements IComparable /** * Collection of merged cell ranges. * - * @var array + * @var string[] */ private $mergeCells = []; @@ -1747,7 +1747,7 @@ class Worksheet implements IComparable /** * Get merge cells array. * - * @return array[] + * @return string[] */ public function getMergeCells() { @@ -1758,6 +1758,8 @@ class Worksheet implements IComparable * Set merge cells array for the entire sheet. Use instead mergeCells() to merge * a single cell range. * + * @param string[] $pValue + * * @return $this */ public function setMergeCells(array $pValue) From dfa6f7717801e6738bb266ef3fac25fd8ad5294d Mon Sep 17 00:00:00 2001 From: Reijn Date: Sat, 23 May 2020 20:01:03 +0300 Subject: [PATCH 006/153] Add support protection of worksheet by a specific hash algorithm --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Xlsx.php | 4 + src/PhpSpreadsheet/Shared/PasswordHasher.php | 91 +++++++++++++- src/PhpSpreadsheet/Worksheet/Protection.php | 124 +++++++++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 16 +++ tests/data/Shared/PasswordHashes.php | 26 ++++ 6 files changed, 261 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75eb393c..dd24e146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292) - Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468) +- Support protection of worksheet by a specific hash algorithm ### Fixed diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 797e59ea..f9f20bdd 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -765,6 +765,10 @@ class Xlsx extends BaseReader if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) { $docSheet->getProtection()->setPassword((string) $xmlSheet->sheetProtection['password'], true); + $docSheet->getProtection()->setAlgorithmName((string) $xmlSheet->sheetProtection['algorithmName']); + $docSheet->getProtection()->setHashValue((string) $xmlSheet->sheetProtection['hashValue']); + $docSheet->getProtection()->setSaltValue((string) $xmlSheet->sheetProtection['saltValue']); + $docSheet->getProtection()->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']); if ($xmlSheet->protectedRanges->protectedRange) { foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true); diff --git a/src/PhpSpreadsheet/Shared/PasswordHasher.php b/src/PhpSpreadsheet/Shared/PasswordHasher.php index 9b0080b9..f3c2d3e1 100644 --- a/src/PhpSpreadsheet/Shared/PasswordHasher.php +++ b/src/PhpSpreadsheet/Shared/PasswordHasher.php @@ -4,6 +4,51 @@ namespace PhpOffice\PhpSpreadsheet\Shared; class PasswordHasher { + const ALGORITHM_MD2 = 'MD2'; + const ALGORITHM_MD4 = 'MD4'; + const ALGORITHM_MD5 = 'MD5'; + const ALGORITHM_SHA_1 = 'SHA-1'; + const ALGORITHM_SHA_256 = 'SHA-256'; + const ALGORITHM_SHA_384 = 'SHA-384'; + const ALGORITHM_SHA_512 = 'SHA-512'; + const ALGORITHM_RIPEMD_128 = 'RIPEMD-128'; + const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; + const ALGORITHM_WHIRLPOOL = 'WHIRLPOOL'; + + /** + * Mapping between algorithm name in Excel and algorithm name in PHP. + * + * @var array + */ + private static $algorithmArray = [ + self::ALGORITHM_MD2 => 'md2', + self::ALGORITHM_MD4 => 'md4', + self::ALGORITHM_MD5 => 'md5', + self::ALGORITHM_SHA_1 => 'sha1', + self::ALGORITHM_SHA_256 => 'sha256', + self::ALGORITHM_SHA_384 => 'sha384', + self::ALGORITHM_SHA_512 => 'sha512', + self::ALGORITHM_RIPEMD_128 => 'ripemd128', + self::ALGORITHM_RIPEMD_160 => 'ripemd160', + self::ALGORITHM_WHIRLPOOL => 'whirlpool', + ]; + + /** + * Get algorithm from self::$algorithmArray. + * + * @param string $pAlgorithmName + * + * @return string + */ + private static function getAlgorithm($pAlgorithmName) + { + if (array_key_exists($pAlgorithmName, self::$algorithmArray)) { + return self::$algorithmArray[$pAlgorithmName]; + } + + return ''; + } + /** * Create a password hash from a given string. * @@ -15,7 +60,7 @@ class PasswordHasher * * @return string Hashed password */ - public static function hashPassword($pPassword) + public static function defaultHashPassword($pPassword) { $password = 0x0000; $charPos = 1; // char position @@ -34,4 +79,48 @@ class PasswordHasher return strtoupper(dechex($password)); } + + /** + * Create a password hash from a given string by a specific algorithm. + * + * 2.4.2.4 ISO Write Protection Method + * + * @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f + * + * @param string $pPassword Password to hash + * @param string $pAlgorithmName Hash algorithm used to compute the password hash value + * @param string $pSaltValue Pseudorandom string + * @param string $pSpinCount Number of times to iterate on a hash of a password + * + * @return string Hashed password + */ + public static function hashPassword($pPassword, $pAlgorithmName = '', $pSaltValue = '', $pSpinCount = 10000) + { + $algorithmName = self::getAlgorithm($pAlgorithmName); + if (!$pAlgorithmName) { + return self::defaultHashPassword($pPassword); + } + + $saltValue = base64_decode($pSaltValue); + $password = mb_convert_encoding($pPassword, 'UCS-2LE', 'UTF-8'); + + $hashValue = hash($algorithmName, $saltValue . $password, true); + for ($i = 0; $i < $pSpinCount; ++$i) { + $hashValue = hash($algorithmName, $hashValue . pack('L', $i), true); + } + + return base64_encode($hashValue); + } + + /** + * Create a pseudorandom string. + * + * @param int $pSize Length of the output string in bytes + * + * @return string Pseudorandom string + */ + public static function generateSalt($pSize = 16) + { + return base64_encode(random_bytes($pSize)); + } } diff --git a/src/PhpSpreadsheet/Worksheet/Protection.php b/src/PhpSpreadsheet/Worksheet/Protection.php index 2fd3e919..3566e255 100644 --- a/src/PhpSpreadsheet/Worksheet/Protection.php +++ b/src/PhpSpreadsheet/Worksheet/Protection.php @@ -125,6 +125,34 @@ class Protection */ private $password = ''; + /** + * Algorithm name. + * + * @var string + */ + private $algorithmName = ''; + + /** + * Hash value. + * + * @var string + */ + private $hashValue = ''; + + /** + * Salt value. + * + * @var string + */ + private $saltValue = ''; + + /** + * Spin count. + * + * @var int + */ + private $spinCount = ''; + /** * Create a new Protection. */ @@ -569,6 +597,102 @@ class Protection return $this; } + /** + * Get AlgorithmName. + * + * @return string + */ + public function getAlgorithmName() + { + return $this->algorithmName; + } + + /** + * Set AlgorithmName. + * + * @param string $pValue + * + * @return $this + */ + public function setAlgorithmName($pValue) + { + $this->algorithmName = $pValue; + + return $this; + } + + /** + * Get HashValue. + * + * @return string + */ + public function getHashValue() + { + return $this->hashValue; + } + + /** + * Set HashValue. + * + * @param string $pValue + * + * @return $this + */ + public function setHashValue($pValue) + { + $this->hashValue = $pValue; + + return $this; + } + + /** + * Get SaltValue. + * + * @return string + */ + public function getSaltValue() + { + return $this->saltValue; + } + + /** + * Set SaltValue. + * + * @param string $pValue + * + * @return $this + */ + public function setSaltValue($pValue) + { + $this->saltValue = $pValue; + + return $this; + } + + /** + * Get SpinCount. + * + * @return int + */ + public function getSpinCount() + { + return $this->spinCount; + } + + /** + * Set SpinCount. + * + * @param int $pValue + * + * @return $this + */ + public function setSpinCount($pValue) + { + $this->spinCount = $pValue; + + return $this; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 3d47eeaa..803ade8a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -424,6 +424,22 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('password', $pSheet->getProtection()->getPassword()); } + if ($pSheet->getProtection()->getHashValue() !== '') { + $objWriter->writeAttribute('hashValue', $pSheet->getProtection()->getHashValue()); + } + + if ($pSheet->getProtection()->getAlgorithmName() !== '') { + $objWriter->writeAttribute('algorithmName', $pSheet->getProtection()->getAlgorithmName()); + } + + if ($pSheet->getProtection()->getSaltValue() !== '') { + $objWriter->writeAttribute('saltValue', $pSheet->getProtection()->getSaltValue()); + } + + if ($pSheet->getProtection()->getSpinCount() !== '') { + $objWriter->writeAttribute('spinCount', $pSheet->getProtection()->getSpinCount()); + } + $objWriter->writeAttribute('sheet', ($pSheet->getProtection()->getSheet() ? 'true' : 'false')); $objWriter->writeAttribute('objects', ($pSheet->getProtection()->getObjects() ? 'true' : 'false')); $objWriter->writeAttribute('scenarios', ($pSheet->getProtection()->getScenarios() ? 'true' : 'false')); diff --git a/tests/data/Shared/PasswordHashes.php b/tests/data/Shared/PasswordHashes.php index b4f348ca..34c25cef 100644 --- a/tests/data/Shared/PasswordHashes.php +++ b/tests/data/Shared/PasswordHashes.php @@ -25,4 +25,30 @@ return [ 'CE4B', '', ], + [ + 'O6EXRLpLEDNJDL/AzYtnnA4O4bY=', + '', + 'SHA-1', + ], + [ + 'GYvlIMljDI1Czc4jfWrGaxU5pxl9n5Og0KUzyAfYxwk=', + 'PhpSpreadsheet', + 'SHA-256', + 'Php_salt', + 1000, + ], + [ + 'sSHdxQv9qgpkr4LDT0bYQxM9hOQJFRhJ4D752/NHQtDDR1EVy67NCEW9cPd6oWvCoBGd96MqKpuma1A7pN1nEA==', + 'Mark Baker', + 'SHA-512', + 'Mark_salt', + 10000, + ], + [ + 'r9KVLLCKIYOILvE2rcby+g==', + '!+&=()~§±æþ', + 'MD5', + 'Symbols_salt', + 100000, + ], ]; From 1eaf40be6940a1b639b918a1748a3eafcef080f5 Mon Sep 17 00:00:00 2001 From: Reijn Date: Sun, 24 May 2020 11:26:17 +0300 Subject: [PATCH 007/153] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd24e146..28aed385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292) - Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468) -- Support protection of worksheet by a specific hash algorithm +- Support protection of worksheet by a specific hash algorithm [#1485](https://github.com/PHPOffice/PhpSpreadsheet/pull/1485) ### Fixed From b9a59660d01c83ff8cbb1667fd4fb3d98c47c36d Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 31 May 2020 20:22:23 +0900 Subject: [PATCH 008/153] Password and hash are exclusive As specified in https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/85f5567f-2599-41ad-ae26-8cfab23ce754 password and hashValue are exlusive and thus should be treated transparently with a single API in our model. --- docs/topics/recipes.md | 76 ++++++++--- src/PhpSpreadsheet/Reader/Xlsx.php | 38 ++++-- src/PhpSpreadsheet/Shared/PasswordHasher.php | 106 ++++++---------- src/PhpSpreadsheet/Worksheet/Protection.php | 120 ++++++++---------- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 61 ++++----- .../Worksheet/ProtectionTest.php | 39 ++++++ 6 files changed, 244 insertions(+), 196 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/ProtectionTest.php diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index b0956b6e..9f8282b9 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -919,29 +919,53 @@ disallow inserting rows on a specific sheet, disallow sorting, ... - Cell: offers the option to lock/unlock a cell as well as show/hide the internal formula. +**Make sure you enable worksheet protection if you need any of the +worksheet or cell protection features!** This can be done using the following +code: + +``` php +$spreadsheet->getActiveSheet()->getProtection()->setSheet(true); +``` + +### Document + An example on setting document security: ``` php -$spreadsheet->getSecurity()->setLockWindows(true); -$spreadsheet->getSecurity()->setLockStructure(true); -$spreadsheet->getSecurity()->setWorkbookPassword("PhpSpreadsheet"); +$security = $spreadsheet->getSecurity(); +$security->setLockWindows(true); +$security->setLockStructure(true); +$security->setWorkbookPassword("PhpSpreadsheet"); ``` +### Worksheet + An example on setting worksheet security: ``` php -$spreadsheet->getActiveSheet() - ->getProtection()->setPassword('PhpSpreadsheet'); -$spreadsheet->getActiveSheet() - ->getProtection()->setSheet(true); -$spreadsheet->getActiveSheet() - ->getProtection()->setSort(true); -$spreadsheet->getActiveSheet() - ->getProtection()->setInsertRows(true); -$spreadsheet->getActiveSheet() - ->getProtection()->setFormatCells(true); +$protection = $spreadsheet->getActiveSheet()->getProtection(); +$protection->setPassword('PhpSpreadsheet'); +$protection->setSheet(true); +$protection->setSort(true); +$protection->setInsertRows(true); +$protection->setFormatCells(true); ``` +If writing Xlsx files you can specify the algorithm used to hash the password +before calling `setPassword()` like so: + +```php +$protection = $spreadsheet->getActiveSheet()->getProtection(); +$protection->setAlgorithm(Protection::ALGORITHM_SHA_512); +$protection->setSpinCount(20000); +$protection->setPassword('PhpSpreadsheet'); +``` + +The salt should **not** be set manually and will be automatically generated +when setting a new password. + +### Cell + An example on setting cell security: ``` php @@ -950,14 +974,30 @@ $spreadsheet->getActiveSheet()->getStyle('B1') ->setLocked(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_UNPROTECTED); ``` -**Make sure you enable worksheet protection if you need any of the -worksheet protection features!** This can be done using the following -code: +## Reading protected spreadsheet -``` php -$spreadsheet->getActiveSheet()->getProtection()->setSheet(true); +Spreadsheets that are protected the as described above can always be read by +PhpSpreadsheet. There is no need to know the password or do anything special in +order to read a protected file. + +However if you need to implement a password verification mechanism, you can use the +following helper method: + + +```php +$protection = $spreadsheet->getActiveSheet()->getProtection(); +$allowed = $protection->verify('my password'); + +if ($allowed) { + doSomething(); +} else { + throw new Exception('Incorrect password'); +} ``` +If you need to completely prevent reading a file by any tool, including PhpSpreadsheet, +then you are looking for "encryption", not "protection". + ## Setting data validation on a cell Data validation is a powerful feature of Xlsx. It allows to specify an diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index f9f20bdd..fb7f0645 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -763,17 +763,8 @@ class Xlsx extends BaseReader } } - if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) { - $docSheet->getProtection()->setPassword((string) $xmlSheet->sheetProtection['password'], true); - $docSheet->getProtection()->setAlgorithmName((string) $xmlSheet->sheetProtection['algorithmName']); - $docSheet->getProtection()->setHashValue((string) $xmlSheet->sheetProtection['hashValue']); - $docSheet->getProtection()->setSaltValue((string) $xmlSheet->sheetProtection['saltValue']); - $docSheet->getProtection()->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']); - if ($xmlSheet->protectedRanges->protectedRange) { - foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { - $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true); - } - } + if ($xmlSheet) { + $this->readSheetProtection($docSheet, $xmlSheet); } if ($xmlSheet && $xmlSheet->autoFilter && !$this->readDataOnly) { @@ -2035,4 +2026,29 @@ class Xlsx extends BaseReader return $workbookBasename; } + + private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void + { + if ($this->readDataOnly || !$xmlSheet->sheetProtection) { + return; + } + + $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName']; + $protection = $docSheet->getProtection(); + $protection->setAlgorithm($algorithmName); + + if ($algorithmName) { + $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true); + $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']); + $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']); + } else { + $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true); + } + + if ($xmlSheet->protectedRanges->protectedRange) { + foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { + $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true); + } + } + } } diff --git a/src/PhpSpreadsheet/Shared/PasswordHasher.php b/src/PhpSpreadsheet/Shared/PasswordHasher.php index f3c2d3e1..9fefe88f 100644 --- a/src/PhpSpreadsheet/Shared/PasswordHasher.php +++ b/src/PhpSpreadsheet/Shared/PasswordHasher.php @@ -2,51 +2,39 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use PhpOffice\PhpSpreadsheet\Exception; +use PhpOffice\PhpSpreadsheet\Worksheet\Protection; + class PasswordHasher { - const ALGORITHM_MD2 = 'MD2'; - const ALGORITHM_MD4 = 'MD4'; - const ALGORITHM_MD5 = 'MD5'; - const ALGORITHM_SHA_1 = 'SHA-1'; - const ALGORITHM_SHA_256 = 'SHA-256'; - const ALGORITHM_SHA_384 = 'SHA-384'; - const ALGORITHM_SHA_512 = 'SHA-512'; - const ALGORITHM_RIPEMD_128 = 'RIPEMD-128'; - const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; - const ALGORITHM_WHIRLPOOL = 'WHIRLPOOL'; - /** - * Mapping between algorithm name in Excel and algorithm name in PHP. - * - * @var array + * Get algorithm name for PHP. */ - private static $algorithmArray = [ - self::ALGORITHM_MD2 => 'md2', - self::ALGORITHM_MD4 => 'md4', - self::ALGORITHM_MD5 => 'md5', - self::ALGORITHM_SHA_1 => 'sha1', - self::ALGORITHM_SHA_256 => 'sha256', - self::ALGORITHM_SHA_384 => 'sha384', - self::ALGORITHM_SHA_512 => 'sha512', - self::ALGORITHM_RIPEMD_128 => 'ripemd128', - self::ALGORITHM_RIPEMD_160 => 'ripemd160', - self::ALGORITHM_WHIRLPOOL => 'whirlpool', - ]; - - /** - * Get algorithm from self::$algorithmArray. - * - * @param string $pAlgorithmName - * - * @return string - */ - private static function getAlgorithm($pAlgorithmName) + private static function getAlgorithm(string $algorithmName): string { - if (array_key_exists($pAlgorithmName, self::$algorithmArray)) { - return self::$algorithmArray[$pAlgorithmName]; + if (!$algorithmName) { + return ''; } - return ''; + // Mapping between algorithm name in Excel and algorithm name in PHP + $mapping = [ + Protection::ALGORITHM_MD2 => 'md2', + Protection::ALGORITHM_MD4 => 'md4', + Protection::ALGORITHM_MD5 => 'md5', + Protection::ALGORITHM_SHA_1 => 'sha1', + Protection::ALGORITHM_SHA_256 => 'sha256', + Protection::ALGORITHM_SHA_384 => 'sha384', + Protection::ALGORITHM_SHA_512 => 'sha512', + Protection::ALGORITHM_RIPEMD_128 => 'ripemd128', + Protection::ALGORITHM_RIPEMD_160 => 'ripemd160', + Protection::ALGORITHM_WHIRLPOOL => 'whirlpool', + ]; + + if (array_key_exists($algorithmName, $mapping)) { + return $mapping[$algorithmName]; + } + + throw new Exception('Unsupported password algorithm: ' . $algorithmName); } /** @@ -57,10 +45,8 @@ class PasswordHasher * Spreadsheet_Excel_Writer by Xavier Noguer . * * @param string $pPassword Password to hash - * - * @return string Hashed password */ - public static function defaultHashPassword($pPassword) + private static function defaultHashPassword(string $pPassword): string { $password = 0x0000; $charPos = 1; // char position @@ -87,40 +73,28 @@ class PasswordHasher * * @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f * - * @param string $pPassword Password to hash - * @param string $pAlgorithmName Hash algorithm used to compute the password hash value - * @param string $pSaltValue Pseudorandom string - * @param string $pSpinCount Number of times to iterate on a hash of a password + * @param string $password Password to hash + * @param string $algorithm Hash algorithm used to compute the password hash value + * @param string $salt Pseudorandom string + * @param int $spinCount Number of times to iterate on a hash of a password * * @return string Hashed password */ - public static function hashPassword($pPassword, $pAlgorithmName = '', $pSaltValue = '', $pSpinCount = 10000) + public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string { - $algorithmName = self::getAlgorithm($pAlgorithmName); - if (!$pAlgorithmName) { - return self::defaultHashPassword($pPassword); + $phpAlgorithm = self::getAlgorithm($algorithm); + if (!$phpAlgorithm) { + return self::defaultHashPassword($password); } - $saltValue = base64_decode($pSaltValue); - $password = mb_convert_encoding($pPassword, 'UCS-2LE', 'UTF-8'); + $saltValue = base64_decode($salt); + $encodedPassword = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); - $hashValue = hash($algorithmName, $saltValue . $password, true); - for ($i = 0; $i < $pSpinCount; ++$i) { - $hashValue = hash($algorithmName, $hashValue . pack('L', $i), true); + $hashValue = hash($phpAlgorithm, $saltValue . $encodedPassword, true); + for ($i = 0; $i < $spinCount; ++$i) { + $hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true); } return base64_encode($hashValue); } - - /** - * Create a pseudorandom string. - * - * @param int $pSize Length of the output string in bytes - * - * @return string Pseudorandom string - */ - public static function generateSalt($pSize = 16) - { - return base64_encode(random_bytes($pSize)); - } } diff --git a/src/PhpSpreadsheet/Worksheet/Protection.php b/src/PhpSpreadsheet/Worksheet/Protection.php index 3566e255..81abc0b7 100644 --- a/src/PhpSpreadsheet/Worksheet/Protection.php +++ b/src/PhpSpreadsheet/Worksheet/Protection.php @@ -6,6 +6,17 @@ use PhpOffice\PhpSpreadsheet\Shared\PasswordHasher; class Protection { + const ALGORITHM_MD2 = 'MD2'; + const ALGORITHM_MD4 = 'MD4'; + const ALGORITHM_MD5 = 'MD5'; + const ALGORITHM_SHA_1 = 'SHA-1'; + const ALGORITHM_SHA_256 = 'SHA-256'; + const ALGORITHM_SHA_384 = 'SHA-384'; + const ALGORITHM_SHA_512 = 'SHA-512'; + const ALGORITHM_RIPEMD_128 = 'RIPEMD-128'; + const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; + const ALGORITHM_WHIRLPOOL = 'WHIRLPOOL'; + /** * Sheet. * @@ -119,7 +130,7 @@ class Protection private $selectUnlockedCells = false; /** - * Password. + * Hashed password. * * @var string */ @@ -130,28 +141,28 @@ class Protection * * @var string */ - private $algorithmName = ''; + private $algorithm = ''; /** * Hash value. * * @var string */ - private $hashValue = ''; + private $hash = ''; /** * Salt value. * * @var string */ - private $saltValue = ''; + private $salt = ''; /** * Spin count. * * @var int */ - private $spinCount = ''; + private $spinCount = 10000; /** * Create a new Protection. @@ -570,7 +581,7 @@ class Protection } /** - * Get Password (hashed). + * Get hashed password. * * @return string */ @@ -590,107 +601,84 @@ class Protection public function setPassword($pValue, $pAlreadyHashed = false) { if (!$pAlreadyHashed) { - $pValue = PasswordHasher::hashPassword($pValue); + $salt = $this->generateSalt(); + $this->setSalt($salt); + $pValue = PasswordHasher::hashPassword($pValue, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount()); } + $this->password = $pValue; return $this; } /** - * Get AlgorithmName. - * - * @return string + * Create a pseudorandom string. */ - public function getAlgorithmName() + private function generateSalt(): string { - return $this->algorithmName; + return base64_encode(random_bytes(16)); } /** - * Set AlgorithmName. - * - * @param string $pValue - * - * @return $this + * Get algorithm name. */ - public function setAlgorithmName($pValue) + public function getAlgorithm(): string { - $this->algorithmName = $pValue; - - return $this; + return $this->algorithm; } /** - * Get HashValue. - * - * @return string + * Set algorithm name. */ - public function getHashValue() + public function setAlgorithm(string $algorithm): void { - return $this->hashValue; + $this->algorithm = $algorithm; } /** - * Set HashValue. - * - * @param string $pValue - * - * @return $this + * Get salt value. */ - public function setHashValue($pValue) + public function getSalt(): string { - $this->hashValue = $pValue; - - return $this; + return $this->salt; } /** - * Get SaltValue. - * - * @return string + * Set salt value. */ - public function getSaltValue() + public function setSalt(string $salt): void { - return $this->saltValue; + $this->salt = $salt; } /** - * Set SaltValue. - * - * @param string $pValue - * - * @return $this + * Get spin count. */ - public function setSaltValue($pValue) - { - $this->saltValue = $pValue; - - return $this; - } - - /** - * Get SpinCount. - * - * @return int - */ - public function getSpinCount() + public function getSpinCount(): int { return $this->spinCount; } /** - * Set SpinCount. - * - * @param int $pValue - * - * @return $this + * Set spin count. */ - public function setSpinCount($pValue) + public function setSpinCount(int $spinCount): void { - $this->spinCount = $pValue; + $this->spinCount = $spinCount; + } - return $this; + /** + * Verify that the given non-hashed password can "unlock" the protection. + */ + public function verify(string $password): bool + { + if (!$this->isProtectionEnabled()) { + return true; + } + + $hash = PasswordHasher::hashPassword($password, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount()); + + return $this->getPassword() === $hash; } /** diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 803ade8a..d101bb40 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -420,42 +420,33 @@ class Worksheet extends WriterPart // sheetProtection $objWriter->startElement('sheetProtection'); - if ($pSheet->getProtection()->getPassword() !== '') { - $objWriter->writeAttribute('password', $pSheet->getProtection()->getPassword()); + $protection = $pSheet->getProtection(); + + if ($protection->getAlgorithm()) { + $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm()); + $objWriter->writeAttribute('hashValue', $protection->getPassword()); + $objWriter->writeAttribute('saltValue', $protection->getSalt()); + $objWriter->writeAttribute('spinCount', $protection->getSpinCount()); + } elseif ($protection->getPassword() !== '') { + $objWriter->writeAttribute('password', $protection->getPassword()); } - if ($pSheet->getProtection()->getHashValue() !== '') { - $objWriter->writeAttribute('hashValue', $pSheet->getProtection()->getHashValue()); - } - - if ($pSheet->getProtection()->getAlgorithmName() !== '') { - $objWriter->writeAttribute('algorithmName', $pSheet->getProtection()->getAlgorithmName()); - } - - if ($pSheet->getProtection()->getSaltValue() !== '') { - $objWriter->writeAttribute('saltValue', $pSheet->getProtection()->getSaltValue()); - } - - if ($pSheet->getProtection()->getSpinCount() !== '') { - $objWriter->writeAttribute('spinCount', $pSheet->getProtection()->getSpinCount()); - } - - $objWriter->writeAttribute('sheet', ($pSheet->getProtection()->getSheet() ? 'true' : 'false')); - $objWriter->writeAttribute('objects', ($pSheet->getProtection()->getObjects() ? 'true' : 'false')); - $objWriter->writeAttribute('scenarios', ($pSheet->getProtection()->getScenarios() ? 'true' : 'false')); - $objWriter->writeAttribute('formatCells', ($pSheet->getProtection()->getFormatCells() ? 'true' : 'false')); - $objWriter->writeAttribute('formatColumns', ($pSheet->getProtection()->getFormatColumns() ? 'true' : 'false')); - $objWriter->writeAttribute('formatRows', ($pSheet->getProtection()->getFormatRows() ? 'true' : 'false')); - $objWriter->writeAttribute('insertColumns', ($pSheet->getProtection()->getInsertColumns() ? 'true' : 'false')); - $objWriter->writeAttribute('insertRows', ($pSheet->getProtection()->getInsertRows() ? 'true' : 'false')); - $objWriter->writeAttribute('insertHyperlinks', ($pSheet->getProtection()->getInsertHyperlinks() ? 'true' : 'false')); - $objWriter->writeAttribute('deleteColumns', ($pSheet->getProtection()->getDeleteColumns() ? 'true' : 'false')); - $objWriter->writeAttribute('deleteRows', ($pSheet->getProtection()->getDeleteRows() ? 'true' : 'false')); - $objWriter->writeAttribute('selectLockedCells', ($pSheet->getProtection()->getSelectLockedCells() ? 'true' : 'false')); - $objWriter->writeAttribute('sort', ($pSheet->getProtection()->getSort() ? 'true' : 'false')); - $objWriter->writeAttribute('autoFilter', ($pSheet->getProtection()->getAutoFilter() ? 'true' : 'false')); - $objWriter->writeAttribute('pivotTables', ($pSheet->getProtection()->getPivotTables() ? 'true' : 'false')); - $objWriter->writeAttribute('selectUnlockedCells', ($pSheet->getProtection()->getSelectUnlockedCells() ? 'true' : 'false')); + $objWriter->writeAttribute('sheet', ($protection->getSheet() ? 'true' : 'false')); + $objWriter->writeAttribute('objects', ($protection->getObjects() ? 'true' : 'false')); + $objWriter->writeAttribute('scenarios', ($protection->getScenarios() ? 'true' : 'false')); + $objWriter->writeAttribute('formatCells', ($protection->getFormatCells() ? 'true' : 'false')); + $objWriter->writeAttribute('formatColumns', ($protection->getFormatColumns() ? 'true' : 'false')); + $objWriter->writeAttribute('formatRows', ($protection->getFormatRows() ? 'true' : 'false')); + $objWriter->writeAttribute('insertColumns', ($protection->getInsertColumns() ? 'true' : 'false')); + $objWriter->writeAttribute('insertRows', ($protection->getInsertRows() ? 'true' : 'false')); + $objWriter->writeAttribute('insertHyperlinks', ($protection->getInsertHyperlinks() ? 'true' : 'false')); + $objWriter->writeAttribute('deleteColumns', ($protection->getDeleteColumns() ? 'true' : 'false')); + $objWriter->writeAttribute('deleteRows', ($protection->getDeleteRows() ? 'true' : 'false')); + $objWriter->writeAttribute('selectLockedCells', ($protection->getSelectLockedCells() ? 'true' : 'false')); + $objWriter->writeAttribute('sort', ($protection->getSort() ? 'true' : 'false')); + $objWriter->writeAttribute('autoFilter', ($protection->getAutoFilter() ? 'true' : 'false')); + $objWriter->writeAttribute('pivotTables', ($protection->getPivotTables() ? 'true' : 'false')); + $objWriter->writeAttribute('selectUnlockedCells', ($protection->getSelectUnlockedCells() ? 'true' : 'false')); $objWriter->endElement(); } @@ -1149,7 +1140,7 @@ class Worksheet extends WriterPart $this->getParentWriter()->getOffice2003Compatibility() === false, 'v', ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#') - ? StringHelper::formatNumber($calculatedValue) : '0' + ? StringHelper::formatNumber($calculatedValue) : '0' ); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ProtectionTest.php b/tests/PhpSpreadsheetTests/Worksheet/ProtectionTest.php new file mode 100644 index 00000000..1cc1ed32 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ProtectionTest.php @@ -0,0 +1,39 @@ +verify('foo'), 'non-protected always pass'); + + $protection->setSheet(true); + self::assertFalse($protection->verify('foo'), 'protected will fail'); + + $protection->setPassword('foo', true); + self::assertSame('foo', $protection->getPassword(), 'was not stored as-is, without hashing'); + self::assertFalse($protection->verify('foo'), 'setting already hashed password will not match'); + + $protection->setPassword('foo'); + self::assertSame('CC40', $protection->getPassword(), 'was hashed'); + self::assertTrue($protection->verify('foo'), 'setting non-hashed password will hash it and not match'); + + $protection->setAlgorithm(Protection::ALGORITHM_MD5); + self::assertFalse($protection->verify('foo'), 'changing algorithm will not match anymore'); + + $protection->setPassword('foo'); + $hash1 = $protection->getPassword(); + $protection->setPassword('foo'); + $hash2 = $protection->getPassword(); + + self::assertSame(24, mb_strlen($hash1)); + self::assertSame(24, mb_strlen($hash2)); + self::assertNotSame($hash1, $hash2, 'was hashed with automatic salt'); + self::assertTrue($protection->verify('foo'), 'setting password again, will hash with proper algorithm and will match'); + } +} From dcf3b9860d93b82278c98679d4362dcbb44e6dc3 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 31 May 2020 22:41:05 +0900 Subject: [PATCH 009/153] Code highlight in docs for PhpStorm --- docs/faq.md | 2 +- docs/topics/accessing-cells.md | 46 +++--- docs/topics/architecture.md | 4 +- docs/topics/autofilters.md | 38 ++--- docs/topics/calculation-engine.md | 96 ++++++------ docs/topics/creating-spreadsheet.md | 6 +- docs/topics/memory_saving.md | 2 +- docs/topics/migration-from-PHPExcel.md | 2 +- docs/topics/reading-and-writing-to-file.md | 102 ++++++------- docs/topics/reading-files.md | 36 ++--- docs/topics/recipes.md | 164 ++++++++++----------- docs/topics/settings.md | 4 +- docs/topics/worksheets.md | 14 +- 13 files changed, 258 insertions(+), 258 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 19f5f8fc..ac69e415 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -23,7 +23,7 @@ When you make use of any of the worksheet protection features (e.g. cell range protection, prohibiting deleting rows, ...), make sure you enable worksheet security. This can for example be done like this: -``` php +```php $spreadsheet->getActiveSheet()->getProtection()->setSheet(true); ``` diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md index 4770d721..edb71514 100644 --- a/docs/topics/accessing-cells.md +++ b/docs/topics/accessing-cells.md @@ -8,7 +8,7 @@ topic lists some of the options to access a cell. Setting a cell value by coordinate can be done using the worksheet's `setCellValue()` method. -``` php +```php // Set cell A1 with a string value $spreadsheet->getActiveSheet()->setCellValue('A1', 'PhpSpreadsheet'); @@ -28,7 +28,7 @@ $spreadsheet->getActiveSheet()->setCellValue( Alternatively, you can retrieve the cell object, and then call the cell’s `setValue()` method: -``` php +```php $spreadsheet->getActiveSheet() ->getCell('B8') ->setValue('Some value'); @@ -56,7 +56,7 @@ the cell object will still retain its data values. What does this mean? Consider the following code: -``` +```php $spreadSheet = new Spreadsheet(); $workSheet = $spreadSheet->getActiveSheet(); @@ -74,7 +74,7 @@ $cellA1 = $workSheet->getCell('A1'); echo 'Value: ', $cellA1->getValue(), '; Address: ', $cellA1->getCoordinate(), PHP_EOL; echo 'Value: ', $cellC1->getValue(), '; Address: ', $cellC1->getCoordinate(), PHP_EOL; -``` +``` The call to `getCell('C1')` returns the cell at `C1` containing its value (`3`), together with its link to the collection (used to identify its @@ -153,7 +153,7 @@ was a formula. To do this, you need to "escape" the value by setting it as "quoted text". -``` +```php // Set cell A4 with a formula $spreadsheet->getActiveSheet()->setCellValue( 'A4', @@ -175,7 +175,7 @@ point value), and a number format mask is used to show how that value should be formatted; so if we want to store a date in a cell, we need to calculate the correct Excel timestamp, and set a number format mask. -``` php +```php // Get the current date/time and convert to an Excel date/time $dateTimeNow = time(); $excelDateValue = \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel( $dateTimeNow ); @@ -210,7 +210,7 @@ behaviour. Firstly, you can set the datatype explicitly as a string so that it is not converted to a number. -``` php +```php // Set cell A8 with a numeric value, but tell PhpSpreadsheet it should be treated as a string $spreadsheet->getActiveSheet()->setCellValueExplicit( 'A8', @@ -222,7 +222,7 @@ $spreadsheet->getActiveSheet()->setCellValueExplicit( Alternatively, you can use a number format mask to display the value with leading zeroes. -``` php +```php // Set cell A9 with a numeric value $spreadsheet->getActiveSheet()->setCellValue('A9', 1513789642); // Set a number format mask to display the value as 11 digits with leading zeroes @@ -236,7 +236,7 @@ $spreadsheet->getActiveSheet()->getStyle('A9') With number format masking, you can even break up the digits into groups to make the value more easily readable. -``` php +```php // Set cell A10 with a numeric value $spreadsheet->getActiveSheet()->setCellValue('A10', 1513789642); // Set a number format mask to display the value as 11 digits with leading zeroes @@ -259,7 +259,7 @@ writers (Xlsx and Xls). It is also possible to set a range of cell values in a single call by passing an array of values to the `fromArray()` method. -``` php +```php $arrayData = [ [NULL, 2010, 2011, 2012], ['Q1', 12, 15, 21], @@ -282,7 +282,7 @@ If you pass a 2-d array, then this will be treated as a series of rows and columns. A 1-d array will be treated as a single row, which is particularly useful if you're fetching an array of data from a database. -``` php +```php $rowArray = ['Value1', 'Value2', 'Value3', 'Value4']; $spreadsheet->getActiveSheet() ->fromArray( @@ -299,7 +299,7 @@ If you have a simple 1-d array, and want to write it as a column, then the following will convert it into an appropriately structured 2-d array that can be fed to the `fromArray()` method: -``` php +```php $rowArray = ['Value1', 'Value2', 'Value3', 'Value4']; $columnArray = array_chunk($rowArray, 1); $spreadsheet->getActiveSheet() @@ -319,7 +319,7 @@ To retrieve the value of a cell, the cell should first be retrieved from the worksheet using the `getCell()` method. A cell's value can be read using the `getValue()` method. -``` php +```php // Get the value from cell A1 $cellValue = $spreadsheet->getActiveSheet()->getCell('A1')->getValue(); ``` @@ -331,7 +331,7 @@ value rather than the formula itself, then use the cell's `getCalculatedValue()` method. This is further explained in [the calculation engine](./calculation-engine.md). -``` php +```php // Get the value from cell A4 $cellValue = $spreadsheet->getActiveSheet()->getCell('A4')->getCalculatedValue(); ``` @@ -340,7 +340,7 @@ Alternatively, if you want to see the value with any cell formatting applied (e.g. for a human-readable date or time value), then you can use the cell's `getFormattedValue()` method. -``` php +```php // Get the value from cell A6 $cellValue = $spreadsheet->getActiveSheet()->getCell('A6')->getFormattedValue(); ``` @@ -350,7 +350,7 @@ $cellValue = $spreadsheet->getActiveSheet()->getCell('A6')->getFormattedValue(); Setting a cell value by coordinate can be done using the worksheet's `setCellValueByColumnAndRow()` method. -``` php +```php // Set cell A5 with a string value $spreadsheet->getActiveSheet()->setCellValueByColumnAndRow(1, 5, 'PhpSpreadsheet'); ``` @@ -363,7 +363,7 @@ To retrieve the value of a cell, the cell should first be retrieved from the worksheet using the `getCellByColumnAndRow()` method. A cell’s value can be read again using the following line of code: -``` php +```php // Get the value from cell B5 $cellValue = $spreadsheet->getActiveSheet()->getCellByColumnAndRow(2, 5)->getValue(); ``` @@ -371,7 +371,7 @@ $cellValue = $spreadsheet->getActiveSheet()->getCellByColumnAndRow(2, 5)->getVal If you need the calculated value of a cell, use the following code. This is further explained in [the calculation engine](./calculation-engine.md). -``` php +```php // Get the value from cell A4 $cellValue = $spreadsheet->getActiveSheet()->getCellByColumnAndRow(1, 4)->getCalculatedValue(); ``` @@ -382,7 +382,7 @@ It is also possible to retrieve a range of cell values to an array in a single call using the `toArray()`, `rangeToArray()` or `namedRangeToArray()` methods. -``` php +```php $dataArray = $spreadsheet->getActiveSheet() ->rangeToArray( 'C3:E5', // The worksheet range that we want to retrieve @@ -409,7 +409,7 @@ cells within a row. Below is an example where we read all the values in a worksheet and display them in a table. -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx'); $reader->setReadDataOnly(TRUE); $spreadsheet = $reader->load("test.xlsx"); @@ -456,7 +456,7 @@ loops. Below is an example where we read all the values in a worksheet and display them in a table. -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx'); $reader->setReadDataOnly(TRUE); $spreadsheet = $reader->load("test.xlsx"); @@ -482,7 +482,7 @@ echo '' . PHP_EOL; Alternatively, you can take advantage of PHP's "Perl-style" character incrementors to loop through the cells by coordinate: -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx'); $reader->setReadDataOnly(TRUE); $spreadsheet = $reader->load("test.xlsx"); @@ -528,7 +528,7 @@ dates entered as strings to the correct format, also setting the cell's style information. The following example demonstrates how to set the value binder in PhpSpreadsheet: -``` php +```php /** PhpSpreadsheet */ require_once 'src/Boostrap.php'; diff --git a/docs/topics/architecture.md b/docs/topics/architecture.md index 0295d672..1c544ef7 100644 --- a/docs/topics/architecture.md +++ b/docs/topics/architecture.md @@ -43,7 +43,7 @@ PhpSpreadsheet supports fluent interfaces in most locations. This means that you can easily "chain" calls to specific methods without requiring a new PHP statement. For example, take the following code: -``` php +```php $spreadsheet->getProperties()->setCreator("Maarten Balliauw"); $spreadsheet->getProperties()->setLastModifiedBy("Maarten Balliauw"); $spreadsheet->getProperties()->setTitle("Office 2007 XLSX Test Document"); @@ -55,7 +55,7 @@ $spreadsheet->getProperties()->setCategory("Test result file"); This can be rewritten as: -``` php +```php $spreadsheet->getProperties() ->setCreator("Maarten Balliauw") ->setLastModifiedBy("Maarten Balliauw") diff --git a/docs/topics/autofilters.md b/docs/topics/autofilters.md index 66321ee9..d5a07f8b 100644 --- a/docs/topics/autofilters.md +++ b/docs/topics/autofilters.md @@ -42,7 +42,7 @@ column, such as "Equals a red cell color" or "Larger than 150". To set an autoFilter on a range of cells. -``` php +```php $spreadsheet->getActiveSheet()->setAutoFilter('A1:E20'); ``` @@ -56,7 +56,7 @@ developer to avoid such errors. If you want to set the whole worksheet as an autofilter region -``` php +```php $spreadsheet->getActiveSheet()->setAutoFilter( $spreadsheet->getActiveSheet() ->calculateWorksheetDimension() @@ -74,7 +74,7 @@ will extend this to other formats. To apply a filter expression to an autoFilter range, you first need to identify which column you're going to be applying this filter to. -``` php +```php $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); $columnFilter = $autoFilter->getColumn('C'); ``` @@ -114,7 +114,7 @@ To create a filter expression, we need to start by identifying the filter type. In this case, we're just going to specify that this filter is a standard filter. -``` php +```php $columnFilter->setFilterType( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER ); @@ -127,7 +127,7 @@ When creating a simple filter in PhpSpreadsheet, you only need to specify the values for "checked" columns: you do this by creating a filter rule for each value. -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL, @@ -152,7 +152,7 @@ standard filters are always treated as being joined by an OR condition. If you want to create a filter to select blank cells, you would use: -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL, @@ -170,7 +170,7 @@ within a year, or individual days within each month. DateGroup filters are still applied as a Standard Filter type. -``` php +```php $columnFilter->setFilterType( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER ); @@ -181,7 +181,7 @@ for "checked" columns as an associative array of year. month, day, hour minute and second. To select a year and month, you need to create a DateGroup rule identifying the selected year and month: -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL, @@ -229,7 +229,7 @@ either an AND or an OR. We start by specifying a Filter type, this time a CUSTOMFILTER. -``` php +```php $columnFilter->setFilterType( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER ); @@ -240,7 +240,7 @@ And then define our rules. The following shows a simple wildcard filter to show all column entries beginning with the letter `U`. -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL, @@ -264,7 +264,7 @@ is the \~ itself. To create a "between" condition, we need to define two rules: -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, @@ -289,7 +289,7 @@ This defined two rules, filtering numbers that are `>= -20` OR `<= 20`, so we also need to modify the join condition to reflect AND rather than OR. -``` php +```php $columnFilter->setAndOr( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_COLUMN_ANDOR_AND ); @@ -320,7 +320,7 @@ column at a time. Again, we start by specifying a Filter type, this time a DYNAMICFILTER. -``` php +```php $columnFilter->setFilterType( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER ); @@ -330,7 +330,7 @@ When defining the rule for a dynamic filter, we don't define a value (we can simply set that to NULL) but we do specify the dynamic filter category. -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL, @@ -420,7 +420,7 @@ column at a time. We start by specifying a Filter type, this time a DYNAMICFILTER. -``` php +```php $columnFilter->setFilterType( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER ); @@ -428,7 +428,7 @@ $columnFilter->setFilterType( Then we create the rule: -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT, @@ -444,7 +444,7 @@ This will filter the Top 5 percent of values in the column. To specify the lowest (bottom 2 values), we would specify a rule of: -``` php +```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE, @@ -490,7 +490,7 @@ If you wish to execute your filter from within a script, you need to do this manually. You can do this using the autofilters `showHideRows()` method. -``` php +```php $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); $autoFilter->showHideRows(); ``` @@ -505,7 +505,7 @@ ever row, whether it matches the filter criteria or not. To selectively access only the filtered rows, you need to test each row’s visibility settings. -``` php +```php foreach ($spreadsheet->getActiveSheet()->getRowIterator() as $row) { if ($spreadsheet->getActiveSheet() ->getRowDimension($row->getRowIndex())->getVisible()) { diff --git a/docs/topics/calculation-engine.md b/docs/topics/calculation-engine.md index 779d73e1..d15dbb16 100644 --- a/docs/topics/calculation-engine.md +++ b/docs/topics/calculation-engine.md @@ -13,7 +13,7 @@ evaluates to the sum of values in A1, A2, ..., A10. To calculate a formula, you can call the cell containing the formula’s method `getCalculatedValue()`, for example: -``` php +```php $spreadsheet->getActiveSheet()->getCell('E11')->getCalculatedValue(); ``` @@ -32,7 +32,7 @@ You see that the formula contained in cell E11 is "SUM(E4:E9)". Now, when I write the following line of code, two new product lines are added: -``` php +```php $spreadsheet->getActiveSheet()->insertNewRowBefore(7, 2); ``` @@ -55,7 +55,7 @@ However, there may be times when you don't want this, perhaps you've changed the underlying data and need to re-evaluate the same formula with that new data. -``` +```php Calculation::getInstance($spreadsheet)->disableCalculationCache(); ``` @@ -63,7 +63,7 @@ Will disable calculation caching, and flush the current calculation cache. If you want only to flush the cache, then you can call -``` +```php Calculation::getInstance($spreadsheet)->clearCalculationCache(); ``` @@ -118,7 +118,7 @@ date values by calling the `\PhpOffice\PhpSpreadsheet\Calculation\Functions::setReturnDateType()` method: -``` php +```php \PhpOffice\PhpSpreadsheet\Calculation\Functions::setReturnDateType($returnDateType); ``` @@ -134,7 +134,7 @@ if an invalid value is passed in for the return date type). The `\PhpOffice\PhpSpreadsheet\Calculation\Functions::getReturnDateType()` method can be used to determine the current value of this setting: -``` php +```php $returnDateType = \PhpOffice\PhpSpreadsheet\Calculation\Functions::getReturnDateType(); ``` @@ -172,7 +172,7 @@ It is possible for scripts to change the calendar used for calculating Excel date values by calling the `\PhpOffice\PhpSpreadsheet\Shared\Date::setExcelCalendar()` method: -``` php +```php \PhpOffice\PhpSpreadsheet\Shared\Date::setExcelCalendar($baseDate); ``` @@ -187,7 +187,7 @@ if an invalid value is passed in). The `\PhpOffice\PhpSpreadsheet\Shared\Date::getExcelCalendar()` method can be used to determine the current value of this setting: -``` php +```php $baseDate = \PhpOffice\PhpSpreadsheet\Shared\Date::getExcelCalendar(); ``` @@ -353,7 +353,7 @@ This is the statistical mean. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -421,7 +421,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -492,7 +492,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -563,7 +563,7 @@ in which you specify a condition for the column. #### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -631,7 +631,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -699,7 +699,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -767,7 +767,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -836,7 +836,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -905,7 +905,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -973,7 +973,7 @@ in which you specify a condition for the column. ##### Examples -``` php +```php $database = [ [ 'Tree', 'Height', 'Age', 'Yield', 'Profit' ], [ 'Apple', 18, 20, 14, 105.00 ], @@ -1074,7 +1074,7 @@ or an Excel timestamp value (real), depending on the value of ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Year') ->setCellValue('A2', 'Month') ->setCellValue('A3', 'Day'); @@ -1089,7 +1089,7 @@ $retVal = $worksheet->getCell('D1')->getCalculatedValue(); // $retVal = 1230681600 ``` -``` php +```php // We're going to be calling the same cell calculation multiple times, // and expecting different return values, so disable calculation cacheing \PhpOffice\PhpSpreadsheet\Calculation\Calculation::getInstance()->setCalculationCacheEnabled(FALSE); @@ -1170,7 +1170,7 @@ the third parameter. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Year') ->setCellValue('A2', 'Month') ->setCellValue('A3', 'Day'); @@ -1208,7 +1208,7 @@ $retVal = $worksheet->getCell('D6')->getCalculatedValue(); // $retVal = 30 ``` -``` php +```php $date1 = 1193317015; // PHP timestamp for 25-Oct-2007 $date2 = 1449579415; // PHP timestamp for 8-Dec-2015 @@ -1279,7 +1279,7 @@ or an Excel timestamp value (real), depending on the value of ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Date String'); ->setCellValue('A2', '31-Dec-2008') ->setCellValue('A3', '31/12/2008') @@ -1301,7 +1301,7 @@ $retVal = $worksheet->getCell('B4')->getCalculatedValue(); // $retVal = 39813.0 for all cases ``` -``` php +```php // We're going to be calling the same cell calculation multiple times, // and expecting different return values, so disable calculation cacheing \PhpOffice\PhpSpreadsheet\Calculation\Calculation::getInstance()->setCalculationCacheEnabled(FALSE); @@ -1371,7 +1371,7 @@ This is an integer ranging from 1 to 31. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Date String') ->setCellValue('A2', '31-Dec-2008') ->setCellValue('A3', '14-Feb-2008'); @@ -1386,7 +1386,7 @@ $retVal = $worksheet->getCell('B3')->getCalculatedValue(); // $retVal = 14 ``` -``` php +```php $retVal = call_user_func_array( ['\PhpOffice\PhpSpreadsheet\Calculation\Functions', 'DAYOFMONTH'], ['25-Dec-2008'] @@ -1444,7 +1444,7 @@ day year. ##### Examples -``` php +```php $worksheet->setCellValue('B1', 'Start Date') ->setCellValue('C1', 'End Date') ->setCellValue('A2', 'Year') @@ -1469,7 +1469,7 @@ $retVal = $worksheet->getCell('E4')->getCalculatedValue(); // $retVal = 1557 ``` -``` php +```php $date1 = 37655.0; // Excel timestamp for 25-Oct-2007 $date2 = 39233.0; // Excel timestamp for 8-Dec-2015 @@ -1529,7 +1529,7 @@ or an Excel timestamp value (real), depending on the value of ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Date String') ->setCellValue('A2', '1-Jan-2008') ->setCellValue('A3', '29-Feb-2008'); @@ -1548,7 +1548,7 @@ $retVal = $worksheet->getCell('B3')->getCalculatedValue(); // $retVal = 39141.0 (28-Feb-2007) ``` -``` php +```php \PhpOffice\PhpSpreadsheet\Calculation\Functions::setReturnDateType( \PhpOffice\PhpSpreadsheet\Calculation\Functions::RETURNDATE_EXCEL ); @@ -1602,7 +1602,7 @@ or an Excel timestamp value (real), depending on the value of ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Date String') ->setCellValue('A2', '1-Jan-2000') ->setCellValue('A3', '14-Feb-2009'); @@ -1619,7 +1619,7 @@ $retVal = $worksheet->getCell('B3')->getCalculatedValue(); // $retVal = 39507.0 (29-Feb-2008) ``` -``` php +```php \PhpOffice\PhpSpreadsheet\Calculation\Functions::setReturnDateType( \PhpOffice\PhpSpreadsheet\Calculation\Functions::RETURNDATE_EXCEL ); @@ -1661,7 +1661,7 @@ This is an integer ranging from 0 to 23. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Time String') ->setCellValue('A2', '31-Dec-2008 17:30') ->setCellValue('A3', '14-Feb-2008 4:20 AM') @@ -1681,7 +1681,7 @@ $retVal = $worksheet->getCell('B4')->getCalculatedValue(); // $retVal = 16 ``` -``` php +```php $retVal = call_user_func_array( ['\PhpOffice\PhpSpreadsheet\Calculation\Functions', 'HOUROFDAY'], ['09:30'] @@ -1719,7 +1719,7 @@ This is an integer ranging from 0 to 59. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Time String') ->setCellValue('A2', '31-Dec-2008 17:30') ->setCellValue('A3', '14-Feb-2008 4:20 AM') @@ -1739,7 +1739,7 @@ $retVal = $worksheet->getCell('B4')->getCalculatedValue(); // $retVal = 45 ``` -``` php +```php $retVal = call_user_func_array( ['\PhpOffice\PhpSpreadsheet\Calculation\Functions', 'MINUTE'], ['09:30'] @@ -1777,7 +1777,7 @@ This is an integer ranging from 1 to 12. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Date String'); $worksheet->setCellValue('A2', '31-Dec-2008'); $worksheet->setCellValue('A3', '14-Feb-2008'); @@ -1792,7 +1792,7 @@ $retVal = $worksheet->getCell('B3')->getCalculatedValue(); // $retVal = 2 ``` -``` php +```php $retVal = call_user_func_array( ['\PhpOffice\PhpSpreadsheet\Calculation\Functions', 'MONTHOFYEAR'], ['14-July-2008'] @@ -1847,10 +1847,10 @@ The number of working days between startDate and endDate. ##### Examples -``` php +```php ``` -``` php +```php ``` ##### Notes @@ -1880,10 +1880,10 @@ or an Excel timestamp value (real), depending on the value of ##### Examples -``` php +```php ``` -``` php +```php ``` ##### Notes @@ -1917,7 +1917,7 @@ This is an integer ranging from 0 to 59. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Time String') ->setCellValue('A2', '31-Dec-2008 17:30:20') ->setCellValue('A3', '14-Feb-2008 4:20 AM') @@ -1937,7 +1937,7 @@ $retVal = $worksheet->getCell('B4')->getCalculatedValue(); // $retVal = 59 ``` -``` php +```php $retVal = call_user_func_array( ['\PhpOffice\PhpSpreadsheet\Calculation\Functions', 'SECOND'], ['09:30:17'] @@ -2002,7 +2002,7 @@ value of method. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Date String') ->setCellValue('A2', '31-Dec-2008') ->setCellValue('A3', '14-Feb-2008'); @@ -2021,7 +2021,7 @@ $retVal = $worksheet->getCell('B4')->getCalculatedValue(); // $retVal = 2 ``` -``` php +```php $retVal = call_user_func_array( ['\PhpOffice\PhpSpreadsheet\Calculation\Functions', 'WEEKDAY'], ['14-July-2008'] @@ -2066,7 +2066,7 @@ This is an integer year value. ##### Examples -``` php +```php $worksheet->setCellValue('A1', 'Date String') ->setCellValue('A2', '17-Jul-1982') ->setCellValue('A3', '16-Apr-2009'); @@ -2081,7 +2081,7 @@ $retVal = $worksheet->getCell('B3')->getCalculatedValue(); // $retVal = 2009 ``` -``` php +```php $retVal = call_user_func_array( ['\PhpOffice\PhpSpreadsheet\Calculation\Functions', 'YEAR'], ['14-July-2001'] diff --git a/docs/topics/creating-spreadsheet.md b/docs/topics/creating-spreadsheet.md index dceafe4b..3a82623e 100644 --- a/docs/topics/creating-spreadsheet.md +++ b/docs/topics/creating-spreadsheet.md @@ -20,7 +20,7 @@ Details of the different spreadsheet formats supported, and the options available to read them into a Spreadsheet object are described fully in the [Reading Files](./reading-files.md) document. -``` php +```php $inputFileName = './sampleData/example1.xls'; /** Load $inputFileName to a Spreadsheet object **/ @@ -32,7 +32,7 @@ $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($inputFileName); If you want to create a new workbook, rather than load one from file, then you simply need to instantiate it as a new Spreadsheet object. -``` php +```php /** Create a new Spreadsheet Object **/ $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); ``` @@ -53,7 +53,7 @@ then you also need to "break" these cyclic references before doing so. PhpSpreadsheet provides the `disconnectWorksheets()` method for this purpose. -``` php +```php $spreadsheet->disconnectWorksheets(); unset($spreadsheet); ``` diff --git a/docs/topics/memory_saving.md b/docs/topics/memory_saving.md index 4c9a848f..157bb704 100644 --- a/docs/topics/memory_saving.md +++ b/docs/topics/memory_saving.md @@ -16,7 +16,7 @@ cache usages. To enable cell caching, you must provide your own implementation of cache like so: -``` php +```php $cache = new MyCustomPsr16Implementation(); \PhpOffice\PhpSpreadsheet\Settings::setCache($cache); diff --git a/docs/topics/migration-from-PHPExcel.md b/docs/topics/migration-from-PHPExcel.md index cc469768..6dc14f1f 100644 --- a/docs/topics/migration-from-PHPExcel.md +++ b/docs/topics/migration-from-PHPExcel.md @@ -12,7 +12,7 @@ need to be done. automatically your codebase. Assuming your files to be migrated lives in `src/`, you can run the migration like so: -``` sh +```sh composer require rector/rector --dev vendor/bin/rector process src --set phpexcel-to-phpspreadsheet composer remove rector/rector diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 8f92e1f2..020e0634 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -33,7 +33,7 @@ You can create a `\PhpOffice\PhpSpreadsheet\Reader\IReader` instance using `\PhpOffice\PhpSpreadsheet\IOFactory` in automatic file type resolving mode using the following code sample: -``` php +```php $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load("05featuredemo.xlsx"); ``` @@ -45,7 +45,7 @@ If you need to set some properties on the reader, (e.g. to only read data, see more about this later), then you may instead want to use this variant: -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile("05featuredemo.xlsx"); $reader->setReadDataOnly(true); $reader->load("05featuredemo.xlsx"); @@ -55,7 +55,7 @@ You can create a `\PhpOffice\PhpSpreadsheet\Reader\IReader` instance using `\PhpOffice\PhpSpreadsheet\IOFactory` in explicit mode using the following code sample: -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader("Xlsx"); $spreadsheet = $reader->load("05featuredemo.xlsx"); ``` @@ -68,7 +68,7 @@ mode. You can create a `\PhpOffice\PhpSpreadsheet\Writer\IWriter` instance using `\PhpOffice\PhpSpreadsheet\IOFactory`: -``` php +```php $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, "Xlsx"); $writer->save("05featuredemo.xlsx"); ``` @@ -84,7 +84,7 @@ outputting the in-memory spreadsheet to a .xlsx file. You can read an .xlsx file using the following code: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet = $reader->load("05featuredemo.xlsx"); ``` @@ -94,7 +94,7 @@ $spreadsheet = $reader->load("05featuredemo.xlsx"); You can set the option setReadDataOnly on the reader, to instruct the reader to ignore styling, data validation, … and just read cell data: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $reader->setReadDataOnly(true); $spreadsheet = $reader->load("05featuredemo.xlsx"); @@ -105,7 +105,7 @@ $spreadsheet = $reader->load("05featuredemo.xlsx"); You can set the option setLoadSheetsOnly on the reader, to instruct the reader to only load the sheets with a given name: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $reader->setLoadSheetsOnly(["Sheet 1", "My special sheet"]); $spreadsheet = $reader->load("05featuredemo.xlsx"); @@ -122,7 +122,7 @@ read using the `\PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter`. The following code will only read row 1 and rows 20 – 30 of any sheet in the Excel file: -``` php +```php class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { public function readCell($column, $row, $worksheetName = '') { @@ -145,7 +145,7 @@ $spreadsheet = $reader->load("06largescale.xlsx"); You can write an .xlsx file using the following code: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); $writer->save("05featuredemo.xlsx"); ``` @@ -156,7 +156,7 @@ By default, this writer pre-calculates all formulas in the spreadsheet. This can be slow on large spreadsheets, and maybe even unwanted. You can however disable formula pre-calculation: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); $writer->setPreCalculateFormulas(false); $writer->save("05featuredemo.xlsx"); @@ -201,7 +201,7 @@ PHP. You can read an .xls file using the following code: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xls(); $spreadsheet = $reader->load("05featuredemo.xls"); ``` @@ -211,7 +211,7 @@ $spreadsheet = $reader->load("05featuredemo.xls"); You can set the option setReadDataOnly on the reader, to instruct the reader to ignore styling, data validation, … and just read cell data: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xls(); $reader->setReadDataOnly(true); $spreadsheet = $reader->load("05featuredemo.xls"); @@ -222,7 +222,7 @@ $spreadsheet = $reader->load("05featuredemo.xls"); You can set the option setLoadSheetsOnly on the reader, to instruct the reader to only load the sheets with a given name: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xls(); $reader->setLoadSheetsOnly(["Sheet 1", "My special sheet"]); $spreadsheet = $reader->load("05featuredemo.xls"); @@ -239,7 +239,7 @@ read using the `\PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter`. The following code will only read row 1 and rows 20 to 30 of any sheet in the Excel file: -``` php +```php class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { public function readCell($column, $row, $worksheetName = '') { @@ -262,7 +262,7 @@ $spreadsheet = $reader->load("06largescale.xls"); You can write an .xls file using the following code: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xls($spreadsheet); $writer->save("05featuredemo.xls"); ``` @@ -282,7 +282,7 @@ spreadsheets via PHP. You can read an Excel 2003 .xml file using the following code: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xml(); $spreadsheet = $reader->load("05featuredemo.xml"); ``` @@ -298,7 +298,7 @@ read using the `\PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter`. The following code will only read row 1 and rows 20 to 30 of any sheet in the Excel file: -``` php +```php class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { public function readCell($column, $row, $worksheetName = '') { @@ -333,7 +333,7 @@ regarding to styling cells and handling large spreadsheets via PHP. You can read an .slk file using the following code: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Slk(); $spreadsheet = $reader->load("05featuredemo.slk"); ``` @@ -349,7 +349,7 @@ read using the `\PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter`. The following code will only read row 1 and rows 20 to 30 of any sheet in the SYLK file: -``` php +```php class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { public function readCell($column, $row, $worksheetName = '') { @@ -378,7 +378,7 @@ Open Office or Libre Office Calc files. You can read an .ods file using the following code: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Ods(); $spreadsheet = $reader->load("05featuredemo.ods"); ``` @@ -394,7 +394,7 @@ read using the `\PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter`. The following code will only read row 1 and rows 20 to 30 of any sheet in the Calc file: -``` php +```php class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { public function readCell($column, $row, $worksheetName = '') { @@ -427,7 +427,7 @@ regarding to styling cells, number formatting, ... You can read a .csv file using the following code: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv(); $spreadsheet = $reader->load("sample.csv"); ``` @@ -449,7 +449,7 @@ were created in Microsoft Office Excel the correct input encoding may rather be Windows-1252 (CP1252). Always make sure that the input encoding is set appropriately. -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv(); $reader->setInputEncoding('CP1252'); $reader->setDelimiter(';'); @@ -464,7 +464,7 @@ $spreadsheet = $reader->load("sample.csv"); CSV files can only contain one worksheet. Therefore, you can specify which sheet to read from CSV: -``` php +```php $reader->setSheetIndex(0); ``` @@ -475,7 +475,7 @@ data into an existing `Spreadsheet` object. The following code loads a CSV file into an existing `$spreadsheet` containing some sheets, and imports onto the 6th sheet: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv(); $reader->setDelimiter(';'); $reader->setEnclosure(''); @@ -490,7 +490,7 @@ $reader->loadIntoExisting("05featuredemo.csv", $spreadsheet); You can write a .csv file using the following code: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Csv($spreadsheet); $writer->save("05featuredemo.csv"); ``` @@ -502,7 +502,7 @@ as a separator. You can instruct `\PhpOffice\PhpSpreadsheet\Writer\Csv` some options before writing a CSV file: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Csv($spreadsheet); $writer->setDelimiter(';'); $writer->setEnclosure(''); @@ -517,7 +517,7 @@ $writer->save("05featuredemo.csv"); CSV files can only contain one worksheet. Therefore, you can specify which sheet to write to CSV: -``` php +```php $writer->setSheetIndex(0); ``` @@ -527,7 +527,7 @@ By default, this writer pre-calculates all formulas in the spreadsheet. This can be slow on large spreadsheets, and maybe even unwanted. You can however disable formula pre-calculation: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Csv($spreadsheet); $writer->setPreCalculateFormulas(false); $writer->save("05featuredemo.csv"); @@ -542,7 +542,7 @@ it should explicitly include a BOM file header; if it doesn't, Excel will not interpret those characters correctly. This can be enabled by using the following code: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Csv($spreadsheet); $writer->setUseBOM(true); $writer->save("05featuredemo.csv"); @@ -560,14 +560,14 @@ to set the characters explicitly as shown below. English users will want to use this before doing the export: -``` php +```php \PhpOffice\PhpSpreadsheet\Shared\StringHelper::setDecimalSeparator('.'); \PhpOffice\PhpSpreadsheet\Shared\StringHelper::setThousandsSeparator(','); ``` German users will want to use the opposite values. -``` php +```php \PhpOffice\PhpSpreadsheet\Shared\StringHelper::setDecimalSeparator(','); \PhpOffice\PhpSpreadsheet\Shared\StringHelper::setThousandsSeparator('.'); ``` @@ -592,7 +592,7 @@ regarding to styling cells, number formatting, ... You can read an .html or .htm file using the following code: -``` php +```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Html(); $spreadsheet = $reader->load("05featuredemo.html"); @@ -610,7 +610,7 @@ first worksheet by default. You can write a .htm file using the following code: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); $writer->save("05featuredemo.htm"); @@ -621,7 +621,7 @@ $writer->save("05featuredemo.htm"); HTML files can contain one or more worksheets. If you want to write all sheets into a single HTML file, use the following code: -``` php +```php $writer->writeAllSheets(); ``` @@ -630,7 +630,7 @@ $writer->writeAllSheets(); HTML files can contain one or more worksheets. Therefore, you can specify which sheet to write to HTML: -``` php +```php $writer->setSheetIndex(0); ``` @@ -639,19 +639,19 @@ $writer->setSheetIndex(0); There might be situations where you want to explicitly set the included images root. For example, instead of: - ``` html + ```html ``` You might want to see: -``` html +```html ``` You can use the following code to achieve this result: -``` php +```php $writer->setImagesRoot('http://www.example.com'); ``` @@ -661,7 +661,7 @@ By default, this writer pre-calculates all formulas in the spreadsheet. This can be slow on large spreadsheets, and maybe even unwanted. You can however disable formula pre-calculation: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); $writer->setPreCalculateFormulas(false); @@ -686,7 +686,7 @@ Supported methods: Here's an example which retrieves all parts independently and merges them into a resulting HTML page: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); $hdr = $writer->generateHTMLHeader(); $sty = $writer->generateStyles(false); // do not write @@ -708,7 +708,7 @@ echo $writer->generateHTMLFooter(); A HTML file can be marked as UTF-8 by writing a BOM file header. This can be enabled by using the following code: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); $writer->setUseBOM(true); @@ -751,7 +751,7 @@ own circumstances. You can instantiate a writer with its specific name, like so: -``` php +```php $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Mpdf'); ``` @@ -759,7 +759,7 @@ Or you can register which writer you are using with a more generic name, so you don't need to remember which library you chose, only that you want to write PDF files: -``` php +```php $class = \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf::class; \PhpOffice\PhpSpreadsheet\IOFactory::registerWriter('Pdf', $class); $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Pdf'); @@ -767,7 +767,7 @@ $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Pdf') Or you can instantiate directly the writer of your choice like so: -``` php +```php $writer = \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf($spreadsheet); ``` @@ -776,7 +776,7 @@ $writer = \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf($spreadsheet); If you need a custom implementation, or custom configuration, of a supported PDF library. You can extends the PDF library, and the PDF writer like so: -``` php +```php class My_Custom_TCPDF extends TCPDF { // ... @@ -802,7 +802,7 @@ class My_Custom_TCPDF_Writer extends \PhpOffice\PhpSpreadsheet\Writer\Pdf\Tcpdf Once you have identified the Renderer that you wish to use for PDF generation, you can write a .pdf file using the following code: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf($spreadsheet); $writer->save("05featuredemo.pdf"); ``` @@ -815,7 +815,7 @@ first worksheet by default. PDF files can contain one or more worksheets. If you want to write all sheets into a single PDF file, use the following code: -``` php +```php $writer->writeAllSheets(); ``` @@ -824,7 +824,7 @@ $writer->writeAllSheets(); PDF files can contain one or more worksheets. Therefore, you can specify which sheet to write to PDF: -``` php +```php $writer->setSheetIndex(0); ``` @@ -834,7 +834,7 @@ By default, this writer pre-calculates all formulas in the spreadsheet. This can be slow on large spreadsheets, and maybe even unwanted. You can however disable formula pre-calculation: -``` php +```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf($spreadsheet); $writer->setPreCalculateFormulas(false); @@ -856,7 +856,7 @@ page setup properties, headers etc. Here is an example how to open a template file, fill in a couple of fields and save it again: -``` php +```php $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('template.xlsx'); $worksheet = $spreadsheet->getActiveSheet(); diff --git a/docs/topics/reading-files.md b/docs/topics/reading-files.md index 779082dc..1451f2ab 100644 --- a/docs/topics/reading-files.md +++ b/docs/topics/reading-files.md @@ -22,7 +22,7 @@ The simplest way to load a workbook file is to let PhpSpreadsheet's IO Factory identify the file type and load it, calling the static `load()` method of the `\PhpOffice\PhpSpreadsheet\IOFactory` class. -``` php +```php $inputFileName = './sampleData/example1.xls'; /** Load $inputFileName to a Spreadsheet Object **/ @@ -59,7 +59,7 @@ supported filetype by name. However, you may get unpredictable results if the file isn't of the right type (e.g. it is a CSV with an extension of .xls), although this type of exception should normally be trapped. -``` php +```php $inputFileName = './sampleData/example1.xls'; /** Create a new Xls Reader **/ @@ -81,7 +81,7 @@ Alternatively, you can use the IO Factory's `createReader()` method to instantiate the reader object for you, simply telling it the file type of the reader that you want instantiating. -``` php +```php $inputFileType = 'Xls'; // $inputFileType = 'Xlsx'; // $inputFileType = 'Xml'; @@ -104,7 +104,7 @@ If you're uncertain of the filetype, you can use the `IOFactory::identify()` method to identify the reader that you need, before using the `createReader()` method to instantiate the reader object. -``` php +```php $inputFileName = './sampleData/example1.xls'; /** Identify the type of $inputFileName **/ @@ -131,7 +131,7 @@ need any of the cell formatting information, then you can set the reader to read only the data values and any formulae from each cell using the `setReadDataOnly()` method. -``` php +```php $inputFileType = 'Xls'; $inputFileName = './sampleData/example1.xls'; @@ -176,7 +176,7 @@ in reading. To read a single sheet, you can pass that sheet name as a parameter to the `setLoadSheetsOnly()` method. -``` php +```php $inputFileType = 'Xls'; $inputFileName = './sampleData/example1.xls'; $sheetname = 'Data Sheet #2'; @@ -195,7 +195,7 @@ for a working example of this code. If you want to read more than just a single sheet, you can pass a list of sheet names as an array parameter to the `setLoadSheetsOnly()` method. -``` php +```php $inputFileType = 'Xls'; $inputFileName = './sampleData/example1.xls'; $sheetnames = ['Data Sheet #1','Data Sheet #3']; @@ -214,7 +214,7 @@ for a working example of this code. To reset this option to the default, you can call the `setLoadAllSheets()` method. -``` php +```php $inputFileType = 'Xls'; $inputFileName = './sampleData/example1.xls'; @@ -248,7 +248,7 @@ should be read by the loader. A read filter must implement the whether a workbook cell identified by those arguments should be read or not. -``` php +```php $inputFileType = 'Xls'; $inputFileName = './sampleData/example1.xls'; $sheetname = 'Data Sheet #3'; @@ -286,7 +286,7 @@ a very specific circumstance (when you only want cells in the range A1:E7 from your worksheet. A generic Read Filter would probably be more useful: -``` php +```php /** Define a Read Filter class implementing \PhpOffice\PhpSpreadsheet\Reader\IReadFilter */ class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { @@ -324,7 +324,7 @@ to read and process a large workbook in "chunks": an example of this usage might be when transferring data from an Excel worksheet to a database. -``` php +```php $inputFileType = 'Xls'; $inputFileName = './sampleData/example2.xls'; @@ -393,7 +393,7 @@ the `setSheetIndex()` method of the `$reader`, then use the `loadIntoExisting()` method rather than the `load()` method to actually read the file into that worksheet. -``` php +```php $inputFileType = 'Csv'; $inputFileNames = [ './sampleData/example1.csv', @@ -452,7 +452,7 @@ Class that we defined in [the above section](#reading-only-specific-columns-and- and the `setSheetIndex()` method of the `$reader`, we can split the CSV file across several individual worksheets. -``` php +```php $inputFileType = 'Csv'; $inputFileName = './sampleData/example2.csv'; @@ -523,7 +523,7 @@ cannot auto-detect, it will default to the comma. If this does not fit your use-case, you can manually specify a separator by using the `setDelimiter()` method. -``` php +```php $inputFileType = 'Csv'; $inputFileName = './sampleData/example1.tsv'; @@ -585,7 +585,7 @@ it encountered a hyperlink, or HTML markup within a CSV file. So using a Value Binder allows a great deal more flexibility in the loader logic when reading unformatted text files. -``` php +```php /** Tell PhpSpreadsheet that we want to use the Advanced Value Binder **/ \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); @@ -619,7 +619,7 @@ manner. The PhpSpreadsheet Readers throw a `\PhpOffice\PhpSpreadsheet\Reader\Exception`. -``` php +```php $inputFileName = './sampleData/example-1.xls'; try { @@ -646,7 +646,7 @@ whole file. The `listWorksheetNames()` method returns a simple array listing each worksheet name within the workbook: -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType); $worksheetNames = $reader->listWorksheetNames($inputFileName); @@ -667,7 +667,7 @@ for a working example of this code. The `listWorksheetInfo()` method returns a nested array, with each entry listing the name and dimensions for a worksheet: -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType); $worksheetData = $reader->listWorksheetInfo($inputFileName); diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 9f8282b9..45dd19bc 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -20,7 +20,7 @@ metadata to search for a specific document in its document lists. Setting spreadsheet metadata is done as follows: -``` php +```php $spreadsheet->getProperties() ->setCreator("Maarten Balliauw") ->setLastModifiedBy("Maarten Balliauw") @@ -38,13 +38,13 @@ $spreadsheet->getProperties() The following line of code sets the active sheet index to the first sheet: -``` php +```php $spreadsheet->setActiveSheetIndex(0); ``` You can also set the active sheet by its name/title -``` php +```php $spreadsheet->setActiveSheetIndexByName('DataSheet') ``` @@ -68,7 +68,7 @@ UST. Writing a date value in a cell consists of 2 lines of code. Select the method that suits you the best. Here are some examples: -``` php +```php // MySQL-like timestamp '2008-12-31' or date string \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); @@ -136,14 +136,14 @@ The following line of code writes the formula formula must start with `=` to make PhpSpreadsheet recognise this as a formula. -``` php +```php $spreadsheet->getActiveSheet()->setCellValue('B8','=IF(C4>500,"profit","loss")'); ``` If you want to write a string beginning with an `=` character to a cell, then you should use the `setCellValueExplicit()` method. -``` php +```php $spreadsheet->getActiveSheet() ->setCellValueExplicit( 'B8', @@ -154,14 +154,14 @@ $spreadsheet->getActiveSheet() A cell's formula can be read again using the following line of code: -``` php +```php $formula = $spreadsheet->getActiveSheet()->getCell('B8')->getValue(); ``` If you need the calculated value of a cell, use the following code. This is further explained in [the calculation engine](./calculation-engine.md). -``` php +```php $value = $spreadsheet->getActiveSheet()->getCell('B8')->getCalculatedValue(); ``` @@ -171,7 +171,7 @@ Some localisation elements have been included in PhpSpreadsheet. You can set a locale by changing the settings. To set the locale to Russian you would use: -``` php +```php $locale = 'ru'; $validLocale = \PhpOffice\PhpSpreadsheet\Settings::setLocale($locale); if (!$validLocale) { @@ -185,7 +185,7 @@ will return an error, and English settings will be used throughout. Once you have set a locale, you can translate a formula from its internal English coding. -``` php +```php $formula = $spreadsheet->getActiveSheet()->getCell('B8')->getValue(); $translatedFormula = \PhpOffice\PhpSpreadsheet\Calculation\Calculation::getInstance()->_translateFormulaToLocale($formula); ``` @@ -194,7 +194,7 @@ You can also create a formula using the function names and argument separators appropriate to the defined locale; then translate it to English before setting the cell value: -``` php +```php $formula = '=ДНЕЙ360(ДАТА(2010;2;5);ДАТА(2010;12;31);ИСТИНА)'; $internalFormula = \PhpOffice\PhpSpreadsheet\Calculation\Calculation::getInstance()->translateFormulaToEnglish($formula); $spreadsheet->getActiveSheet()->setCellValue('B8',$internalFormula); @@ -232,7 +232,7 @@ the cell. Here is how to achieve this in PhpSpreadsheet: -``` php +```php $spreadsheet->getActiveSheet()->getCell('A1')->setValue("hello\nworld"); $spreadsheet->getActiveSheet()->getStyle('A1')->getAlignment()->setWrapText(true); ``` @@ -247,7 +247,7 @@ AdvancedValuebinder.php automatically turns on "wrap text" for the cell when it sees a newline character in a string that you are inserting in a cell. Just like Microsoft Office Excel. Try this: -``` php +```php \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); $spreadsheet->getActiveSheet()->getCell('A1')->setValue("hello\nworld"); @@ -261,7 +261,7 @@ You can set a cell's datatype explicitly by using the cell's setValueExplicit method, or the setCellValueExplicit method of a worksheet. Here's an example: -``` php +```php $spreadsheet->getActiveSheet()->getCell('A1') ->setValueExplicit( '25', @@ -273,7 +273,7 @@ $spreadsheet->getActiveSheet()->getCell('A1') You can make a cell a clickable URL by setting its hyperlink property: -``` php +```php $spreadsheet->getActiveSheet()->setCellValue('E26', 'www.phpexcel.net'); $spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setUrl('https://www.example.com'); ``` @@ -281,7 +281,7 @@ $spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setUrl('https:// If you want to make a hyperlink to another worksheet/cell, use the following code: -``` php +```php $spreadsheet->getActiveSheet()->setCellValue('E26', 'www.phpexcel.net'); $spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setUrl("sheet://'Sheetname'!A1"); ``` @@ -293,7 +293,7 @@ $spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setUrl("sheet:// Setting a worksheet's page orientation and size can be done using the following lines of code: -``` php +```php $spreadsheet->getActiveSheet()->getPageSetup() ->setOrientation(\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE); $spreadsheet->getActiveSheet()->getPageSetup() @@ -324,7 +324,7 @@ setFitToHeight(...) | 1 | setFitToPage(TRUE) | value 0 mean Here is how to fit to 1 page wide by infinite pages tall: -``` php +```php $spreadsheet->getActiveSheet()->getPageSetup()->setFitToWidth(1); $spreadsheet->getActiveSheet()->getPageSetup()->setFitToHeight(0); ``` @@ -340,7 +340,7 @@ the initial values. To set page margins for a worksheet, use this code: -``` php +```php $spreadsheet->getActiveSheet()->getPageMargins()->setTop(1); $spreadsheet->getActiveSheet()->getPageMargins()->setRight(0.75); $spreadsheet->getActiveSheet()->getPageMargins()->setLeft(0.75); @@ -356,7 +356,7 @@ Note that the margin values are specified in inches. To center a page horizontally/vertically, you can use the following code: -``` php +```php $spreadsheet->getActiveSheet()->getPageSetup()->setHorizontalCentered(true); $spreadsheet->getActiveSheet()->getPageSetup()->setVerticalCentered(false); ``` @@ -366,7 +366,7 @@ $spreadsheet->getActiveSheet()->getPageSetup()->setVerticalCentered(false); Setting a worksheet's print header and footer can be done using the following lines of code: -``` php +```php $spreadsheet->getActiveSheet()->getHeaderFooter() ->setOddHeader('&C&HPlease treat this document as confidential!'); $spreadsheet->getActiveSheet()->getHeaderFooter() @@ -460,13 +460,13 @@ $spreadsheet->getActiveSheet()->getHeaderFooter()->addImage($drawing, \PhpOffice To set a print break, use the following code, which sets a row break on row 10. -``` php +```php $spreadsheet->getActiveSheet()->setBreak('A10', \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_ROW); ``` The following line of code sets a print break on column D: -``` php +```php $spreadsheet->getActiveSheet()->setBreak('D10', \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_COLUMN); ``` @@ -484,7 +484,7 @@ PhpSpreadsheet can repeat specific rows/cells at top/left of a page. The following code is an example of how to repeat row 1 to 5 on each printed page of a specific worksheet: -``` php +```php $spreadsheet->getActiveSheet()->getPageSetup()->setRowsToRepeatAtTopByStartAndEnd(1, 5); ``` @@ -492,13 +492,13 @@ $spreadsheet->getActiveSheet()->getPageSetup()->setRowsToRepeatAtTopByStartAndEn To specify a worksheet's printing area, use the following code: -``` php +```php $spreadsheet->getActiveSheet()->getPageSetup()->setPrintArea('A1:E5'); ``` There can also be multiple printing areas in a single worksheet: -``` php +```php $spreadsheet->getActiveSheet()->getPageSetup()->setPrintArea('A1:E5,G4:M20'); ``` @@ -511,7 +511,7 @@ For example, one can set the foreground colour of a cell to red, aligned to the right, and the border to black and thick border style. Let's do that on cell B2: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('B2') ->getFont()->getColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_RED); $spreadsheet->getActiveSheet()->getStyle('B2') @@ -533,7 +533,7 @@ $spreadsheet->getActiveSheet()->getStyle('B2') `getStyle()` also accepts a cell range as a parameter. For example, you can set a red background color on a range of cells: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('B3:B7')->getFill() ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID) ->getStartColor()->setARGB('FFFF0000'); @@ -548,7 +548,7 @@ There is also an alternative manner to set styles. The following code sets a cell's style to font bold, alignment right, top border thin and a gradient fill: -``` php +```php $styleArray = [ 'font' => [ 'bold' => true, @@ -578,7 +578,7 @@ $spreadsheet->getActiveSheet()->getStyle('A3')->applyFromArray($styleArray); Or with a range of cells: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('B3:B7')->applyFromArray($styleArray); ``` @@ -602,7 +602,7 @@ number format code unless you need a custom number format. In PhpSpreadsheet, you can also apply various predefined number formats. Example: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('A1')->getNumberFormat() ->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); ``` @@ -614,7 +614,7 @@ up as 1.587,20) You can achieve exactly the same as the above by using this: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('A1')->getNumberFormat() ->setFormatCode('#,##0.00'); ``` @@ -623,7 +623,7 @@ In Microsoft Office Excel, as well as in PhpSpreadsheet, you will have to interact with raw number format codes whenever you need some special custom number format. Example: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('A1')->getNumberFormat() ->setFormatCode('[Blue][>=3000]$#,##0;[Red][<0]$#,##0;$#,##0'); ``` @@ -631,7 +631,7 @@ $spreadsheet->getActiveSheet()->getStyle('A1')->getNumberFormat() Another example is when you want numbers zero-padded with leading zeros to a fixed length: -``` php +```php $spreadsheet->getActiveSheet()->getCell('A1')->setValue(19); $spreadsheet->getActiveSheet()->getStyle('A1')->getNumberFormat() ->setFormatCode('0000'); // will show as 0019 in Excel @@ -646,7 +646,7 @@ The readers shipped with PhpSpreadsheet come to the rescue. Load your template workbook using e.g. Xlsx reader to reveal the number format code. Example how read a number format code for cell A1: -``` php +```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load('template.xlsx'); var_dump($spreadsheet->getActiveSheet()->getStyle('A1')->getNumberFormat()->getFormatCode()); @@ -661,14 +661,14 @@ code in *xl/styles.xml*. Let's set vertical alignment to the top for cells A1:D4 -``` php +```php $spreadsheet->getActiveSheet()->getStyle('A1:D4') ->getAlignment()->setVertical(\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_TOP); ``` Here is how to achieve wrap text: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('A1:D4') ->getAlignment()->setWrapText(true); ``` @@ -678,7 +678,7 @@ $spreadsheet->getActiveSheet()->getStyle('A1:D4') It is possible to set the default style of a workbook. Let's set the default font to Arial size 8: -``` php +```php $spreadsheet->getDefaultStyle()->getFont()->setName('Arial'); $spreadsheet->getDefaultStyle()->getFont()->setSize(8); ``` @@ -689,7 +689,7 @@ In PhpSpreadsheet it is easy to apply various borders on a rectangular selection. Here is how to apply a thick red border outline around cells B2:G8. -``` php +```php $styleArray = [ 'borders' => [ 'outline' => [ @@ -839,7 +839,7 @@ is below zero, and to green if its value is zero or more. One can set a conditional style ruleset to a cell using the following code: -``` php +```php $conditional1 = new \PhpOffice\PhpSpreadsheet\Style\Conditional(); $conditional1->setConditionType(\PhpOffice\PhpSpreadsheet\Style\Conditional::CONDITION_CELLIS); $conditional1->setOperatorType(\PhpOffice\PhpSpreadsheet\Style\Conditional::OPERATOR_LESSTHAN); @@ -864,7 +864,7 @@ $spreadsheet->getActiveSheet()->getStyle('B2')->setConditionalStyles($conditiona If you want to copy the ruleset to other cells, you can duplicate the style object: -``` php +```php $spreadsheet->getActiveSheet() ->duplicateStyle( $spreadsheet->getActiveSheet()->getStyle('B2'), @@ -877,7 +877,7 @@ $spreadsheet->getActiveSheet() To add a comment to a cell, use the following code. The example below adds a comment to cell E11: -``` php +```php $spreadsheet->getActiveSheet() ->getComment('E11') ->setAuthor('Mark Baker'); @@ -899,7 +899,7 @@ $spreadsheet->getActiveSheet() To apply an autofilter to a range of cells, use the following code: -``` php +```php $spreadsheet->getActiveSheet()->setAutoFilter('A1:C9'); ``` @@ -923,7 +923,7 @@ the internal formula. worksheet or cell protection features!** This can be done using the following code: -``` php +```php $spreadsheet->getActiveSheet()->getProtection()->setSheet(true); ``` @@ -931,7 +931,7 @@ $spreadsheet->getActiveSheet()->getProtection()->setSheet(true); An example on setting document security: -``` php +```php $security = $spreadsheet->getSecurity(); $security->setLockWindows(true); $security->setLockStructure(true); @@ -942,7 +942,7 @@ $security->setWorkbookPassword("PhpSpreadsheet"); An example on setting worksheet security: -``` php +```php $protection = $spreadsheet->getActiveSheet()->getProtection(); $protection->setPassword('PhpSpreadsheet'); $protection->setSheet(true); @@ -968,7 +968,7 @@ when setting a new password. An example on setting cell security: -``` php +```php $spreadsheet->getActiveSheet()->getStyle('B1') ->getProtection() ->setLocked(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_UNPROTECTED); @@ -1008,7 +1008,7 @@ filter can be a range (i.e. value must be between 0 and 10), a list The following piece of code only allows numbers between 10 and 20 to be entered in cell B3: -``` php +```php $validation = $spreadsheet->getActiveSheet()->getCell('B3') ->getDataValidation(); $validation->setType( \PhpOffice\PhpSpreadsheet\Cell\DataValidation::TYPE_WHOLE ); @@ -1027,7 +1027,7 @@ $validation->setFormula2(20); The following piece of code only allows an item picked from a list of data to be entered in cell B5: -``` php +```php $validation = $spreadsheet->getActiveSheet()->getCell('B5') ->getDataValidation(); $validation->setType( \PhpOffice\PhpSpreadsheet\Cell\DataValidation::TYPE_LIST ); @@ -1057,7 +1057,7 @@ the item values themselves can contain the comma `,` character itself. If you need data validation on multiple cells, one can clone the ruleset: -``` php +```php $spreadsheet->getActiveSheet()->getCell('B8')->setDataValidation(clone $validation); ``` @@ -1065,7 +1065,7 @@ $spreadsheet->getActiveSheet()->getCell('B8')->setDataValidation(clone $validati A column's width can be set using the following code: -``` php +```php $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(12); ``` @@ -1073,7 +1073,7 @@ If you want PhpSpreadsheet to perform an automatic width calculation, use the following code. PhpSpreadsheet will approximate the column with to the width of the widest column value. -``` php +```php $spreadsheet->getActiveSheet()->getColumnDimension('B')->setAutoSize(true); ``` @@ -1110,7 +1110,7 @@ To set a worksheet's column visibility, you can use the following code. The first line explicitly shows the column C, the second line hides column D. -``` php +```php $spreadsheet->getActiveSheet()->getColumnDimension('C')->setVisible(true); $spreadsheet->getActiveSheet()->getColumnDimension('D')->setVisible(false); ``` @@ -1119,7 +1119,7 @@ $spreadsheet->getActiveSheet()->getColumnDimension('D')->setVisible(false); To group/outline a column, you can use the following code: -``` php +```php $spreadsheet->getActiveSheet()->getColumnDimension('E')->setOutlineLevel(1); ``` @@ -1127,7 +1127,7 @@ You can also collapse the column. Note that you should also set the column invisible, otherwise the collapse will not be visible in Excel 2007. -``` php +```php $spreadsheet->getActiveSheet()->getColumnDimension('E')->setCollapsed(true); $spreadsheet->getActiveSheet()->getColumnDimension('E')->setVisible(false); ``` @@ -1138,7 +1138,7 @@ on collapsing. You can instruct PhpSpreadsheet to add a summary to the right (default), or to the left. The following code adds the summary to the left: -``` php +```php $spreadsheet->getActiveSheet()->setShowSummaryRight(false); ``` @@ -1146,7 +1146,7 @@ $spreadsheet->getActiveSheet()->setShowSummaryRight(false); A row's height can be set using the following code: -``` php +```php $spreadsheet->getActiveSheet()->getRowDimension('10')->setRowHeight(100); ``` @@ -1159,7 +1159,7 @@ of values is between 0 and 409 pts, where 0 pts is a hidden row. To set a worksheet''s row visibility, you can use the following code. The following example hides row number 10. -``` php +```php $spreadsheet->getActiveSheet()->getRowDimension('10')->setVisible(false); ``` @@ -1171,21 +1171,21 @@ AutoFilter range if you save the file. To group/outline a row, you can use the following code: -``` php +```php $spreadsheet->getActiveSheet()->getRowDimension('5')->setOutlineLevel(1); ``` You can also collapse the row. Note that you should also set the row invisible, otherwise the collapse will not be visible in Excel 2007. -``` php +```php $spreadsheet->getActiveSheet()->getRowDimension('5')->setCollapsed(true); $spreadsheet->getActiveSheet()->getRowDimension('5')->setVisible(false); ``` Here's an example which collapses rows 50 to 80: -``` php +```php for ($i = 51; $i <= 80; $i++) { $spreadsheet->getActiveSheet()->setCellValue('A' . $i, "FName $i"); $spreadsheet->getActiveSheet()->setCellValue('B' . $i, "LName $i"); @@ -1202,7 +1202,7 @@ $spreadsheet->getActiveSheet()->getRowDimension(81)->setCollapsed(true); You can instruct PhpSpreadsheet to add a summary below the collapsible rows (default), or above. The following code adds the summary above: -``` php +```php $spreadsheet->getActiveSheet()->setShowSummaryBelow(false); ``` @@ -1212,13 +1212,13 @@ If you have a big piece of data you want to display in a worksheet, you can merge two or more cells together, to become one cell. This can be done using the following code: -``` php +```php $spreadsheet->getActiveSheet()->mergeCells('A18:E22'); ``` Removing a merge can be done using the unmergeCells method: -``` php +```php $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); ``` @@ -1227,7 +1227,7 @@ $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); You can insert/remove rows/columns at a specific position. The following code inserts 2 new rows, right before row 7: -``` php +```php $spreadsheet->getActiveSheet()->insertNewRowBefore(7, 2); ``` @@ -1238,7 +1238,7 @@ to a worksheet. Therefore, you must first instantiate a new `\PhpOffice\PhpSpreadsheet\Worksheet\Drawing`, and assign its properties a meaningful value: -``` php +```php $drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $drawing->setName('Logo'); $drawing->setDescription('Logo'); @@ -1250,13 +1250,13 @@ To add the above drawing to the worksheet, use the following snippet of code. PhpSpreadsheet creates the link between the drawing and the worksheet: -``` php +```php $drawing->setWorksheet($spreadsheet->getActiveSheet()); ``` You can set numerous properties on a drawing, here are some examples: -``` php +```php $drawing->setName('Paid'); $drawing->setDescription('Paid'); $drawing->setPath('./images/paid.png'); @@ -1270,7 +1270,7 @@ $drawing->getShadow()->setDirection(45); You can also add images created using GD functions without needing to save them to disk first as In-Memory drawings. -``` php +```php // Use GD to create an in-memory image $gdImage = @imagecreatetruecolor(120, 20) or die('Cannot Initialize new GD image stream'); $textColor = imagecolorallocate($gdImage, 255, 255, 255); @@ -1298,7 +1298,7 @@ that has been loaded, and save them as individual image files to disk. The following code extracts images from the current active worksheet, and writes each as a separate file. -``` php +```php $i = 0; foreach ($spreadsheet->getActiveSheet()->getDrawingCollection() as $drawing) { if ($drawing instanceof \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing) { @@ -1343,7 +1343,7 @@ creates the following rich text string: > This invoice is ***payable within thirty days after the end of the > month*** unless specified otherwise on the invoice. -``` php +```php $richText = new \PhpOffice\PhpSpreadsheet\RichText\RichText(); $richText->createText('This invoice is '); $payable = $richText->createTextRun('payable within thirty days after the end of the month'); @@ -1359,7 +1359,7 @@ $spreadsheet->getActiveSheet()->getCell('A18')->setValue($richText); PhpSpreadsheet supports the definition of named ranges. These can be defined using the following code: -``` php +```php // Add some data $spreadsheet->setActiveSheetIndex(0); $spreadsheet->getActiveSheet()->setCellValue('A1', 'Firstname:'); @@ -1402,7 +1402,7 @@ your document is needed, it is recommended not to use `php://output`. Example of a script redirecting an Excel 2007 file to the client's browser: -``` php +```php /* Here there will be some code where you create $spreadsheet */ // redirect output to client browser @@ -1416,7 +1416,7 @@ $writer->save('php://output'); Example of a script redirecting an Xls file to the client's browser: -``` php +```php /* Here there will be some code where you create $spreadsheet */ // redirect output to client browser @@ -1444,7 +1444,7 @@ at the client browser, and/or that headers cannot be set by PHP Default column width can be set using the following code: -``` php +```php $spreadsheet->getActiveSheet()->getDefaultColumnDimension()->setWidth(12); ``` @@ -1452,7 +1452,7 @@ $spreadsheet->getActiveSheet()->getDefaultColumnDimension()->setWidth(12); Default row height can be set using the following code: -``` php +```php $spreadsheet->getActiveSheet()->getDefaultRowDimension()->setRowHeight(15); ``` @@ -1465,7 +1465,7 @@ file to a temporary location. Here''s an example which generates an image in memory and adds it to the active worksheet: -``` php +```php // Generate an image $gdImage = @imagecreatetruecolor(120, 20) or die('Cannot Initialize new GD image stream'); $textColor = imagecolorallocate($gdImage, 255, 255, 255); @@ -1486,7 +1486,7 @@ $drawing->setWorksheet($spreadsheet->getActiveSheet()); To set a worksheet's zoom level, the following code can be used: -``` php +```php $spreadsheet->getActiveSheet()->getSheetView()->setZoomScale(75); ``` @@ -1497,7 +1497,7 @@ Note that zoom level should be in range 10 - 400. Sometimes you want to set a color for sheet tab. For example you can have a red sheet tab: -``` php +```php $worksheet->getTabColor()->setRGB('FF0000'); ``` @@ -1505,7 +1505,7 @@ $worksheet->getTabColor()->setRGB('FF0000'); If you need to create more worksheets in the workbook, here is how: -``` php +```php $worksheet1 = $spreadsheet->createSheet(); $worksheet1->setTitle('Another sheet'); ``` @@ -1518,7 +1518,7 @@ worksheets in the workbook. Set a worksheet to be **hidden** using this code: -``` php +```php $spreadsheet->getActiveSheet() ->setSheetState(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN); ``` @@ -1540,7 +1540,7 @@ Worksheets can be set individually whether column `A` should start at left or right side. Default is left. Here is how to set columns from right-to-left. -``` php +```php // right-to-left worksheet $spreadsheet->getActiveSheet()->setRightToLeft(true); ``` diff --git a/docs/topics/settings.md b/docs/topics/settings.md index a9aae9f9..4463ceeb 100644 --- a/docs/topics/settings.md +++ b/docs/topics/settings.md @@ -13,7 +13,7 @@ Read more about [memory saving](./memory_saving.md). To enable cell caching, you must provide your own implementation of cache like so: -``` php +```php $cache = new MyCustomPsr16Implementation(); \PhpOffice\PhpSpreadsheet\Settings::setCache($cache); @@ -25,7 +25,7 @@ Some localisation elements have been included in PhpSpreadsheet. You can set a locale by changing the settings. To set the locale to Brazilian Portuguese you would use: -``` php +```php $locale = 'pt_br'; $validLocale = \PhpOffice\PhpSpreadsheet\Settings::setLocale($locale); if (!$validLocale) { diff --git a/docs/topics/worksheets.md b/docs/topics/worksheets.md index f97a0066..0199f13c 100644 --- a/docs/topics/worksheets.md +++ b/docs/topics/worksheets.md @@ -25,7 +25,7 @@ each worksheet "tab" is shown when the workbook is opened in MS Excel (or other appropriate Spreadsheet program). To access a sheet by its index, use the `getSheet()` method. -``` php +```php // Get the second sheet in the workbook // Note that sheets are indexed from 0 $spreadsheet->getSheet(1); @@ -38,7 +38,7 @@ workbook. To access a sheet by name, use the `getSheetByName()` method, specifying the name of the worksheet that you want to access. -``` php +```php // Retrieve the worksheet called 'Worksheet 1' $spreadsheet->getSheetByName('Worksheet 1'); ``` @@ -48,7 +48,7 @@ and you can access that directly. The currently active worksheet is the one that will be active when the workbook is opened in MS Excel (or other appropriate Spreadsheet program). -``` php +```php // Retrieve the current active worksheet $spreadsheet->getActiveSheet(); ``` @@ -64,7 +64,7 @@ a new "last" sheet; but you can also specify an index position as an argument, and the worksheet will be inserted at that position, shuffling all subsequent worksheets in the collection down a place. -``` php +```php $spreadsheet->createSheet(); ``` @@ -76,7 +76,7 @@ Alternatively, you can instantiate a new worksheet (setting the title to whatever you choose) and then insert it into your workbook using the `addSheet()` method. -``` php +```php // Create a new worksheet called "My Data" $myWorkSheet = new \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet($spreadsheet, 'My Data'); @@ -93,7 +93,7 @@ Sheets within the same workbook can be copied by creating a clone of the worksheet you wish to copy, and then using the `addSheet()` method to insert the clone into the workbook. -``` php +```php $clonedWorksheet = clone $spreadsheet->getSheetByName('Worksheet 1'); $clonedWorksheet->setTitle('Copy of Worksheet 1'); $spreadsheet->addSheet($clonedWorksheet); @@ -117,7 +117,7 @@ duplicate name. You can delete a worksheet from a workbook, identified by its index position, using the `removeSheetByIndex()` method -``` php +```php $sheetIndex = $spreadsheet->getIndex( $spreadsheet->getSheetByName('Worksheet 1') ); From 21bfb5b3243b8ceb9eda499a4d699fc42c11a9d1 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 31 May 2020 22:49:28 +0900 Subject: [PATCH 010/153] 1.13.0 ### Added - Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292) - Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468) - Support protection of worksheet by a specific hash algorithm [#1485](https://github.com/PHPOffice/PhpSpreadsheet/pull/1485) ### Fixed - Fix Chart samples by updating chart parameter from 0 to DataSeries::EMPTY_AS_GAP [#1448](https://github.com/PHPOffice/PhpSpreadsheet/pull/1448) - Fix return type in docblock for the Cells::get() [#1398](https://github.com/PHPOffice/PhpSpreadsheet/pull/1398) - Fix RATE, PRICE, XIRR, and XNPV Functions [#1456](https://github.com/PHPOffice/PhpSpreadsheet/pull/1456) - Save Excel 2010+ functions properly in XLSX [#1461](https://github.com/PHPOffice/PhpSpreadsheet/pull/1461) - Several improvements in HTML writer [#1464](https://github.com/PHPOffice/PhpSpreadsheet/pull/1464) - Fix incorrect behaviour when saving XLSX file with drawings [#1462](https://github.com/PHPOffice/PhpSpreadsheet/pull/1462), - Fix Crash while trying setting a cell the value "123456\n" [#1476](https://github.com/PHPOffice/PhpSpreadsheet/pull/1481) - Improved DATEDIF() function and reduced errors for Y and YM units [#1466](https://github.com/PHPOffice/PhpSpreadsheet/pull/1466) - Stricter typing for mergeCells [#1494](https://github.com/PHPOffice/PhpSpreadsheet/pull/1494) ### Changed - Drop support for PHP 7.1, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support - Drop partial migration tool in favor of complete migration via RectorPHP [#1445](https://github.com/PHPOffice/PhpSpreadsheet/issues/1445) - Limit composer package to `src/` [#1424](https://github.com/PHPOffice/PhpSpreadsheet/pull/1424) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28aed385..0c139850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [1.13.0] - 2020-05-31 ### Added From ac7fb4a31d16cec9ee8313cd7ffe357bc9039447 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 31 May 2020 23:11:40 +0900 Subject: [PATCH 011/153] Generate API doc on master It seems we can't be both on a tag and on master at the same time --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 31ae804e..05e964a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ jobs: - php ocular.phar code-coverage:upload --format=php-clover tests/coverage-clover.xml - stage: API documentations - if: tag is present AND branch = master + if: branch = master php: 7.4 before_script: - curl -LO https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.0.0-rc/phpDocumentor.phar From d7efe9f67151c68eddfdbe9660a26696cb7cc4ea Mon Sep 17 00:00:00 2001 From: Reijn Date: Sun, 31 May 2020 19:03:19 +0300 Subject: [PATCH 012/153] Delete unnecessary class member --- src/PhpSpreadsheet/Worksheet/Protection.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Protection.php b/src/PhpSpreadsheet/Worksheet/Protection.php index 81abc0b7..ba3af0a7 100644 --- a/src/PhpSpreadsheet/Worksheet/Protection.php +++ b/src/PhpSpreadsheet/Worksheet/Protection.php @@ -143,13 +143,6 @@ class Protection */ private $algorithm = ''; - /** - * Hash value. - * - * @var string - */ - private $hash = ''; - /** * Salt value. * From 9ba8db761b71b1d0dea43cac98ec18a60ae1911b Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 10:51:53 -0700 Subject: [PATCH 013/153] Update docs/topics/reading-and-writing-to-file.md Co-authored-by: Adrien Crivelli --- docs/topics/reading-and-writing-to-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index abd7c5f3..aa885f61 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -703,7 +703,7 @@ echo $writer->generateSheetData(); echo $writer->generateHTMLFooter(); ``` -#### Editing HTML During Save Via a Callback +#### Editing HTML during save via a callback You can also add a callback function to edit the generated html before saving. For example, you could add a webfont From 7e87a9f8d8cdd25dfec07ee2a9a12b791183bf09 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 10:54:30 -0700 Subject: [PATCH 014/153] Update samples/Pdf/21a_Pdf.php Co-authored-by: Adrien Crivelli --- samples/Pdf/21a_Pdf.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Pdf/21a_Pdf.php b/samples/Pdf/21a_Pdf.php index c4dc2c48..6ab1938f 100644 --- a/samples/Pdf/21a_Pdf.php +++ b/samples/Pdf/21a_Pdf.php @@ -25,7 +25,7 @@ background-color: yellow; EOF; - return preg_replace('@@', "$newstyle", $html); + return preg_replace('~~', "$newstyle", $html); } $helper->log('Write to Dompdf'); From 32b01148f438d534d09739c62c5a8162cfce1ebf Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 10:56:27 -0700 Subject: [PATCH 015/153] Update src/PhpSpreadsheet/Writer/Html.php Co-authored-by: Adrien Crivelli --- src/PhpSpreadsheet/Writer/Html.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 963b02d7..2796de50 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -197,9 +197,9 @@ class Html extends BaseWriter // Write footer $html .= $this->generateHTMLFooter(); - $cbk = $this->editHtmlCallback; - if ($cbk) { - $html = $cbk($html); + $callback = $this->editHtmlCallback; + if ($callback) { + $html = $callback($html); } Calculation::setArrayReturnType($saveArrayReturnType); From 68002815ec436157e851d863d4968e9b0417f59a Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 10:59:10 -0700 Subject: [PATCH 016/153] Update src/PhpSpreadsheet/Writer/Html.php Co-authored-by: Adrien Crivelli --- src/PhpSpreadsheet/Writer/Html.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 2796de50..1403d9be 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -134,9 +134,9 @@ class Html extends BaseWriter /** * Callback for editing generated html. * - * @var callable + * @var null|callable */ - protected $editHtmlCallback = ''; + protected $editHtmlCallback = null; /** * Create a new HTML. From f42c3ef5a093571f22922c9e708896313e8109a2 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 10:59:52 -0700 Subject: [PATCH 017/153] Update src/PhpSpreadsheet/Writer/Html.php Co-authored-by: Adrien Crivelli --- src/PhpSpreadsheet/Writer/Html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 1403d9be..fceec678 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -208,7 +208,7 @@ class Html extends BaseWriter return $html; } - public function setEditHtmlCallback(callable $cbk): void + public function setEditHtmlCallback(?callable $cbk): void { $this->editHtmlCallback = $cbk; } From 39eeef5ec658b796a689694a3ea66d95de596910 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 11:01:14 -0700 Subject: [PATCH 018/153] Update src/PhpSpreadsheet/Writer/Html.php Co-authored-by: Adrien Crivelli --- src/PhpSpreadsheet/Writer/Html.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index fceec678..942685f6 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -213,11 +213,6 @@ class Html extends BaseWriter $this->editHtmlCallback = $cbk; } - public function resetEditHtmlCallback(): void - { - $this->editHtmlCallback = ''; - } - const VALIGN_ARR = [ Alignment::VERTICAL_BOTTOM => 'bottom', Alignment::VERTICAL_TOP => 'top', From 48c65cff7fe76dc587757ce3e29d482af9055be0 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 11:11:18 -0700 Subject: [PATCH 019/153] Update CallbackTest.php resetEditHtmlCallback was removed per suggestions from PowerKiki --- tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php index f712419c..388dbd0e 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php @@ -33,7 +33,7 @@ EOF; $html1 = $writer->generateHTMLall(); $writer->setEditHtmlCallback([$this, 'yellowBody']); $html2 = $writer->generateHTMLall(); - $writer->resetEditHtmlCallback(); + $writer->setEditHtmlCallback(null); $html3 = $writer->generateHTMLall(); self::assertFalse(strpos($html1, 'background-color: yellow')); From bbaf03c2efec48720c67dbaa8f3b2aab6a112786 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 31 May 2020 11:18:47 -0700 Subject: [PATCH 020/153] Update Html.php Travis says no need to initialize private class variable to null. --- src/PhpSpreadsheet/Writer/Html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 942685f6..752f286f 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -136,7 +136,7 @@ class Html extends BaseWriter * * @var null|callable */ - protected $editHtmlCallback = null; + protected $editHtmlCallback; /** * Create a new HTML. From 5c18bb5798be3c186e89b534de691a20beab21a7 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 2 Jun 2020 07:38:35 +0200 Subject: [PATCH 021/153] Range operator tests (#1501) * Improved handling of named ranges, although there are still some issues (names ranges using a union type with an overlap don't handle the overlap twice, which as the MS Excel approach to set overlaps as opposed to the mathematical approach which only applies overlap values once) * Fix tests that misused space and comma as simple separators in cell ranges --- .../Calculation/Calculation.php | 12 ++- src/PhpSpreadsheet/Cell/Coordinate.php | 73 ++++++++++++----- .../Calculation/Engine/RangeTest.php | 80 +++++++++++++------ .../Cell/CoordinateTest.php | 49 +++++++----- .../CellExtractAllCellReferencesInRange.php | 25 +----- 5 files changed, 151 insertions(+), 88 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 5aa309c5..2d67a134 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3625,7 +3625,6 @@ class Calculation $expectingOperand = false; $val = $match[1]; $length = strlen($val); - if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) { $val = preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function @@ -3660,7 +3659,6 @@ class Calculation } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) { // Watch for this case-change when modifying to allow cell references in different worksheets... // Should only be applied to the actual cell column, not the worksheet name - // If the last entry on the stack was a : operator, then we have a cell range reference $testPrevOp = $stack->last(1); if ($testPrevOp !== null && $testPrevOp['value'] == ':') { @@ -3717,6 +3715,8 @@ class Calculation } $localeConstant = false; + $stackItemType = 'Value'; + $stackItemReference = null; if ($opCharacter == self::FORMULA_STRING_QUOTE) { // UnEscape any quotes within the string $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val))); @@ -3727,12 +3727,17 @@ class Calculation $val = (int) $val; } } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) { + $stackItemType = 'Constant'; $excelConstant = trim(strtoupper($val)); $val = self::$excelConstants[$excelConstant]; } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) { + $stackItemType = 'Constant'; $val = self::$excelConstants[$localeConstant]; + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', $val, $match)) { + $stackItemType = 'Named Range'; + $stackItemReference = $val; } - $details = $stack->getStackItem('Value', $val, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $details = $stack->getStackItem($stackItemType, $val, $stackItemReference, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); if ($localeConstant) { $details['localeValue'] = $localeConstant; } @@ -3842,7 +3847,6 @@ class Calculation $fakedForBranchPruning = []; // help us to know when pruning ['branchTestId' => true/false] $branchStore = []; - // Loop through each token in turn foreach ($tokens as $tokenData) { $token = $tokenData['value']; diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 8c679913..2afeebe9 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -312,32 +312,59 @@ abstract class Coordinate /** * Extract all cell references in range, which may be comprised of multiple cell ranges. * - * @param string $pRange Range (e.g. A1 or A1:C10 or A1:E10 A20:E25) + * @param string $cellRange Range: e.g. 'A1' or 'A1:C10' or 'A1:E10,A20:E25' or 'A1:E5 C3:G7' or 'A1:C1,A3:C3 B1:C3' * * @return array Array containing single cell references */ - public static function extractAllCellReferencesInRange($pRange) + public static function extractAllCellReferencesInRange($cellRange): array { - $returnValue = []; + [$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange); - // Explode spaces - $cellBlocks = self::getCellBlocksFromRangeString($pRange); - foreach ($cellBlocks as $cellBlock) { - $returnValue = array_merge($returnValue, self::getReferencesForCellBlock($cellBlock)); + $cells = []; + foreach ($ranges as $range) { + $cells[] = self::getReferencesForCellBlock($range); } + $cells = self::processRangeSetOperators($operators, $cells); + + if (empty($cells)) { + return []; + } + + $cellList = array_merge(...$cells); + $cellList = self::sortCellReferenceArray($cellList); + + return $cellList; + } + + private static function processRangeSetOperators(array $operators, array $cells): array + { + for ($offset = 0; $offset < count($operators); ++$offset) { + $operator = $operators[$offset]; + if ($operator !== ' ') { + continue; + } + + $cells[$offset] = array_intersect($cells[$offset], $cells[$offset + 1]); + unset($operators[$offset], $cells[$offset + 1]); + $operators = array_values($operators); + $cells = array_values($cells); + --$offset; + } + + return $cells; + } + + private static function sortCellReferenceArray(array $cellList): array + { // Sort the result by column and row $sortKeys = []; - foreach (array_unique($returnValue) as $coord) { - $column = ''; - $row = 0; - - sscanf($coord, '%[A-Z]%d', $column, $row); + foreach ($cellList as $coord) { + [$column, $row] = sscanf($coord, '%[A-Z]%d'); $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord; } ksort($sortKeys); - // Return value return array_values($sortKeys); } @@ -482,15 +509,25 @@ abstract class Coordinate } /** - * Get the individual cell blocks from a range string, splitting by space and removing any $ characters. + * Get the individual cell blocks from a range string, removing any $ characters. + * then splitting by operators and returning an array with ranges and operators. * - * @param string $pRange + * @param string $rangeString * - * @return string[] + * @return array[] */ - private static function getCellBlocksFromRangeString($pRange) + private static function getCellBlocksFromRangeString($rangeString) { - return explode(' ', str_replace('$', '', strtoupper($pRange))); + $rangeString = str_replace('$', '', strtoupper($rangeString)); + + // split range sets on intersection (space) or union (,) operators + $tokens = preg_split('/([ ,])/', $rangeString, -1, PREG_SPLIT_DELIM_CAPTURE); + // separate the range sets and the operators into arrays + $split = array_chunk($tokens, 2); + $ranges = array_column($split, 0); + $operators = array_column($split, 1); + + return [$ranges, $operators]; } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index d1ad229b..ee566db2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -45,11 +45,21 @@ class RangeTest extends TestCase { return[ ['=SUM(A1:B3,A1:C2)', 48], + ['=COUNT(A1:B3,A1:C2)', 12], ['=SUM(A1:B3 A1:C2)', 12], + ['=COUNT(A1:B3 A1:C2)', 4], ['=SUM(A1:A3,C1:C3)', 30], + ['=COUNT(A1:A3,C1:C3)', 6], ['=SUM(A1:A3 C1:C3)', Functions::null()], + ['=COUNT(A1:A3 C1:C3)', 0], ['=SUM(A1:B2,B2:C3)', 40], + ['=COUNT(A1:B2,B2:C3)', 8], ['=SUM(A1:B2 B2:C3)', 5], + ['=COUNT(A1:B2 B2:C3)', 1], + ['=SUM(A1:C1,A3:C3,B1:C3)', 63], + ['=COUNT(A1:C1,A3:C3,B1:C3)', 12], + ['=SUM(A1:C1,A3:C3 B1:C3)', 23], + ['=COUNT(A1:C1,A3:C3 B1:C3)', 5], ]; } @@ -69,35 +79,57 @@ class RangeTest extends TestCase $workSheet->setCellValue('E1', $formula); - $actualRresult = $workSheet->getCell('E1')->getCalculatedValue(); - self::assertSame($expectedResult, $actualRresult); + $sumRresult = $workSheet->getCell('E1')->getCalculatedValue(); + self::assertSame($expectedResult, $sumRresult); } public function providerNamedRangeEvaluation() { return[ - [ - 'A1:B3', - 'A1:C2', - '=SUM(GROUP1,GROUP2)', - 48, - ], - [ - 'A1:B3', - 'A1:C2', - '=SUM(GROUP1 GROUP2)', - 12, - ], - [ - 'A1:B2', - 'B2:C3', - '=SUM(GROUP1,GROUP2)', - 40, - ], - [ - 'A1:B2', - 'B2:C3', - '=SUM(GROUP1 GROUP2)', + ['A1:B3', 'A1:C2', '=SUM(GROUP1,GROUP2)', 48], + ['A1:B3', 'A1:C2', '=COUNT(GROUP1,GROUP2)', 12], + ['A1:B3', 'A1:C2', '=SUM(GROUP1 GROUP2)', 12], + ['A1:B3', 'A1:C2', '=COUNT(GROUP1 GROUP2)', 4], + ['A1:B2', 'B2:C3', '=SUM(GROUP1,GROUP2)', 40], + ['A1:B2', 'B2:C3', '=COUNT(GROUP1,GROUP2)', 8], + ['A1:B2', 'B2:C3', '=SUM(GROUP1 GROUP2)', 5], + ['A1:B2', 'B2:C3', '=COUNT(GROUP1 GROUP2)', 1], + ]; + } + + /** + * @dataProvider providerCompositeNamedRangeEvaluation + * + * @param string $composite + * @param int $expectedSum + * @param int $expectedCount + */ + public function testCompositeNamedRangeEvaluation($composite, $expectedSum, $expectedCount): void + { + $workSheet = $this->spreadSheet->getActiveSheet(); + $this->spreadSheet->addNamedRange(new NamedRange('COMPOSITE', $workSheet, $composite)); + + $workSheet->setCellValue('E1', '=SUM(COMPOSITE)'); + $workSheet->setCellValue('E2', '=COUNT(COMPOSITE)'); + + $actualSum = $workSheet->getCell('E1')->getCalculatedValue(); + self::assertSame($expectedSum, $actualSum); + $actualCount = $workSheet->getCell('E2')->getCalculatedValue(); + self::assertSame($expectedCount, $actualCount); + } + + public function providerCompositeNamedRangeEvaluation() + { + return[ + // Calculation engine doesn't yet handle union ranges with overlap + // 'Union with overlap' => [ + // 'A1:C1,A3:C3,B1:C3', + // 63, + // 12, + // ], + 'Intersection' => [ + 'A1:C1,A3:C3 B1:C3', + 23, 5, ], ]; diff --git a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php index 37579e80..8e0e98a9 100644 --- a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php @@ -83,10 +83,11 @@ class CoordinateTest extends TestCase * @dataProvider providerCoordinates * * @param mixed $expectedResult + * @param string $rangeSet */ - public function testCoordinateFromString($expectedResult, ...$args): void + public function testCoordinateFromString($expectedResult, $rangeSet): void { - $result = Coordinate::coordinateFromString(...$args); + $result = Coordinate::coordinateFromString($rangeSet); self::assertEquals($expectedResult, $result); } @@ -143,11 +144,12 @@ class CoordinateTest extends TestCase /** * @dataProvider providerAbsoluteCoordinates * - * @param mixed $expectedResult + * @param string $expectedResult + * @param string $rangeSet */ - public function testAbsoluteCoordinateFromString($expectedResult, ...$args): void + public function testAbsoluteCoordinateFromString($expectedResult, $rangeSet): void { - $result = Coordinate::absoluteCoordinate(...$args); + $result = Coordinate::absoluteCoordinate($rangeSet); self::assertEquals($expectedResult, $result); } @@ -175,10 +177,11 @@ class CoordinateTest extends TestCase * @dataProvider providerAbsoluteReferences * * @param mixed $expectedResult + * @param string $rangeSet */ - public function testAbsoluteReferenceFromString($expectedResult, ...$args): void + public function testAbsoluteReferenceFromString($expectedResult, $rangeSet): void { - $result = Coordinate::absoluteReference(...$args); + $result = Coordinate::absoluteReference($rangeSet); self::assertEquals($expectedResult, $result); } @@ -206,10 +209,11 @@ class CoordinateTest extends TestCase * @dataProvider providerSplitRange * * @param mixed $expectedResult + * @param string $rangeSet */ - public function testSplitRange($expectedResult, ...$args): void + public function testSplitRange($expectedResult, $rangeSet): void { - $result = Coordinate::splitRange(...$args); + $result = Coordinate::splitRange($rangeSet); foreach ($result as $key => $split) { if (!is_array($expectedResult[$key])) { self::assertEquals($expectedResult[$key], $split[0]); @@ -252,10 +256,11 @@ class CoordinateTest extends TestCase * @dataProvider providerRangeBoundaries * * @param mixed $expectedResult + * @param string $rangeSet */ - public function testRangeBoundaries($expectedResult, ...$args): void + public function testRangeBoundaries($expectedResult, $rangeSet): void { - $result = Coordinate::rangeBoundaries(...$args); + $result = Coordinate::rangeBoundaries($rangeSet); self::assertEquals($expectedResult, $result); } @@ -268,10 +273,11 @@ class CoordinateTest extends TestCase * @dataProvider providerRangeDimension * * @param mixed $expectedResult + * @param string $rangeSet */ - public function testRangeDimension($expectedResult, ...$args): void + public function testRangeDimension($expectedResult, $rangeSet): void { - $result = Coordinate::rangeDimension(...$args); + $result = Coordinate::rangeDimension($rangeSet); self::assertEquals($expectedResult, $result); } @@ -284,10 +290,11 @@ class CoordinateTest extends TestCase * @dataProvider providerGetRangeBoundaries * * @param mixed $expectedResult + * @param string $rangeSet */ - public function testGetRangeBoundaries($expectedResult, ...$args): void + public function testGetRangeBoundaries($expectedResult, $rangeSet): void { - $result = Coordinate::getRangeBoundaries(...$args); + $result = Coordinate::getRangeBoundaries($rangeSet); self::assertEquals($expectedResult, $result); } @@ -299,11 +306,12 @@ class CoordinateTest extends TestCase /** * @dataProvider providerExtractAllCellReferencesInRange * - * @param mixed $expectedResult + * @param array $expectedResult + * @param string $rangeSet */ - public function testExtractAllCellReferencesInRange($expectedResult, ...$args): void + public function testExtractAllCellReferencesInRange($expectedResult, $rangeSet): void { - $result = Coordinate::extractAllCellReferencesInRange(...$args); + $result = Coordinate::extractAllCellReferencesInRange($rangeSet); self::assertEquals($expectedResult, $result); } @@ -350,10 +358,11 @@ class CoordinateTest extends TestCase * @dataProvider providerCoordinateIsRange * * @param mixed $expectedResult + * @param string $rangeSet */ - public function testCoordinateIsRange($expectedResult, ...$args): void + public function testCoordinateIsRange($expectedResult, $rangeSet): void { - $result = Coordinate::coordinateIsRange(...$args); + $result = Coordinate::coordinateIsRange($rangeSet); self::assertEquals($expectedResult, $result); } diff --git a/tests/data/CellExtractAllCellReferencesInRange.php b/tests/data/CellExtractAllCellReferencesInRange.php index cf093289..b005b1fe 100644 --- a/tests/data/CellExtractAllCellReferencesInRange.php +++ b/tests/data/CellExtractAllCellReferencesInRange.php @@ -22,12 +22,6 @@ return [ ], [ [ - 'B4', - 'B5', - 'B6', - 'D4', - 'D5', - 'D6', ], 'B4:B6 D4:D6', ], @@ -66,20 +60,10 @@ return [ ], [ [ - 'B4', - 'B5', - 'B6', - 'C4', 'C5', 'C6', - 'C7', - 'D4', 'D5', 'D6', - 'D7', - 'E5', - 'E6', - 'E7', ], 'B4:D6 C5:E7', ], @@ -105,7 +89,7 @@ return [ 'F5', 'F6', ], - 'B2:D4 C5:D5 E3:E5 D6:E6 F4:F6', + 'B2:D4,C5:D5,E3:E5,D6:E6,F4:F6', ], [ [ @@ -129,16 +113,13 @@ return [ 'F5', 'F6', ], - 'B2:D4 C3:E5 D4:F6', + 'B2:D4,C3:E5,D4:F6', ], [ [ - 'B4', 'B5', - 'B6', - 'B8', ], - 'B4:B6 B8', + 'B4:B6 B5', ], [ [ From 7ab920de5bf96d75298c17434998eebd49f3e0b4 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Wed, 3 Jun 2020 09:13:29 +0900 Subject: [PATCH 022/153] Typo --- docs/topics/recipes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 45dd19bc..f85576a2 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -976,7 +976,7 @@ $spreadsheet->getActiveSheet()->getStyle('B1') ## Reading protected spreadsheet -Spreadsheets that are protected the as described above can always be read by +Spreadsheets that are protected as described above can always be read by PhpSpreadsheet. There is no need to know the password or do anything special in order to read a protected file. From c47b407e3995c27ecc285a2cc09f9a5798fffeac Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Tue, 9 Jun 2020 00:22:22 -0700 Subject: [PATCH 023/153] Different Example for Callback Replace default gridlines with different style. Usable in PDF as well as HTML. Documentation mentioned use of setUseBOM with Html, but that method does not exist, and there is no real reason to support it. Removed it from documentation. --- docs/topics/reading-and-writing-to-file.md | 35 +++++-------------- samples/Basic/17b_Html.php | 13 ++----- samples/Pdf/21a_Pdf.php | 30 +++------------- src/PhpSpreadsheet/Writer/Html.php | 17 ++++----- .../Writer/Html/CallbackTest.php | 2 +- 5 files changed, 22 insertions(+), 75 deletions(-) diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index abd7c5f3..13b62e01 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -703,43 +703,24 @@ echo $writer->generateSheetData(); echo $writer->generateHTMLFooter(); ``` -#### Editing HTML During Save Via a Callback +#### Editing HTML during save via a callback You can also add a callback function to edit the generated html -before saving. For example, you could add a webfont -(not currently supported for Pdf) as follows: +before saving. For example, you could change the gridlines +from a thin solid black line: ``` php -function webfont(string $html): string +function changeGridlines(string $html): string { - $linktag = << - -EOF; - $html = preg_replace('@setEditHtmlCallback('webfont'); +$writer->setEditHtmlCallback('changeGridlines'); $writer->save($filename); ``` -#### Writing UTF-8 HTML files - -A HTML file can be marked as UTF-8 by writing a BOM file header. This -can be enabled by using the following code: - -``` php -$writer = new \PhpOffice\PhpSpreadsheet\Writer\Html($spreadsheet); -$writer->setUseBOM(true); - -$writer->save("05featuredemo.htm"); -``` - #### Decimal and thousands separators See section `\PhpOffice\PhpSpreadsheet\Writer\Csv` how to control the @@ -866,7 +847,7 @@ $writer->setPreCalculateFormulas(false); $writer->save("05featuredemo.pdf"); ``` -#### Editing Pdf During Save Via a Callback +#### Editing Pdf during save via a callback You can also add a callback function to edit the html used to generate the Pdf before saving. diff --git a/samples/Basic/17b_Html.php b/samples/Basic/17b_Html.php index 05649314..97bb29a3 100644 --- a/samples/Basic/17b_Html.php +++ b/samples/Basic/17b_Html.php @@ -8,20 +8,13 @@ $spreadsheet = require __DIR__ . '/../templates/sampleSpreadsheet.php'; $filename = $helper->getFilename(__FILE__, 'html'); $writer = new Html($spreadsheet); -function webfont(string $html): string +function changeGridlines(string $html): string { - $linktag = << - -EOF; - $html = preg_replace('@setEmbedImages(true); -$writer->setEditHtmlCallback('webfont'); +$writer->setEditHtmlCallback('changeGridlines'); $writer->save($filename); $helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Pdf/21a_Pdf.php b/samples/Pdf/21a_Pdf.php index c4dc2c48..b5572afe 100644 --- a/samples/Pdf/21a_Pdf.php +++ b/samples/Pdf/21a_Pdf.php @@ -1,9 +1,7 @@ getActiveSheet()->setShowGridLines(false); $helper->log('Set orientation to landscape'); $spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); +$spreadsheet->setActiveSheetIndex(0)->setPrintGridlines(true); -function yellowBody(string $html): string +function changeGridlines(string $html): string { - $newstyle = << -body { -background-color: yellow; + return str_replace('{border: 1px solid black;}', '{border: 2px dashed red;}', $html); } - - -EOF; - - return preg_replace('@@', "$newstyle", $html); -} - -$helper->log('Write to Dompdf'); -$writer = new Dompdf($spreadsheet); -$filename = $helper->getFileName('21a_Pdf_dompdf.xlsx', 'pdf'); -$writer->setEditHtmlCallback('yellowBody'); -$writer->save($filename); $helper->log('Write to Mpdf'); $writer = new Mpdf($spreadsheet); $filename = $helper->getFileName('21a_Pdf_mpdf.xlsx', 'pdf'); -$writer->setEditHtmlCallback('yellowBody'); -$writer->save($filename); - -$helper->log('Write to Tcpdf'); -$writer = new Tcpdf($spreadsheet); -$filename = $helper->getFileName('21a_Pdf_tcpdf.xlsx', 'pdf'); -$writer->setEditHtmlCallback('yellowBody'); +$writer->setEditHtmlCallback('changeGridlines'); $writer->save($filename); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 963b02d7..752f286f 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -134,9 +134,9 @@ class Html extends BaseWriter /** * Callback for editing generated html. * - * @var callable + * @var null|callable */ - protected $editHtmlCallback = ''; + protected $editHtmlCallback; /** * Create a new HTML. @@ -197,9 +197,9 @@ class Html extends BaseWriter // Write footer $html .= $this->generateHTMLFooter(); - $cbk = $this->editHtmlCallback; - if ($cbk) { - $html = $cbk($html); + $callback = $this->editHtmlCallback; + if ($callback) { + $html = $callback($html); } Calculation::setArrayReturnType($saveArrayReturnType); @@ -208,16 +208,11 @@ class Html extends BaseWriter return $html; } - public function setEditHtmlCallback(callable $cbk): void + public function setEditHtmlCallback(?callable $cbk): void { $this->editHtmlCallback = $cbk; } - public function resetEditHtmlCallback(): void - { - $this->editHtmlCallback = ''; - } - const VALIGN_ARR = [ Alignment::VERTICAL_BOTTOM => 'bottom', Alignment::VERTICAL_TOP => 'top', diff --git a/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php index f712419c..388dbd0e 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php @@ -33,7 +33,7 @@ EOF; $html1 = $writer->generateHTMLall(); $writer->setEditHtmlCallback([$this, 'yellowBody']); $html2 = $writer->generateHTMLall(); - $writer->resetEditHtmlCallback(); + $writer->setEditHtmlCallback(null); $html3 = $writer->generateHTMLall(); self::assertFalse(strpos($html1, 'background-color: yellow')); From 12dd92bafe75d6fa19a19604156a06ae9ebb346c Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 13 Jun 2020 17:35:29 +0200 Subject: [PATCH 024/153] Resolve utf-8 named ranges in calculation engine (#1522) * Resolve use of UTF-8 in defined names in the calculation engine --- CHANGELOG.md | 6 ++++ .../Calculation/Calculation.php | 20 ++++++------- .../Calculation/Engine/RangeTest.php | 30 +++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c139850..37f4f3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] + +### Fixed + +- Resolve evaluation of utf-8 named ranges in calculation engine [#1522](https://github.com/PHPOffice/PhpSpreadsheet/pull/1522) + ## [1.13.0] - 2020-05-31 ### Added diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2d67a134..848f9068 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -24,11 +24,11 @@ class Calculation // Opening bracket const CALCULATION_REGEXP_OPENBRACE = '\('; // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) - const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([A-Z][A-Z0-9\.]*)[\s]*\('; + const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; // Cell reference (cell or range of cells, with or without a sheet reference) const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Named Range of cells - const CALCULATION_REGEXP_NAMEDRANGE = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_A-Z][_A-Z0-9\.]*)'; + const CALCULATION_REGEXP_NAMEDRANGE = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; // Error const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; @@ -3395,7 +3395,7 @@ class Calculation '|' . self::CALCULATION_REGEXP_OPENBRACE . '|' . self::CALCULATION_REGEXP_NAMEDRANGE . '|' . self::CALCULATION_REGEXP_ERROR . - ')/si'; + ')/sui'; // Start with initialisation $index = 0; @@ -3499,7 +3499,7 @@ class Calculation --$parenthesisDepthMap[$pendingStoreKey]; } - if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) { // Did this parenthesis just close a function? + if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) { // Did this parenthesis just close a function? if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) { // we are closing an IF( if ($d['value'] != 'IF(') { @@ -3602,7 +3602,7 @@ class Calculation } // make sure there was a function $d = $stack->last(2); - if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) { + if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) { return $this->raiseFormulaError('Formula Error: Unexpected ,'); } $d = $stack->pop(); @@ -3625,7 +3625,7 @@ class Calculation $expectingOperand = false; $val = $match[1]; $length = strlen($val); - if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) { + if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { $val = preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function $valToUpper = strtoupper($val); @@ -3733,7 +3733,7 @@ class Calculation } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) { $stackItemType = 'Constant'; $val = self::$excelConstants[$localeConstant]; - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', $val, $match)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/miu', $val, $match)) { $stackItemType = 'Named Range'; $stackItemReference = $val; } @@ -3782,7 +3782,7 @@ class Calculation if (($expectingOperator) && ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) && ($output[count($output) - 1]['type'] == 'Cell Reference') || - (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', substr($formula, $index), $match)) && + (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/miu', substr($formula, $index), $match)) && ($output[count($output) - 1]['type'] == 'Named Range' || $output[count($output) - 1]['type'] == 'Value') )) { while ($stack->count() > 0 && @@ -4208,7 +4208,7 @@ class Calculation } // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token, $matches)) { if ($pCellParent) { $pCell->attach($pCellParent); } @@ -4306,7 +4306,7 @@ class Calculation $branchStore[$storeKey] = $token; } // if the token is a named range, push the named range name onto the stack - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/miu', $token, $matches)) { $namedRange = $matches[6]; $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange); diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index ee566db2..e2e6e11d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -97,6 +97,36 @@ class RangeTest extends TestCase ]; } + /** + * @dataProvider providerUTF8NamedRangeEvaluation + * + * @param string $names + * @param string $ranges + * @param string $formula + * @param int $expectedResult + */ + public function testUTF8NamedRangeEvaluation($names, $ranges, $formula, $expectedResult): void + { + $workSheet = $this->spreadSheet->getActiveSheet(); + foreach ($names as $index => $name) { + $range = $ranges[$index]; + $this->spreadSheet->addNamedRange(new NamedRange($name, $workSheet, $range)); + } + $workSheet->setCellValue('E1', $formula); + + $sumRresult = $workSheet->getCell('E1')->getCalculatedValue(); + self::assertSame($expectedResult, $sumRresult); + } + + public function providerUTF8NamedRangeEvaluation() + { + return[ + [['Γειά', 'σου', 'Κόσμε'], ['A1', 'B1:B2', 'C1:C3'], '=SUM(Γειά,σου,Κόσμε)', 26], + [['Γειά', 'σου', 'Κόσμε'], ['A1', 'B1:B2', 'C1:C3'], '=COUNT(Γειά,σου,Κόσμε)', 6], + [['Здравствуй', 'мир'], ['A1:A3', 'C1:C3'], '=SUM(Здравствуй,мир)', 30], + ]; + } + /** * @dataProvider providerCompositeNamedRangeEvaluation * From 82ea1d559647f357b5ce04e60a0bb33ccaa6de77 Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 11:26:02 -0700 Subject: [PATCH 025/153] Fix for #1516 (#1530) This problem is that ZipStream, in contrast to ZipArchive, is saving 2 files with the same path. I have opened an issue with ZipStream, who agree that this appears to be a bug. For the case in question, PhpSpreadsheet is attempting to save a file with the same path twice (and unexpectedly succeeding) because of a clone operation. This fix attempts to rectify the problem by keeping track of all the paths being saved in the zip file, and not attempting to save any duplicate paths. The problem case attempted to save printersettings1.bin twice, but there are other possible exposures, e.g. by cloning a sheet with a drawing.The new test cases clone an existing sample which has both printer settings and drawings. --- src/PhpSpreadsheet/Writer/Xlsx.php | 88 +++++++++------ .../Writer/Xlsx/UnparsedDataCloneTest.php | 101 ++++++++++++++++++ 2 files changed, 154 insertions(+), 35 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index bfcde049..4cb10287 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -107,6 +107,13 @@ class Xlsx extends BaseWriter */ private $drawingHashTable; + /** + * Private handle for zip stream. + * + * @var ZipStream + */ + private $zip; + /** * Create a new Xlsx Writer. */ @@ -173,6 +180,7 @@ class Xlsx extends BaseWriter { if ($this->spreadSheet !== null) { // garbage collect + $this->pathNames = []; $this->spreadSheet->garbageCollect(); $this->openFileHandle($pFilename); @@ -203,73 +211,73 @@ class Xlsx extends BaseWriter $options->setEnableZip64(false); $options->setOutputStream($this->fileHandle); - $zip = new ZipStream(null, $options); + $this->zip = new ZipStream(null, $options); // Add [Content_Types].xml to ZIP file - $zip->addFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts)); + $this->addZipFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts)); //if hasMacros, add the vbaProject.bin file, Certificate file(if exists) if ($this->spreadSheet->hasMacros()) { $macrosCode = $this->spreadSheet->getMacrosCode(); if ($macrosCode !== null) { // we have the code ? - $zip->addFile('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin + $this->addZipFile('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin if ($this->spreadSheet->hasMacrosCertificate()) { //signed macros ? // Yes : add the certificate file and the related rels file - $zip->addFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate()); - $zip->addFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet)); + $this->addZipFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate()); + $this->addZipFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet)); } } } //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels) if ($this->spreadSheet->hasRibbon()) { $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target'); - $zip->addFile($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data')); + $this->addZipFile($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data')); if ($this->spreadSheet->hasRibbonBinObjects()) { $tmpRootPath = dirname($tmpRibbonTarget) . '/'; $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write foreach ($ribbonBinObjects as $aPath => $aContent) { - $zip->addFile($tmpRootPath . $aPath, $aContent); + $this->addZipFile($tmpRootPath . $aPath, $aContent); } //the rels for files - $zip->addFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet)); + $this->addZipFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet)); } } // Add relationships to ZIP file - $zip->addFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet)); - $zip->addFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet)); + $this->addZipFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet)); + $this->addZipFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet)); // Add document properties to ZIP file - $zip->addFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet)); - $zip->addFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet)); + $this->addZipFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet)); + $this->addZipFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet)); $customPropertiesPart = $this->getWriterPart('DocProps')->writeDocPropsCustom($this->spreadSheet); if ($customPropertiesPart !== null) { - $zip->addFile('docProps/custom.xml', $customPropertiesPart); + $this->addZipFile('docProps/custom.xml', $customPropertiesPart); } // Add theme to ZIP file - $zip->addFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet)); + $this->addZipFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet)); // Add string table to ZIP file - $zip->addFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable)); + $this->addZipFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable)); // Add styles to ZIP file - $zip->addFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet)); + $this->addZipFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet)); // Add workbook to ZIP file - $zip->addFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); + $this->addZipFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); $chartCount = 0; // Add worksheets for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { - $zip->addFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); + $this->addZipFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); if ($this->includeCharts) { $charts = $this->spreadSheet->getSheet($i)->getChartCollection(); if (count($charts) > 0) { foreach ($charts as $chart) { - $zip->addFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas)); + $this->addZipFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas)); ++$chartCount; } } @@ -280,19 +288,19 @@ class Xlsx extends BaseWriter // Add worksheet relationships (drawings, ...) for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { // Add relationships - $zip->addFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); + $this->addZipFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); // Add unparsedLoadedData $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); $unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData(); if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) { - $zip->addFile($ctrlProp['filePath'], $ctrlProp['content']); + $this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']); } } if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) { - $zip->addFile($ctrlProp['filePath'], $ctrlProp['content']); + $this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']); } } @@ -305,13 +313,13 @@ class Xlsx extends BaseWriter // Add drawing and image relationship parts if (($drawingCount > 0) || ($chartCount > 0)) { // Drawing relationships - $zip->addFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); + $this->addZipFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); // Drawings - $zip->addFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) { // Drawings - $zip->addFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); } // Add unparsed drawings @@ -320,7 +328,7 @@ class Xlsx extends BaseWriter $drawingFile = array_search($relId, $unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']); if ($drawingFile !== false) { $drawingFile = ltrim($drawingFile, '.'); - $zip->addFile('xl' . $drawingFile, $drawingXml); + $this->addZipFile('xl' . $drawingFile, $drawingXml); } } } @@ -328,30 +336,30 @@ class Xlsx extends BaseWriter // Add comment relationship parts if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) { // VML Comments - $zip->addFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i))); // Comments - $zip->addFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i))); } // Add unparsed relationship parts if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) { - $zip->addFile($vmlDrawing['filePath'], $vmlDrawing['content']); + $this->addZipFile($vmlDrawing['filePath'], $vmlDrawing['content']); } } // Add header/footer relationship parts if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) { // VML Drawings - $zip->addFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); // VML Drawing relationships - $zip->addFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); // Media foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) { - $zip->addFile('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath())); + $this->addZipFile('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath())); } } } @@ -374,7 +382,7 @@ class Xlsx extends BaseWriter $imageContents = file_get_contents($imagePath); } - $zip->addFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + $this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) { ob_start(); call_user_func( @@ -384,7 +392,7 @@ class Xlsx extends BaseWriter $imageContents = ob_get_contents(); ob_end_clean(); - $zip->addFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + $this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); } } @@ -393,7 +401,7 @@ class Xlsx extends BaseWriter // Close file try { - $zip->finish(); + $this->zip->finish(); } catch (OverflowException $e) { throw new WriterException('Could not close resource.'); } @@ -535,4 +543,14 @@ class Xlsx extends BaseWriter return $this; } + + private $pathNames = []; + + private function addZipFile(string $path, string $content): void + { + if (!in_array($path, $this->pathNames)) { + $this->pathNames[] = $path; + $this->zip->addFile($path, $content); + } + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php new file mode 100644 index 00000000..d2afb423 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php @@ -0,0 +1,101 @@ +load($sampleFilename); + $spreadsheet->setActiveSheetIndex(1); + $sheet = $spreadsheet->getActiveSheet(); + $drawings = $sheet->getDrawingCollection(); + self::assertCount(1, $drawings); + $sheetCodeName = $sheet->getCodeName(); + $unparsedLoadedData = $spreadsheet->getUnparsedLoadedData(); + self::assertArrayHasKey('printerSettings', $unparsedLoadedData['sheets'][$sheetCodeName]); + self::assertCount(1, $unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings']); + + $clonedSheet = clone $spreadsheet->getActiveSheet(); + $clonedSheet->setTitle('Clone'); + $spreadsheet->addSheet($clonedSheet); + + $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); + $writer->save($resultFilename); + $dupname = 'Unable to open saved file'; + $zip = zip_open($resultFilename); + if (is_resource($zip)) { + $names = []; + $dupname = ''; + while ($zip_entry = zip_read($zip)) { + $zipname = zip_entry_name($zip_entry); + if (in_array($zipname, $names)) { + $dupname .= "$zipname,"; + } else { + $names[] = $zipname; + } + } + zip_close($zip); + } + unlink($resultFilename); + self::assertEquals('', $dupname); + } + + /** + * Test that saving twice with same writer works. + */ + public function testSaveTwice(): void + { + $sampleFilename = 'tests/data/Writer/XLSX/drawing_on_2nd_page.xlsx'; + $resultFilename1 = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test1'); + $resultFilename2 = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test2'); + self::assertNotEquals($resultFilename1, $resultFilename2); + Settings::setLibXmlLoaderOptions(null); // reset to default options + $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); + $spreadsheet = $reader->load($sampleFilename); + $sheet = $spreadsheet->setActiveSheetIndex(1); + $sheet->setTitle('Original'); + + $clonedSheet = clone $spreadsheet->getActiveSheet(); + $clonedSheet->setTitle('Clone'); + $spreadsheet->addSheet($clonedSheet); + $clonedSheet->getCell('A8')->setValue('cloned'); + $sheet->getCell('A8')->setValue('original'); + + $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); + $writer->save($resultFilename1); + $reader1 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); + $spreadsheet1 = $reader1->load($resultFilename1); + unlink($resultFilename1); + $sheet1c = $spreadsheet1->getSheetByName('Clone'); + $sheet1o = $spreadsheet1->getSheetByName('Original'); + + $writer->save($resultFilename2); + $reader2 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); + $spreadsheet2 = $reader2->load($resultFilename2); + unlink($resultFilename2); + $sheet2c = $spreadsheet2->getSheetByName('Clone'); + $sheet2o = $spreadsheet2->getSheetByName('Original'); + + self::assertEquals($spreadsheet1->getSheetCount(), $spreadsheet2->getSheetCount()); + self::assertCount(1, $sheet1c->getDrawingCollection()); + self::assertCount(1, $sheet1o->getDrawingCollection()); + self::assertCount(1, $sheet2c->getDrawingCollection()); + self::assertCount(1, $sheet2o->getDrawingCollection()); + self::assertEquals('original', $sheet1o->getCell('A8')->getValue()); + self::assertEquals('original', $sheet2o->getCell('A8')->getValue()); + self::assertEquals('cloned', $sheet1c->getCell('A8')->getValue()); + self::assertEquals('cloned', $sheet2c->getCell('A8')->getValue()); + } +} From ce6ac1f0404ae5bd9155faeba4d1f5816b354e34 Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 11:28:57 -0700 Subject: [PATCH 026/153] Fix For #1509 (#1518) * Fix For #1509 User expected no CSV enclosures after $writer->setEnclosure(''), which had been changed to be consistent with $reader->setEnclosure(''). Writer will now omit enclosures after code above; no change to Reader. Tests have been added for this condition. * Add Option to Write CSV Enclosure Only When Required Allowing the user to specify no enclosure when writing a CSV can lead to a situation where PhpSpreadsheet (likewise Excel) will not read the resulting file as intended, e.g. if any cell contains a delimiter character. This is demonstrated in new test TestBadReread. No existing setting will rectify this situation. A better choice would be to add an option to write the enclosure only when it is needed, which is what Excel does. The RFC4180 spec at https://tools.ietf.org/html/rfc4180 states when it is needed - when the cell contains the delimiter, or the enclosure, or a newline. New test TestGoodReread demonstrates that the file is read as intended. The documentation has been updated to describe the new function, and to change the write example where the enclosure is set to null. * Scrutinizer Suggestions 3 minor changes, all in tests. --- docs/topics/reading-and-writing-to-file.md | 18 +- src/PhpSpreadsheet/Writer/Csv.php | 42 +++- .../Writer/Csv/CsvEnclosureTest.php | 216 ++++++++++++++++++ .../Writer/Csv/CsvWriteTest.php | 4 +- 4 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Csv/CsvEnclosureTest.php diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 020e0634..f8ba084b 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -478,7 +478,7 @@ imports onto the 6th sheet: ```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv(); $reader->setDelimiter(';'); -$reader->setEnclosure(''); +$reader->setEnclosure('"'); $reader->setSheetIndex(5); $reader->loadIntoExisting("05featuredemo.csv", $spreadsheet); @@ -505,13 +505,26 @@ file: ```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Csv($spreadsheet); $writer->setDelimiter(';'); -$writer->setEnclosure(''); +$writer->setEnclosure('"'); $writer->setLineEnding("\r\n"); $writer->setSheetIndex(0); $writer->save("05featuredemo.csv"); ``` +#### CSV enclosures + +By default, all CSV fields are wrapped in the enclosure character, +which defaults to double-quote. +You can change to use the enclosure character only when required: + +``` php +$writer = new \PhpOffice\PhpSpreadsheet\Writer\Csv($spreadsheet); +$writer->setEnclosureRequired(false); + +$writer->save("05featuredemo.csv"); +``` + #### Write a specific worksheet CSV files can only contain one worksheet. Therefore, you can specify @@ -538,6 +551,7 @@ $writer->save("05featuredemo.csv"); CSV files are written in UTF-8. If they do not contain characters outside the ASCII range, nothing else need be done. However, if such characters are in the file, +or if the file starts with the 2 characters 'ID', it should explicitly include a BOM file header; if it doesn't, Excel will not interpret those characters correctly. This can be enabled by using the following code: diff --git a/src/PhpSpreadsheet/Writer/Csv.php b/src/PhpSpreadsheet/Writer/Csv.php index 414bdf90..74f28636 100644 --- a/src/PhpSpreadsheet/Writer/Csv.php +++ b/src/PhpSpreadsheet/Writer/Csv.php @@ -168,9 +168,9 @@ class Csv extends BaseWriter * * @return $this */ - public function setEnclosure($pValue) + public function setEnclosure($pValue = '"') { - $this->enclosure = $pValue ? $pValue : '"'; + $this->enclosure = $pValue; return $this; } @@ -296,6 +296,20 @@ class Csv extends BaseWriter return $this; } + private $enclosureRequired = true; + + public function setEnclosureRequired(bool $value): self + { + $this->enclosureRequired = $value; + + return $this; + } + + public function getEnclosureRequired(): bool + { + return $this->enclosureRequired; + } + /** * Write line to CSV file. * @@ -305,24 +319,28 @@ class Csv extends BaseWriter private function writeLine($pFileHandle, array $pValues): void { // No leading delimiter - $writeDelimiter = false; + $delimiter = ''; // Build the line $line = ''; foreach ($pValues as $element) { - // Escape enclosures - $element = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $element); - // Add delimiter - if ($writeDelimiter) { - $line .= $this->delimiter; - } else { - $writeDelimiter = true; + $line .= $delimiter; + $delimiter = $this->delimiter; + // Escape enclosures + $enclosure = $this->enclosure; + if ($enclosure) { + // If enclosure is not required, use enclosure only if + // element contains newline, delimiter, or enclosure. + if (!$this->enclosureRequired && strpbrk($element, "$delimiter$enclosure\n") === false) { + $enclosure = ''; + } else { + $element = str_replace($enclosure, $enclosure . $enclosure, $element); + } } - // Add enclosed string - $line .= $this->enclosure . $element . $this->enclosure; + $line .= $enclosure . $element . $enclosure; } // Add line ending diff --git a/tests/PhpSpreadsheetTests/Writer/Csv/CsvEnclosureTest.php b/tests/PhpSpreadsheetTests/Writer/Csv/CsvEnclosureTest.php new file mode 100644 index 00000000..d048183c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Csv/CsvEnclosureTest.php @@ -0,0 +1,216 @@ + '2020-06-03', + 'B1' => '000123', + 'C1' => '06.53', + 'D1' => '14.22', + 'A2' => '2020-06-04', + 'B2' => '000234', + 'C2' => '07.12', + 'D2' => '15.44', + ]; + + public function testNormalEnclosure(): void + { + $delimiter = ';'; + $enclosure = '"'; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + foreach (self::$cellValues as $key => $value) { + $sheet->setCellValue($key, $value); + } + $writer = new CsvWriter($spreadsheet); + $writer->setDelimiter($delimiter); + $writer->setEnclosure($enclosure); + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($filename); + $filedata = file_get_contents($filename); + $filedata = preg_replace('/\\r?\\n/', $delimiter, $filedata); + $reader = new CsvReader(); + $reader->setDelimiter($delimiter); + $reader->setEnclosure($enclosure); + $newspreadsheet = $reader->load($filename); + unlink($filename); + $sheet = $newspreadsheet->getActiveSheet(); + $expected = ''; + foreach (self::$cellValues as $key => $value) { + self::assertEquals($value, $sheet->getCell($key)->getValue()); + $expected .= "$enclosure$value$enclosure$delimiter"; + } + self::assertEquals($expected, $filedata); + } + + public function testNoEnclosure(): void + { + $delimiter = ';'; + $enclosure = ''; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + foreach (self::$cellValues as $key => $value) { + $sheet->setCellValue($key, $value); + } + $writer = new CsvWriter($spreadsheet); + $writer->setDelimiter($delimiter); + $writer->setEnclosure($enclosure); + self::assertEquals('', $writer->getEnclosure()); + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($filename); + $filedata = file_get_contents($filename); + $filedata = preg_replace('/\\r?\\n/', $delimiter, $filedata); + $reader = new CsvReader(); + $reader->setDelimiter($delimiter); + $reader->setEnclosure($enclosure); + self::assertEquals('"', $reader->getEnclosure()); + $newspreadsheet = $reader->load($filename); + unlink($filename); + $sheet = $newspreadsheet->getActiveSheet(); + $expected = ''; + foreach (self::$cellValues as $key => $value) { + self::assertEquals($value, $sheet->getCell($key)->getValue()); + $expected .= "$enclosure$value$enclosure$delimiter"; + } + self::assertEquals($expected, $filedata); + } + + public function testNotRequiredEnclosure1(): void + { + $delimiter = ';'; + $enclosure = '"'; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + foreach (self::$cellValues as $key => $value) { + $sheet->setCellValue($key, $value); + } + $writer = new CsvWriter($spreadsheet); + self::assertTrue($writer->getEnclosureRequired()); + $writer->setEnclosureRequired(false)->setDelimiter($delimiter)->setEnclosure($enclosure); + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($filename); + $filedata = file_get_contents($filename); + $filedata = preg_replace('/\\r?\\n/', $delimiter, $filedata); + $reader = new CsvReader(); + $reader->setDelimiter($delimiter); + $reader->setEnclosure($enclosure); + $newspreadsheet = $reader->load($filename); + unlink($filename); + $sheet = $newspreadsheet->getActiveSheet(); + $expected = ''; + foreach (self::$cellValues as $key => $value) { + self::assertEquals($value, $sheet->getCell($key)->getValue()); + $expected .= "$value$delimiter"; + } + self::assertEquals($expected, $filedata); + } + + public function testNotRequiredEnclosure2(): void + { + $cellValues2 = [ + 'A1' => '2020-06-03', + 'B1' => 'has,separator', + 'C1' => 'has;non-separator', + 'D1' => 'has"enclosure', + 'A2' => 'has space', + 'B2' => "has\nnewline", + 'C2' => '', + 'D2' => '15.44', + 'A3' => ' leadingspace', + 'B3' => 'trailingspace ', + 'C3' => '=D2*2', + 'D3' => ',leadingcomma', + 'A4' => 'trailingquote"', + 'B4' => 'unused', + 'C4' => 'unused', + 'D4' => 'unused', + ]; + $calcc3 = '30.88'; + $expected1 = '2020-06-03,"has,separator",has;non-separator,"has""enclosure"'; + $expected2 = 'has space,"has' . "\n" . 'newline",,15.44'; + $expected3 = ' leadingspace,trailingspace ,' . $calcc3 . ',",leadingcomma"'; + $expected4 = '"trailingquote""",unused,unused,unused'; + $expectedfile = "$expected1\n$expected2\n$expected3\n$expected4\n"; + $delimiter = ','; + $enclosure = '"'; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + foreach ($cellValues2 as $key => $value) { + $sheet->setCellValue($key, $value); + } + $writer = new CsvWriter($spreadsheet); + self::assertTrue($writer->getEnclosureRequired()); + $writer->setEnclosureRequired(false)->setDelimiter($delimiter)->setEnclosure($enclosure); + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($filename); + $filedata = file_get_contents($filename); + $filedata = preg_replace('/\\r/', '', $filedata); + $reader = new CsvReader(); + $reader->setDelimiter($delimiter); + $reader->setEnclosure($enclosure); + $newspreadsheet = $reader->load($filename); + unlink($filename); + $sheet = $newspreadsheet->getActiveSheet(); + foreach ($cellValues2 as $key => $value) { + self::assertEquals(($key === 'C3') ? $calcc3 : $value, $sheet->getCell($key)->getValue()); + } + self::assertEquals($expectedfile, $filedata); + } + + public function testGoodReread(): void + { + $delimiter = ','; + $enclosure = '"'; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A1', '1'); + $sheet->setCellValue('B1', '2,3'); + $sheet->setCellValue('C1', '4'); + $writer = new CsvWriter($spreadsheet); + $writer->setEnclosureRequired(false)->setDelimiter($delimiter)->setEnclosure($enclosure); + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($filename); + $reader = new CsvReader(); + $reader->setDelimiter($delimiter); + $reader->setEnclosure($enclosure); + $newspreadsheet = $reader->load($filename); + unlink($filename); + $sheet = $newspreadsheet->getActiveSheet(); + self::assertEquals('1', $sheet->getCell('A1')->getValue()); + self::assertEquals('2,3', $sheet->getCell('B1')->getValue()); + self::assertEquals('4', $sheet->getCell('C1')->getValue()); + } + + public function testBadReread(): void + { + $delimiter = ','; + $enclosure = ''; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A1', '1'); + $sheet->setCellValue('B1', '2,3'); + $sheet->setCellValue('C1', '4'); + $writer = new CsvWriter($spreadsheet); + $writer->setDelimiter($delimiter)->setEnclosure($enclosure); + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($filename); + $reader = new CsvReader(); + $reader->setDelimiter($delimiter); + $reader->setEnclosure($enclosure); + $newspreadsheet = $reader->load($filename); + unlink($filename); + $sheet = $newspreadsheet->getActiveSheet(); + self::assertEquals('1', $sheet->getCell('A1')->getValue()); + self::assertEquals('2', $sheet->getCell('B1')->getValue()); + self::assertEquals('3', $sheet->getCell('C1')->getValue()); + self::assertEquals('4', $sheet->getCell('D1')->getValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Csv/CsvWriteTest.php b/tests/PhpSpreadsheetTests/Writer/Csv/CsvWriteTest.php index 7252ecf9..7fe1902b 100644 --- a/tests/PhpSpreadsheetTests/Writer/Csv/CsvWriteTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Csv/CsvWriteTest.php @@ -1,6 +1,6 @@ setEnclosure('\''); self::assertEquals('\'', $writer->getEnclosure()); $writer->setEnclosure(''); + self::assertEquals('', $writer->getEnclosure()); + $writer->setEnclosure(); self::assertEquals('"', $writer->getEnclosure()); self::assertEquals(PHP_EOL, $writer->getLineEnding()); self::assertFalse($writer->getUseBOM()); From 73379cdfb1034db66167ae2c01e0b339366b4ddd Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 11:34:02 -0700 Subject: [PATCH 027/153] Improve Coverage for Gnumeric (#1517) * Improve Coverage for Gnumeric I believe that both BaseReader and Gnumeric Reader are now 100% covered. My goal was to use PhpSpreadsheet to load the test file, save it as Xlsx, and visually compare the two, then add a test loaded with assertions. Results were generally pretty good, but there were no tests with assertions. I added a few cells to exercise some previously uncovered code. Code was extensively refactored; logic changes are noted below. Code allowed for specifying document properties in an old format. I considered removing that, but I found the original spec at http://www.jfree.org/jworkbook/download/gnumeric-xml.pdf This allowed me to create an old file, which was not handled correctly because of namespace differences. The code was corrected to allow for this difference. Added support for textRotation. Mapping of fill types was not correct. * PHP7.2 Error One assertion failed under PHP7.2. Apparently there was some change in the handling of SimpleXMLElement between 7.2 and 7.3. Casting to string before use eliminates the problem. * Scrutinizer Recommendations All minor, solved (hopefully) mostly by casts. * One Last Scrutinizer Fix ... I hope. --- samples/templates/GnumericTest.gnumeric | Bin 7823 -> 8064 bytes samples/templates/old.gnumeric | Bin 0 -> 1276 bytes src/PhpSpreadsheet/Reader/BaseReader.php | 23 +- src/PhpSpreadsheet/Reader/Gnumeric.php | 1123 ++++++++--------- tests/PhpSpreadsheetTests/IOFactoryTest.php | 1 + .../Reader/Gnumeric/GnumericFilter.php | 14 + .../Reader/Gnumeric/GnumericInfoTest.php | 47 + .../Reader/Gnumeric/GnumericLoadTest.php | 162 +++ .../Reader/Gnumeric/GnumericStylesTest.php | 269 ++++ 9 files changed, 1060 insertions(+), 579 deletions(-) create mode 100644 samples/templates/old.gnumeric create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericFilter.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericInfoTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericStylesTest.php diff --git a/samples/templates/GnumericTest.gnumeric b/samples/templates/GnumericTest.gnumeric index ea2fac379e51e707060c8c8d5314899c60bad287..0493e762360924353d0d60a1709528637db0224b 100644 GIT binary patch literal 8064 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 literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php index 77a6421b..eb0e3ba2 100644 --- a/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/BaseReader.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Shared\File; @@ -133,11 +134,7 @@ abstract class BaseReader implements IReader public function getSecurityScanner() { - if (property_exists($this, 'securityScanner')) { - return $this->securityScanner; - } - - return null; + return $this->securityScanner; } /** @@ -147,12 +144,18 @@ abstract class BaseReader implements IReader */ protected function openFile($pFilename): void { - File::assertFile($pFilename); + if ($pFilename) { + File::assertFile($pFilename); - // Open file - $this->fileHandle = fopen($pFilename, 'rb'); - if ($this->fileHandle === false) { - throw new Exception('Could not open file ' . $pFilename . ' for reading.'); + // Open file + $fileHandle = fopen($pFilename, 'rb'); + } else { + $fileHandle = false; + } + if ($fileHandle !== false) { + $this->fileHandle = $fileHandle; + } else { + throw new ReaderException('Could not open file ' . $pFilename . ' for reading.'); } } } diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index f9768029..81096730 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use SimpleXMLElement; use XMLReader; class Gnumeric extends BaseReader @@ -29,8 +30,23 @@ class Gnumeric extends BaseReader */ private $expressions = []; + /** + * Spreadsheet shared across all functions. + * + * @var Spreadsheet + */ + private $spreadsheet; + private $referenceHelper; + /** + * Namespace shared across all functions. + * It is 'gnm', except for really old sheets which use 'gmr'. + * + * @var string + */ + private $gnm = 'gnm'; + /** * Create a new Gnumeric. */ @@ -53,18 +69,22 @@ class Gnumeric extends BaseReader File::assertFile($pFilename); // Check if gzlib functions are available - if (!function_exists('gzread')) { - throw new Exception('gzlib library is not enabled'); + $data = ''; + if (function_exists('gzread')) { + // Read signature data (first 3 bytes) + $fh = fopen($pFilename, 'rb'); + $data = fread($fh, 2); + fclose($fh); } - // Read signature data (first 3 bytes) - $fh = fopen($pFilename, 'rb'); - $data = fread($fh, 2); - fclose($fh); - return $data == chr(0x1F) . chr(0x8B); } + private static function matchXml(string $name, string $field): bool + { + return 1 === preg_match("/^(gnm|gmr):$field$/", $name); + } + /** * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. * @@ -82,10 +102,10 @@ class Gnumeric extends BaseReader $worksheetNames = []; while ($xml->read()) { - if ($xml->name == 'gnm:SheetName' && $xml->nodeType == XMLReader::ELEMENT) { + if (self::matchXml($xml->name, 'SheetName') && $xml->nodeType == XMLReader::ELEMENT) { $xml->read(); // Move onto the value node $worksheetNames[] = (string) $xml->value; - } elseif ($xml->name == 'gnm:Sheets') { + } elseif (self::matchXml($xml->name, 'Sheets')) { // break out of the loop once we've got our sheet names rather than parse the entire file break; } @@ -111,7 +131,7 @@ class Gnumeric extends BaseReader $worksheetInfo = []; while ($xml->read()) { - if ($xml->name == 'gnm:Sheet' && $xml->nodeType == XMLReader::ELEMENT) { + if (self::matchXml($xml->name, 'Sheet') && $xml->nodeType == XMLReader::ELEMENT) { $tmpInfo = [ 'worksheetName' => '', 'lastColumnLetter' => 'A', @@ -121,18 +141,20 @@ class Gnumeric extends BaseReader ]; while ($xml->read()) { - if ($xml->name == 'gnm:Name' && $xml->nodeType == XMLReader::ELEMENT) { - $xml->read(); // Move onto the value node - $tmpInfo['worksheetName'] = (string) $xml->value; - } elseif ($xml->name == 'gnm:MaxCol' && $xml->nodeType == XMLReader::ELEMENT) { - $xml->read(); // Move onto the value node - $tmpInfo['lastColumnIndex'] = (int) $xml->value; - $tmpInfo['totalColumns'] = (int) $xml->value + 1; - } elseif ($xml->name == 'gnm:MaxRow' && $xml->nodeType == XMLReader::ELEMENT) { - $xml->read(); // Move onto the value node - $tmpInfo['totalRows'] = (int) $xml->value + 1; + if ($xml->nodeType == XMLReader::ELEMENT) { + if (self::matchXml($xml->name, 'Name')) { + $xml->read(); // Move onto the value node + $tmpInfo['worksheetName'] = (string) $xml->value; + } elseif (self::matchXml($xml->name, 'MaxCol')) { + $xml->read(); // Move onto the value node + $tmpInfo['lastColumnIndex'] = (int) $xml->value; + $tmpInfo['totalColumns'] = (int) $xml->value + 1; + } elseif (self::matchXml($xml->name, 'MaxRow')) { + $xml->read(); // Move onto the value node + $tmpInfo['totalRows'] = (int) $xml->value + 1; - break; + break; + } } } $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); @@ -162,6 +184,283 @@ class Gnumeric extends BaseReader return $data; } + private static $mappings = [ + 'borderStyle' => [ + '0' => Border::BORDER_NONE, + '1' => Border::BORDER_THIN, + '2' => Border::BORDER_MEDIUM, + '3' => Border::BORDER_SLANTDASHDOT, + '4' => Border::BORDER_DASHED, + '5' => Border::BORDER_THICK, + '6' => Border::BORDER_DOUBLE, + '7' => Border::BORDER_DOTTED, + '8' => Border::BORDER_MEDIUMDASHED, + '9' => Border::BORDER_DASHDOT, + '10' => Border::BORDER_MEDIUMDASHDOT, + '11' => Border::BORDER_DASHDOTDOT, + '12' => Border::BORDER_MEDIUMDASHDOTDOT, + '13' => Border::BORDER_MEDIUMDASHDOTDOT, + ], + 'dataType' => [ + '10' => DataType::TYPE_NULL, + '20' => DataType::TYPE_BOOL, + '30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel + '40' => DataType::TYPE_NUMERIC, // Float + '50' => DataType::TYPE_ERROR, + '60' => DataType::TYPE_STRING, + //'70': // Cell Range + //'80': // Array + ], + 'fillType' => [ + '1' => Fill::FILL_SOLID, + '2' => Fill::FILL_PATTERN_DARKGRAY, + '3' => Fill::FILL_PATTERN_MEDIUMGRAY, + '4' => Fill::FILL_PATTERN_LIGHTGRAY, + '5' => Fill::FILL_PATTERN_GRAY125, + '6' => Fill::FILL_PATTERN_GRAY0625, + '7' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe + '8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe + '9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe + '10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe + '11' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch + '12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch + '13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL, + '14' => Fill::FILL_PATTERN_LIGHTVERTICAL, + '15' => Fill::FILL_PATTERN_LIGHTUP, + '16' => Fill::FILL_PATTERN_LIGHTDOWN, + '17' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch + '18' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch + ], + 'horizontal' => [ + '1' => Alignment::HORIZONTAL_GENERAL, + '2' => Alignment::HORIZONTAL_LEFT, + '4' => Alignment::HORIZONTAL_RIGHT, + '8' => Alignment::HORIZONTAL_CENTER, + '16' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + '32' => Alignment::HORIZONTAL_JUSTIFY, + '64' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + ], + 'underline' => [ + '1' => Font::UNDERLINE_SINGLE, + '2' => Font::UNDERLINE_DOUBLE, + '3' => Font::UNDERLINE_SINGLEACCOUNTING, + '4' => Font::UNDERLINE_DOUBLEACCOUNTING, + ], + 'vertical' => [ + '1' => Alignment::VERTICAL_TOP, + '2' => Alignment::VERTICAL_BOTTOM, + '4' => Alignment::VERTICAL_CENTER, + '8' => Alignment::VERTICAL_JUSTIFY, + ], + ]; + + public static function gnumericMappings(): array + { + return self::$mappings; + } + + private function docPropertiesOld(SimpleXMLElement $gnmXML): void + { + $docProps = $this->spreadsheet->getProperties(); + foreach ($gnmXML->Summary->Item as $summaryItem) { + $propertyName = $summaryItem->name; + $propertyValue = $summaryItem->{'val-string'}; + switch ($propertyName) { + case 'title': + $docProps->setTitle(trim($propertyValue)); + + break; + case 'comments': + $docProps->setDescription(trim($propertyValue)); + + break; + case 'keywords': + $docProps->setKeywords(trim($propertyValue)); + + break; + case 'category': + $docProps->setCategory(trim($propertyValue)); + + break; + case 'manager': + $docProps->setManager(trim($propertyValue)); + + break; + case 'author': + $docProps->setCreator(trim($propertyValue)); + $docProps->setLastModifiedBy(trim($propertyValue)); + + break; + case 'company': + $docProps->setCompany(trim($propertyValue)); + + break; + } + } + } + + private function docPropertiesDC(SimpleXMLElement $officePropertyDC): void + { + $docProps = $this->spreadsheet->getProperties(); + foreach ($officePropertyDC as $propertyName => $propertyValue) { + $propertyValue = trim((string) $propertyValue); + switch ($propertyName) { + case 'title': + $docProps->setTitle($propertyValue); + + break; + case 'subject': + $docProps->setSubject($propertyValue); + + break; + case 'creator': + $docProps->setCreator($propertyValue); + $docProps->setLastModifiedBy($propertyValue); + + break; + case 'date': + $creationDate = strtotime($propertyValue); + $docProps->setCreated($creationDate); + $docProps->setModified($creationDate); + + break; + case 'description': + $docProps->setDescription($propertyValue); + + break; + } + } + } + + private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta, array $namespacesMeta): void + { + $docProps = $this->spreadsheet->getProperties(); + foreach ($officePropertyMeta as $propertyName => $propertyValue) { + $attributes = $propertyValue->attributes($namespacesMeta['meta']); + $propertyValue = trim((string) $propertyValue); + switch ($propertyName) { + case 'keyword': + $docProps->setKeywords($propertyValue); + + break; + case 'initial-creator': + $docProps->setCreator($propertyValue); + $docProps->setLastModifiedBy($propertyValue); + + break; + case 'creation-date': + $creationDate = strtotime($propertyValue); + $docProps->setCreated($creationDate); + $docProps->setModified($creationDate); + + break; + case 'user-defined': + [, $attrName] = explode(':', $attributes['name']); + switch ($attrName) { + case 'publisher': + $docProps->setCompany($propertyValue); + + break; + case 'category': + $docProps->setCategory($propertyValue); + + break; + case 'manager': + $docProps->setManager($propertyValue); + + break; + } + + break; + } + } + } + + private function docProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML, array $namespacesMeta): void + { + if (isset($namespacesMeta['office'])) { + $officeXML = $xml->children($namespacesMeta['office']); + $officeDocXML = $officeXML->{'document-meta'}; + $officeDocMetaXML = $officeDocXML->meta; + + foreach ($officeDocMetaXML as $officePropertyData) { + $officePropertyDC = []; + if (isset($namespacesMeta['dc'])) { + $officePropertyDC = $officePropertyData->children($namespacesMeta['dc']); + } + $this->docPropertiesDC($officePropertyDC); + + $officePropertyMeta = []; + if (isset($namespacesMeta['meta'])) { + $officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']); + } + $this->docPropertiesMeta($officePropertyMeta, $namespacesMeta); + } + } elseif (isset($gnmXML->Summary)) { + $this->docPropertiesOld($gnmXML); + } + } + + private function sheetMargin(string $key, float $marginSize): void + { + switch ($key) { + case 'top': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setTop($marginSize); + + break; + case 'bottom': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom($marginSize); + + break; + case 'left': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft($marginSize); + + break; + case 'right': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setRight($marginSize); + + break; + case 'header': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader($marginSize); + + break; + case 'footer': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter($marginSize); + + break; + } + } + + private function sheetMargins(SimpleXMLElement $sheet): void + { + if (!$this->readDataOnly && isset($sheet->PrintInformation, $sheet->PrintInformation->Margins)) { + foreach ($sheet->PrintInformation->Margins->children($this->gnm, true) as $key => $margin) { + $marginAttributes = $margin->attributes(); + $marginSize = 72 / 100; // Default + switch ($marginAttributes['PrefUnit']) { + case 'mm': + $marginSize = (int) ($marginAttributes['Points']) / 100; + + break; + } + $this->sheetMargin($key, (float) $marginSize); + } + } + } + + private function processComments(SimpleXMLElement $sheet): void + { + if ((!$this->readDataOnly) && (isset($sheet->Objects))) { + foreach ($sheet->Objects->children($this->gnm, true) as $key => $comment) { + $commentAttributes = $comment->attributes(); + // Only comment objects are handled at the moment + if ($commentAttributes->Text) { + $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound)->setAuthor((string) $commentAttributes->Author)->setText($this->parseRichText((string) $commentAttributes->Text)); + } + } + } + } + /** * Loads Spreadsheet from file. * @@ -173,6 +472,7 @@ class Gnumeric extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); // Load into this instance return $this->loadIntoExisting($pFilename, $spreadsheet); @@ -180,143 +480,21 @@ class Gnumeric extends BaseReader /** * Loads from file into Spreadsheet instance. - * - * @param string $pFilename - * - * @return Spreadsheet */ - public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) + public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): Spreadsheet { + $this->spreadsheet = $spreadsheet; File::assertFile($pFilename); $gFileData = $this->gzfileGetContents($pFilename); - $xml = simplexml_load_string($this->securityScanner->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); + $xml2 = simplexml_load_string($this->securityScanner->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); + $xml = ($xml2 !== false) ? $xml2 : new SimpleXMLElement(''); $namespacesMeta = $xml->getNamespaces(true); + $this->gnm = array_key_exists('gmr', $namespacesMeta) ? 'gmr' : 'gnm'; - $gnmXML = $xml->children($namespacesMeta['gnm']); - - $docProps = $spreadsheet->getProperties(); - // Document Properties are held differently, depending on the version of Gnumeric - if (isset($namespacesMeta['office'])) { - $officeXML = $xml->children($namespacesMeta['office']); - $officeDocXML = $officeXML->{'document-meta'}; - $officeDocMetaXML = $officeDocXML->meta; - - foreach ($officeDocMetaXML as $officePropertyData) { - $officePropertyDC = []; - if (isset($namespacesMeta['dc'])) { - $officePropertyDC = $officePropertyData->children($namespacesMeta['dc']); - } - foreach ($officePropertyDC as $propertyName => $propertyValue) { - $propertyValue = (string) $propertyValue; - switch ($propertyName) { - case 'title': - $docProps->setTitle(trim($propertyValue)); - - break; - case 'subject': - $docProps->setSubject(trim($propertyValue)); - - break; - case 'creator': - $docProps->setCreator(trim($propertyValue)); - $docProps->setLastModifiedBy(trim($propertyValue)); - - break; - case 'date': - $creationDate = strtotime(trim($propertyValue)); - $docProps->setCreated($creationDate); - $docProps->setModified($creationDate); - - break; - case 'description': - $docProps->setDescription(trim($propertyValue)); - - break; - } - } - $officePropertyMeta = []; - if (isset($namespacesMeta['meta'])) { - $officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']); - } - foreach ($officePropertyMeta as $propertyName => $propertyValue) { - $attributes = $propertyValue->attributes($namespacesMeta['meta']); - $propertyValue = (string) $propertyValue; - switch ($propertyName) { - case 'keyword': - $docProps->setKeywords(trim($propertyValue)); - - break; - case 'initial-creator': - $docProps->setCreator(trim($propertyValue)); - $docProps->setLastModifiedBy(trim($propertyValue)); - - break; - case 'creation-date': - $creationDate = strtotime(trim($propertyValue)); - $docProps->setCreated($creationDate); - $docProps->setModified($creationDate); - - break; - case 'user-defined': - [, $attrName] = explode(':', $attributes['name']); - switch ($attrName) { - case 'publisher': - $docProps->setCompany(trim($propertyValue)); - - break; - case 'category': - $docProps->setCategory(trim($propertyValue)); - - break; - case 'manager': - $docProps->setManager(trim($propertyValue)); - - break; - } - - break; - } - } - } - } elseif (isset($gnmXML->Summary)) { - foreach ($gnmXML->Summary->Item as $summaryItem) { - $propertyName = $summaryItem->name; - $propertyValue = $summaryItem->{'val-string'}; - switch ($propertyName) { - case 'title': - $docProps->setTitle(trim($propertyValue)); - - break; - case 'comments': - $docProps->setDescription(trim($propertyValue)); - - break; - case 'keywords': - $docProps->setKeywords(trim($propertyValue)); - - break; - case 'category': - $docProps->setCategory(trim($propertyValue)); - - break; - case 'manager': - $docProps->setManager(trim($propertyValue)); - - break; - case 'author': - $docProps->setCreator(trim($propertyValue)); - $docProps->setLastModifiedBy(trim($propertyValue)); - - break; - case 'company': - $docProps->setCompany(trim($propertyValue)); - - break; - } - } - } + $gnmXML = $xml->children($namespacesMeta[$this->gnm]); + $this->docProperties($xml, $gnmXML, $namespacesMeta); $worksheetID = 0; foreach ($gnmXML->Sheets->Sheet as $sheet) { @@ -328,53 +506,14 @@ class Gnumeric extends BaseReader $maxRow = $maxCol = 0; // Create new Worksheet - $spreadsheet->createSheet(); - $spreadsheet->setActiveSheetIndex($worksheetID); + $this->spreadsheet->createSheet(); + $this->spreadsheet->setActiveSheetIndex($worksheetID); // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet // name in line with the formula, not the reverse - $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); - if ((!$this->readDataOnly) && (isset($sheet->PrintInformation))) { - if (isset($sheet->PrintInformation->Margins)) { - foreach ($sheet->PrintInformation->Margins->children('gnm', true) as $key => $margin) { - $marginAttributes = $margin->attributes(); - $marginSize = 72 / 100; // Default - switch ($marginAttributes['PrefUnit']) { - case 'mm': - $marginSize = (int) ($marginAttributes['Points']) / 100; - - break; - } - switch ($key) { - case 'top': - $spreadsheet->getActiveSheet()->getPageMargins()->setTop($marginSize); - - break; - case 'bottom': - $spreadsheet->getActiveSheet()->getPageMargins()->setBottom($marginSize); - - break; - case 'left': - $spreadsheet->getActiveSheet()->getPageMargins()->setLeft($marginSize); - - break; - case 'right': - $spreadsheet->getActiveSheet()->getPageMargins()->setRight($marginSize); - - break; - case 'header': - $spreadsheet->getActiveSheet()->getPageMargins()->setHeader($marginSize); - - break; - case 'footer': - $spreadsheet->getActiveSheet()->getPageMargins()->setFooter($marginSize); - - break; - } - } - } - } + $this->sheetMargins($sheet); foreach ($sheet->Cells->Cell as $cell) { $cellAttributes = $cell->attributes(); @@ -420,48 +559,19 @@ class Gnumeric extends BaseReader } $type = DataType::TYPE_FORMULA; } else { - switch ($ValueType) { - case '10': // NULL - $type = DataType::TYPE_NULL; - - break; - case '20': // Boolean - $type = DataType::TYPE_BOOL; - $cell = $cell == 'TRUE'; - - break; - case '30': // Integer - $cell = (int) $cell; - // Excel 2007+ doesn't differentiate between integer and float, so set the value and dropthru to the next (numeric) case - // no break - case '40': // Float - $type = DataType::TYPE_NUMERIC; - - break; - case '50': // Error - $type = DataType::TYPE_ERROR; - - break; - case '60': // String - $type = DataType::TYPE_STRING; - - break; - case '70': // Cell Range - case '80': // Array + $vtype = (string) $ValueType; + if (array_key_exists($vtype, self::$mappings['dataType'])) { + $type = self::$mappings['dataType'][$vtype]; + } + if ($vtype == '20') { // Boolean + $cell = $cell == 'TRUE'; } } - $spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit($cell, $type); + $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); } - if ((!$this->readDataOnly) && (isset($sheet->Objects))) { - foreach ($sheet->Objects->children('gnm', true) as $key => $comment) { - $commentAttributes = $comment->attributes(); - // Only comment objects are handled at the moment - if ($commentAttributes->Text) { - $spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound)->setAuthor((string) $commentAttributes->Author)->setText($this->parseRichText((string) $commentAttributes->Text)); - } - } - } + $this->processComments($sheet); + foreach ($sheet->Styles->StyleRegion as $styleRegion) { $styleAttributes = $styleRegion->attributes(); if (($styleAttributes['startRow'] <= $maxRow) && @@ -471,306 +581,197 @@ class Gnumeric extends BaseReader $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); - $endRow = ($styleAttributes['endRow'] > $maxRow) ? $maxRow : $styleAttributes['endRow']; - ++$endRow; + $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : $styleAttributes['endRow']); $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; $styleAttributes = $styleRegion->Style->attributes(); + $styleArray = []; // We still set the number format mask for date/time values, even if readDataOnly is true - if ((!$this->readDataOnly) || - (Date::isDateTimeFormatCode((string) $styleAttributes['Format']))) { - $styleArray = []; - $styleArray['numberFormat']['formatCode'] = (string) $styleAttributes['Format']; + $formatCode = (string) $styleAttributes['Format']; + if (Date::isDateTimeFormatCode($formatCode)) { + $styleArray['numberFormat']['formatCode'] = $formatCode; + } + if (!$this->readDataOnly) { // If readDataOnly is false, we set all formatting information - if (!$this->readDataOnly) { - switch ($styleAttributes['HAlign']) { - case '1': - $styleArray['alignment']['horizontal'] = Alignment::HORIZONTAL_GENERAL; + $styleArray['numberFormat']['formatCode'] = $formatCode; - break; - case '2': - $styleArray['alignment']['horizontal'] = Alignment::HORIZONTAL_LEFT; + self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); + self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); + $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; + $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); + $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; + $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; - break; - case '4': - $styleArray['alignment']['horizontal'] = Alignment::HORIZONTAL_RIGHT; + $this->addColors($styleArray, $styleAttributes); - break; - case '8': - $styleArray['alignment']['horizontal'] = Alignment::HORIZONTAL_CENTER; + $fontAttributes = $styleRegion->Style->Font->attributes(); + $styleArray['font']['name'] = (string) $styleRegion->Style->Font; + $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); + $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; + $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; + $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; + self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); - break; - case '16': - case '64': - $styleArray['alignment']['horizontal'] = Alignment::HORIZONTAL_CENTER_CONTINUOUS; + switch ($fontAttributes['Script']) { + case '1': + $styleArray['font']['superscript'] = true; - break; - case '32': - $styleArray['alignment']['horizontal'] = Alignment::HORIZONTAL_JUSTIFY; + break; + case '-1': + $styleArray['font']['subscript'] = true; - break; - } - - switch ($styleAttributes['VAlign']) { - case '1': - $styleArray['alignment']['vertical'] = Alignment::VERTICAL_TOP; - - break; - case '2': - $styleArray['alignment']['vertical'] = Alignment::VERTICAL_BOTTOM; - - break; - case '4': - $styleArray['alignment']['vertical'] = Alignment::VERTICAL_CENTER; - - break; - case '8': - $styleArray['alignment']['vertical'] = Alignment::VERTICAL_JUSTIFY; - - break; - } - - $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; - $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; - $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; - - $RGB = self::parseGnumericColour($styleAttributes['Fore']); - $styleArray['font']['color']['rgb'] = $RGB; - $RGB = self::parseGnumericColour($styleAttributes['Back']); - $shade = $styleAttributes['Shade']; - if (($RGB != '000000') || ($shade != '0')) { - $styleArray['fill']['color']['rgb'] = $styleArray['fill']['startColor']['rgb'] = $RGB; - $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); - $styleArray['fill']['endColor']['rgb'] = $RGB2; - switch ($shade) { - case '1': - $styleArray['fill']['fillType'] = Fill::FILL_SOLID; - - break; - case '2': - $styleArray['fill']['fillType'] = Fill::FILL_GRADIENT_LINEAR; - - break; - case '3': - $styleArray['fill']['fillType'] = Fill::FILL_GRADIENT_PATH; - - break; - case '4': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_DARKDOWN; - - break; - case '5': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_DARKGRAY; - - break; - case '6': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_DARKGRID; - - break; - case '7': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_DARKHORIZONTAL; - - break; - case '8': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_DARKTRELLIS; - - break; - case '9': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_DARKUP; - - break; - case '10': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_DARKVERTICAL; - - break; - case '11': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_GRAY0625; - - break; - case '12': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_GRAY125; - - break; - case '13': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_LIGHTDOWN; - - break; - case '14': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_LIGHTGRAY; - - break; - case '15': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_LIGHTGRID; - - break; - case '16': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_LIGHTHORIZONTAL; - - break; - case '17': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_LIGHTTRELLIS; - - break; - case '18': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_LIGHTUP; - - break; - case '19': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_LIGHTVERTICAL; - - break; - case '20': - $styleArray['fill']['fillType'] = Fill::FILL_PATTERN_MEDIUMGRAY; - - break; - } - } - - $fontAttributes = $styleRegion->Style->Font->attributes(); - $styleArray['font']['name'] = (string) $styleRegion->Style->Font; - $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); - $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; - $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; - $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; - switch ($fontAttributes['Underline']) { - case '1': - $styleArray['font']['underline'] = Font::UNDERLINE_SINGLE; - - break; - case '2': - $styleArray['font']['underline'] = Font::UNDERLINE_DOUBLE; - - break; - case '3': - $styleArray['font']['underline'] = Font::UNDERLINE_SINGLEACCOUNTING; - - break; - case '4': - $styleArray['font']['underline'] = Font::UNDERLINE_DOUBLEACCOUNTING; - - break; - default: - $styleArray['font']['underline'] = Font::UNDERLINE_NONE; - - break; - } - switch ($fontAttributes['Script']) { - case '1': - $styleArray['font']['superscript'] = true; - - break; - case '-1': - $styleArray['font']['subscript'] = true; - - break; - } - - if (isset($styleRegion->Style->StyleBorder)) { - if (isset($styleRegion->Style->StyleBorder->Top)) { - $styleArray['borders']['top'] = self::parseBorderAttributes($styleRegion->Style->StyleBorder->Top->attributes()); - } - if (isset($styleRegion->Style->StyleBorder->Bottom)) { - $styleArray['borders']['bottom'] = self::parseBorderAttributes($styleRegion->Style->StyleBorder->Bottom->attributes()); - } - if (isset($styleRegion->Style->StyleBorder->Left)) { - $styleArray['borders']['left'] = self::parseBorderAttributes($styleRegion->Style->StyleBorder->Left->attributes()); - } - if (isset($styleRegion->Style->StyleBorder->Right)) { - $styleArray['borders']['right'] = self::parseBorderAttributes($styleRegion->Style->StyleBorder->Right->attributes()); - } - if ((isset($styleRegion->Style->StyleBorder->Diagonal)) && (isset($styleRegion->Style->StyleBorder->{'Rev-Diagonal'}))) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($styleRegion->Style->StyleBorder->Diagonal->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; - } elseif (isset($styleRegion->Style->StyleBorder->Diagonal)) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($styleRegion->Style->StyleBorder->Diagonal->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; - } elseif (isset($styleRegion->Style->StyleBorder->{'Rev-Diagonal'})) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($styleRegion->Style->StyleBorder->{'Rev-Diagonal'}->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; - } - } - if (isset($styleRegion->Style->HyperLink)) { - // TO DO - $hyperlink = $styleRegion->Style->HyperLink->attributes(); - } + break; } - $spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); - } - } - } - if ((!$this->readDataOnly) && (isset($sheet->Cols))) { - // Column Widths - $columnAttributes = $sheet->Cols->attributes(); - $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; - $c = 0; - foreach ($sheet->Cols->ColInfo as $columnOverride) { - $columnAttributes = $columnOverride->attributes(); - $column = $columnAttributes['No']; - $columnWidth = $columnAttributes['Unit'] / 5.4; - $hidden = (isset($columnAttributes['Hidden'])) && ($columnAttributes['Hidden'] == '1'); - $columnCount = (isset($columnAttributes['Count'])) ? $columnAttributes['Count'] : 1; - while ($c < $column) { - $spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); - ++$c; - } - while (($c < ($column + $columnCount)) && ($c <= $maxCol)) { - $spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($columnWidth); - if ($hidden) { - $spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setVisible(false); + if (isset($styleRegion->Style->StyleBorder)) { + $srssb = $styleRegion->Style->StyleBorder; + $this->addBorderStyle($srssb, $styleArray, 'top'); + $this->addBorderStyle($srssb, $styleArray, 'bottom'); + $this->addBorderStyle($srssb, $styleArray, 'left'); + $this->addBorderStyle($srssb, $styleArray, 'right'); + $this->addBorderDiagonal($srssb, $styleArray); } - ++$c; - } - } - while ($c <= $maxCol) { - $spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); - ++$c; - } - } - - if ((!$this->readDataOnly) && (isset($sheet->Rows))) { - // Row Heights - $rowAttributes = $sheet->Rows->attributes(); - $defaultHeight = $rowAttributes['DefaultSizePts']; - $r = 0; - - foreach ($sheet->Rows->RowInfo as $rowOverride) { - $rowAttributes = $rowOverride->attributes(); - $row = $rowAttributes['No']; - $rowHeight = $rowAttributes['Unit']; - $hidden = (isset($rowAttributes['Hidden'])) && ($rowAttributes['Hidden'] == '1'); - $rowCount = (isset($rowAttributes['Count'])) ? $rowAttributes['Count'] : 1; - while ($r < $row) { - ++$r; - $spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); - } - while (($r < ($row + $rowCount)) && ($r < $maxRow)) { - ++$r; - $spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($rowHeight); - if ($hidden) { - $spreadsheet->getActiveSheet()->getRowDimension($r)->setVisible(false); + if (isset($styleRegion->Style->HyperLink)) { + // TO DO + $hyperlink = $styleRegion->Style->HyperLink->attributes(); } } - } - while ($r < $maxRow) { - ++$r; - $spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); + $this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); } } - // Handle Merged Cells in this worksheet - if (isset($sheet->MergedRegions)) { - foreach ($sheet->MergedRegions->Merge as $mergeCells) { - if (strpos($mergeCells, ':') !== false) { - $spreadsheet->getActiveSheet()->mergeCells($mergeCells); - } - } - } + $this->processColumnWidths($sheet, $maxCol); + $this->processRowHeights($sheet, $maxRow); + $this->processMergedCells($sheet); ++$worksheetID; } + $this->processDefinedNames($gnmXML); + + // Return + return $this->spreadsheet; + } + + private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void + { + if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; + } elseif (isset($srssb->Diagonal)) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; + } elseif (isset($srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; + } + } + + private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void + { + $ucDirection = ucfirst($direction); + if (isset($srssb->$ucDirection)) { + $styleArray['borders'][$direction] = self::parseBorderAttributes($srssb->$ucDirection->attributes()); + } + } + + private function processMergedCells(SimpleXMLElement $sheet): void + { + // Handle Merged Cells in this worksheet + if (isset($sheet->MergedRegions)) { + foreach ($sheet->MergedRegions->Merge as $mergeCells) { + if (strpos($mergeCells, ':') !== false) { + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); + } + } + } + } + + private function processColumnLoop(int $c, int $maxCol, SimpleXMLElement $columnOverride, float $defaultWidth): int + { + $columnAttributes = $columnOverride->attributes(); + $column = $columnAttributes['No']; + $columnWidth = ((float) $columnAttributes['Unit']) / 5.4; + $hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1'); + $columnCount = (isset($columnAttributes['Count'])) ? $columnAttributes['Count'] : 1; + while ($c < $column) { + $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); + ++$c; + } + while (($c < ($column + $columnCount)) && ($c <= $maxCol)) { + $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($columnWidth); + if ($hidden) { + $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setVisible(false); + } + ++$c; + } + + return $c; + } + + private function processColumnWidths(SimpleXMLElement $sheet, int $maxCol): void + { + if ((!$this->readDataOnly) && (isset($sheet->Cols))) { + // Column Widths + $columnAttributes = $sheet->Cols->attributes(); + $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; + $c = 0; + foreach ($sheet->Cols->ColInfo as $columnOverride) { + $c = $this->processColumnLoop($c, $maxCol, $columnOverride, $defaultWidth); + } + while ($c <= $maxCol) { + $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); + ++$c; + } + } + } + + private function processRowLoop(int $r, int $maxRow, SimpleXMLElement $rowOverride, float $defaultHeight): int + { + $rowAttributes = $rowOverride->attributes(); + $row = $rowAttributes['No']; + $rowHeight = (float) $rowAttributes['Unit']; + $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1'); + $rowCount = (isset($rowAttributes['Count'])) ? $rowAttributes['Count'] : 1; + while ($r < $row) { + ++$r; + $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); + } + while (($r < ($row + $rowCount)) && ($r < $maxRow)) { + ++$r; + $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($rowHeight); + if ($hidden) { + $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setVisible(false); + } + } + + return $r; + } + + private function processRowHeights(SimpleXMLElement $sheet, int $maxRow): void + { + if ((!$this->readDataOnly) && (isset($sheet->Rows))) { + // Row Heights + $rowAttributes = $sheet->Rows->attributes(); + $defaultHeight = (float) $rowAttributes['DefaultSizePts']; + $r = 0; + + foreach ($sheet->Rows->RowInfo as $rowOverride) { + $r = $this->processRowLoop($r, $maxRow, $rowOverride, $defaultHeight); + } + // never executed, I can't figure out any circumstances + // under which it would be executed, and, even if + // such exist, I'm not convinced this is needed. + //while ($r < $maxRow) { + // ++$r; + // $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); + //} + } + } + + private function processDefinedNames(SimpleXMLElement $gnmXML): void + { // Loop through definedNames (global named ranges) if (isset($gnmXML->Names)) { foreach ($gnmXML->Names->Name as $namedRange) { @@ -782,15 +783,37 @@ class Gnumeric extends BaseReader $range = Worksheet::extractSheetTitle($range, true); $range[0] = trim($range[0], "'"); - if ($worksheet = $spreadsheet->getSheetByName($range[0])) { + if ($worksheet = $this->spreadsheet->getSheetByName($range[0])) { $extractedRange = str_replace('$', '', $range[1]); - $spreadsheet->addNamedRange(new NamedRange($name, $worksheet, $extractedRange)); + $this->spreadsheet->addNamedRange(new NamedRange($name, $worksheet, $extractedRange)); } } } + } - // Return - return $spreadsheet; + private function calcRotation(SimpleXMLElement $styleAttributes): int + { + $rotation = (int) $styleAttributes->Rotation; + if ($rotation >= 270 && $rotation <= 360) { + $rotation -= 360; + } + $rotation = (abs($rotation) > 90) ? 0 : $rotation; + + return $rotation; + } + + private static function addStyle(array &$styleArray, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key] = self::$mappings[$key][$value]; + } + } + + private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key1][$key] = self::$mappings[$key][$value]; + } } private static function parseBorderAttributes($borderAttributes) @@ -800,64 +823,7 @@ class Gnumeric extends BaseReader $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); } - switch ($borderAttributes['Style']) { - case '0': - $styleArray['borderStyle'] = Border::BORDER_NONE; - - break; - case '1': - $styleArray['borderStyle'] = Border::BORDER_THIN; - - break; - case '2': - $styleArray['borderStyle'] = Border::BORDER_MEDIUM; - - break; - case '3': - $styleArray['borderStyle'] = Border::BORDER_SLANTDASHDOT; - - break; - case '4': - $styleArray['borderStyle'] = Border::BORDER_DASHED; - - break; - case '5': - $styleArray['borderStyle'] = Border::BORDER_THICK; - - break; - case '6': - $styleArray['borderStyle'] = Border::BORDER_DOUBLE; - - break; - case '7': - $styleArray['borderStyle'] = Border::BORDER_DOTTED; - - break; - case '8': - $styleArray['borderStyle'] = Border::BORDER_MEDIUMDASHED; - - break; - case '9': - $styleArray['borderStyle'] = Border::BORDER_DASHDOT; - - break; - case '10': - $styleArray['borderStyle'] = Border::BORDER_MEDIUMDASHDOT; - - break; - case '11': - $styleArray['borderStyle'] = Border::BORDER_DASHDOTDOT; - - break; - case '12': - $styleArray['borderStyle'] = Border::BORDER_MEDIUMDASHDOTDOT; - - break; - case '13': - $styleArray['borderStyle'] = Border::BORDER_MEDIUMDASHDOTDOT; - - break; - } + self::addStyle($styleArray, 'borderStyle', $borderAttributes['Style']); return $styleArray; } @@ -879,4 +845,23 @@ class Gnumeric extends BaseReader return $gnmR . $gnmG . $gnmB; } + + private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void + { + $RGB = self::parseGnumericColour($styleAttributes['Fore']); + $styleArray['font']['color']['rgb'] = $RGB; + $RGB = self::parseGnumericColour($styleAttributes['Back']); + $shade = (string) $styleAttributes['Shade']; + if (($RGB != '000000') || ($shade != '0')) { + $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); + if ($shade == '1') { + $styleArray['fill']['startColor']['rgb'] = $RGB; + $styleArray['fill']['endColor']['rgb'] = $RGB2; + } else { + $styleArray['fill']['endColor']['rgb'] = $RGB; + $styleArray['fill']['startColor']['rgb'] = $RGB2; + } + self::addStyle2($styleArray, 'fill', 'fillType', $shade); + } + } } diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index 983ba35e..906375bd 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -123,6 +123,7 @@ class IOFactoryTest extends TestCase return [ ['samples/templates/26template.xlsx', 'Xlsx', Reader\Xlsx::class], ['samples/templates/GnumericTest.gnumeric', 'Gnumeric', Reader\Gnumeric::class], + ['samples/templates/old.gnumeric', 'Gnumeric', Reader\Gnumeric::class], ['samples/templates/30template.xls', 'Xls', Reader\Xls::class], ['samples/templates/OOCalcTest.ods', 'Ods', Reader\Ods::class], ['samples/templates/SylkTest.slk', 'Slk', Reader\Slk::class], diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericFilter.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericFilter.php new file mode 100644 index 00000000..0904e2d4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericFilter.php @@ -0,0 +1,14 @@ +listWorksheetNames($filename); + self::assertCount(2, $names); + self::assertEquals('Sample Data', $names[0]); + self::assertEquals('Report Data', $names[1]); + } + + public function testListInfo(): void + { + $filename = __DIR__ + . '/../../../..' + . '/samples/templates/GnumericTest.gnumeric'; + $reader = new Gnumeric(); + $info = $reader->listWorksheetInfo($filename); + $expected = [ + [ + 'worksheetName' => 'Sample Data', + 'lastColumnLetter' => 'N', + 'lastColumnIndex' => 13, + 'totalRows' => 31, + 'totalColumns' => 14, + ], + [ + 'worksheetName' => 'Report Data', + 'lastColumnLetter' => 'K', + 'lastColumnIndex' => 10, + 'totalRows' => 65535, + 'totalColumns' => 11, + ], + ]; + self::assertEquals($expected, $info); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php new file mode 100644 index 00000000..e24178e5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php @@ -0,0 +1,162 @@ +load($filename); + self::assertEquals(2, $spreadsheet->getSheetCount()); + + $sheet = $spreadsheet->getSheet(1); + self::assertEquals('Report Data', $sheet->getTitle()); + self::assertEquals('BCD', $sheet->getCell('A4')->getValue()); + $props = $spreadsheet->getProperties(); + self::assertEquals('Mark Baker', $props->getCreator()); + + $sheet = $spreadsheet->getSheet(0); + self::assertEquals('Sample Data', $sheet->getTitle()); + self::assertEquals('Test String 1', $sheet->getCell('A1')->getValue()); + self::assertEquals('FFFF0000', $sheet->getCell('A1')->getStyle()->getFont()->getColor()->getARGB()); + self::assertEquals(Font::UNDERLINE_SINGLE, $sheet->getCell('A3')->getStyle()->getFont()->getUnderline()); + self::assertEquals('Test with (") in string', $sheet->getCell('A4')->getValue()); + + self::assertEquals(22269, $sheet->getCell('A10')->getValue()); + self::assertEquals('dd/mm/yyyy', $sheet->getCell('A10')->getStyle()->getNumberFormat()->getFormatCode()); + self::assertEquals('19/12/1960', $sheet->getCell('A10')->getFormattedValue()); + self::assertEquals(1.5, $sheet->getCell('A11')->getValue()); + self::assertEquals('# ?0/??0', $sheet->getCell('A11')->getStyle()->getNumberFormat()->getFormatCode()); + // Same pattern, same value, different display in Gnumeric vs Excel + //self::assertEquals('1 1/2', $sheet->getCell('A11')->getFormattedValue()); + + self::assertEquals('=B1+C1', $sheet->getCell('H1')->getValue()); + self::assertEquals('=E2&F2', $sheet->getCell('J2')->getValue()); + self::assertEquals('=sum(C1:C4)', $sheet->getCell('I5')->getValue()); + + self::assertTrue($sheet->getCell('E1')->getStyle()->getFont()->getBold()); + self::assertTrue($sheet->getCell('E1')->getStyle()->getFont()->getItalic()); + + self::assertFalse($sheet->getCell('E2')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('E2')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('E2')->getStyle()->getFont()->getUnderline()); + self::assertTrue($sheet->getCell('E3')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('E3')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('E3')->getStyle()->getFont()->getUnderline()); + self::assertFalse($sheet->getCell('E4')->getStyle()->getFont()->getBold()); + self::assertTrue($sheet->getCell('E4')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('E4')->getStyle()->getFont()->getUnderline()); + + self::assertTrue($sheet->getCell('F1')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('F1')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('F1')->getStyle()->getFont()->getUnderline()); + self::assertFalse($sheet->getCell('F2')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('F2')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('F2')->getStyle()->getFont()->getUnderline()); + self::assertTrue($sheet->getCell('F3')->getStyle()->getFont()->getBold()); + self::assertTrue($sheet->getCell('F3')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('F3')->getStyle()->getFont()->getUnderline()); + self::assertFalse($sheet->getCell('F4')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('F4')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('F4')->getStyle()->getFont()->getUnderline()); + + self::assertEquals(Border::BORDER_MEDIUM, $sheet->getCell('C10')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C10')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C10')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C10')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C12')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C12')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_MEDIUM, $sheet->getCell('C12')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C12')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C14')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C14')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C14')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_MEDIUM, $sheet->getCell('C14')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C16')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_MEDIUM, $sheet->getCell('C16')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C16')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C16')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_THICK, $sheet->getCell('C18')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Color::COLOR_RED, $sheet->getCell('C18')->getStyle()->getBorders()->getTop()->getColor()->getARGB()); + self::assertEquals(Border::BORDER_THICK, $sheet->getCell('C18')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Color::COLOR_YELLOW, $sheet->getCell('C18')->getStyle()->getBorders()->getRight()->getColor()->getARGB()); + self::assertEquals(Border::BORDER_THICK, $sheet->getCell('C18')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C18')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C18')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + + self::assertEquals(Fill::FILL_PATTERN_DARKHORIZONTAL, $sheet->getCell('K19')->getStyle()->getFill()->getFillType()); + self::assertEquals('FF00CCFF', $sheet->getCell('K19')->getStyle()->getFill()->getEndColor()->getARGB()); + self::assertEquals(Color::COLOR_BLUE, $sheet->getCell('K19')->getStyle()->getFill()->getStartColor()->getARGB()); + self::assertEquals(Fill::FILL_PATTERN_GRAY0625, $sheet->getCell('L19')->getStyle()->getFill()->getFillType()); + self::assertEquals(Color::COLOR_RED, $sheet->getCell('L19')->getStyle()->getFill()->getEndColor()->getARGB()); + self::assertEquals(Color::COLOR_YELLOW, $sheet->getCell('L19')->getStyle()->getFill()->getStartColor()->getARGB()); + self::assertEquals(Fill::FILL_SOLID, $sheet->getCell('K3')->getStyle()->getFill()->getFillType()); + self::assertEquals(Color::COLOR_RED, $sheet->getCell('K3')->getStyle()->getFill()->getStartColor()->getARGB()); + + self::assertEquals(45, $sheet->getCell('E22')->getStyle()->getAlignment()->getTextRotation()); + self::assertEquals(-90, $sheet->getCell('G22')->getStyle()->getAlignment()->getTextRotation()); + self::assertEquals(Border::BORDER_DOUBLE, $sheet->getCell('N13')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + + self::assertEquals(Borders::DIAGONAL_BOTH, $sheet->getCell('E18')->getStyle()->getBorders()->getDiagonalDirection()); + self::assertEquals(Borders::DIAGONAL_DOWN, $sheet->getCell('I18')->getStyle()->getBorders()->getDiagonalDirection()); + self::assertEquals(Borders::DIAGONAL_UP, $sheet->getCell('J18')->getStyle()->getBorders()->getDiagonalDirection()); + self::assertEquals(Font::UNDERLINE_DOUBLE, $sheet->getCell('A24')->getStyle()->getFont()->getUnderline()); + self::assertTrue($sheet->getCell('B23')->getStyle()->getFont()->getSubScript()); + self::assertTrue($sheet->getCell('B24')->getStyle()->getFont()->getSuperScript()); + self::assertFalse($sheet->getRowDimension(30)->getVisible()); + } + + public function testLoadFilter(): void + { + $filename = __DIR__ + . '/../../../..' + . '/samples/templates/GnumericTest.gnumeric'; + $reader = new Gnumeric(); + $filter = new GnumericFilter(); + $reader->setReadFilter($filter); + $spreadsheet = $reader->load($filename); + self::assertEquals(2, $spreadsheet->getSheetCount()); + $sheet = $spreadsheet->getSheet(1); + self::assertEquals('Report Data', $sheet->getTitle()); + self::assertEquals('', $sheet->getCell('A4')->getValue()); + $props = $spreadsheet->getProperties(); + self::assertEquals('Mark Baker', $props->getCreator()); + } + + public function testLoadOld(): void + { + $filename = __DIR__ + . '/../../../..' + . '/samples/templates/old.gnumeric'; + $reader = new Gnumeric(); + $spreadsheet = $reader->load($filename); + $props = $spreadsheet->getProperties(); + self::assertEquals('David Gilbert', $props->getCreator()); + } + + public function testLoadSelectedSheets(): void + { + $filename = __DIR__ + . '/../../../..' + . '/samples/templates/GnumericTest.gnumeric'; + $reader = new Gnumeric(); + $reader->setLoadSheetsOnly(['Unknown Sheet', 'Report Data']); + $spreadsheet = $reader->load($filename); + self::assertEquals(1, $spreadsheet->getSheetCount()); + $sheet = $spreadsheet->getSheet(0); + self::assertEquals('Report Data', $sheet->getTitle()); + self::assertEquals('Third Heading', $sheet->getCell('C2')->getValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericStylesTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericStylesTest.php new file mode 100644 index 00000000..fd8d7800 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericStylesTest.php @@ -0,0 +1,269 @@ + $val) { + $covered[$key] = 0; + } + $tests = $this->providerBorderStyle(); + foreach ($tests as $test) { + $covered[$test[0]] = 1; + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "Borderstyle $key not tested"); + } + } + + /** + * @dataProvider providerfillType + */ + public function testFillType(string $style, string $expectedResult): void + { + $styles = Gnumeric::gnumericMappings(); + $borders = $styles['fillType']; + self::assertEquals($expectedResult, $borders[$style]); + } + + public function testFillTypeCoverage(): void + { + $styles = Gnumeric::gnumericMappings(); + $expected = $styles['fillType']; + $covered = []; + foreach ($expected as $key => $val) { + $covered[$key] = 0; + } + $tests = $this->providerfillType(); + foreach ($tests as $test) { + $covered[$test[0]] = 1; + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "fillType $key not tested"); + } + } + + /** + * @dataProvider providerHorizontal + */ + public function testHorizontal(string $style, string $expectedResult): void + { + $styles = Gnumeric::gnumericMappings(); + $borders = $styles['horizontal']; + self::assertEquals($expectedResult, $borders[$style]); + } + + public function testHorizontalCoverage(): void + { + $styles = Gnumeric::gnumericMappings(); + $expected = $styles['horizontal']; + $covered = []; + foreach ($expected as $key => $val) { + $covered[$key] = 0; + } + $tests = $this->providerHorizontal(); + foreach ($tests as $test) { + $covered[$test[0]] = 1; + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "horizontal $key not tested"); + } + } + + /** + * @dataProvider providerunderline + */ + public function testUnderline(string $style, string $expectedResult): void + { + $styles = Gnumeric::gnumericMappings(); + $borders = $styles['underline']; + self::assertEquals($expectedResult, $borders[$style]); + } + + public function testUnderlineCoverage(): void + { + $styles = Gnumeric::gnumericMappings(); + $expected = $styles['underline']; + $covered = []; + foreach ($expected as $key => $val) { + $covered[$key] = 0; + } + $tests = $this->providerUnderline(); + foreach ($tests as $test) { + $covered[$test[0]] = 1; + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "underline $key not tested"); + } + } + + /** + * @dataProvider providerVertical + */ + public function testVertical(string $style, string $expectedResult): void + { + $styles = Gnumeric::gnumericMappings(); + $borders = $styles['vertical']; + self::assertEquals($expectedResult, $borders[$style]); + } + + public function testVerticalCoverage(): void + { + $styles = Gnumeric::gnumericMappings(); + $expected = $styles['vertical']; + $covered = []; + foreach ($expected as $key => $val) { + $covered[$key] = 0; + } + $tests = $this->providerVertical(); + foreach ($tests as $test) { + $covered[$test[0]] = 1; + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "vertical $key not tested"); + } + } + + /** + * @dataProvider providerDataType + */ + public function testDataType(string $style, string $expectedResult): void + { + $styles = Gnumeric::gnumericMappings(); + $borders = $styles['dataType']; + self::assertEquals($expectedResult, $borders[$style]); + } + + public function testDataTypeCoverage(): void + { + $styles = Gnumeric::gnumericMappings(); + $expected = $styles['dataType']; + self::assertArrayNotHasKey('70', $expected); + self::assertArrayNotHasKey('80', $expected); + $covered = []; + foreach ($expected as $key => $val) { + $covered[$key] = 0; + } + $tests = $this->providerDataType(); + foreach ($tests as $test) { + $covered[$test[0]] = 1; + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "dataType $key not tested"); + } + } + + public function providerBorderStyle(): array + { + return [ + ['0', Border::BORDER_NONE], + ['1', Border::BORDER_THIN], + ['2', Border::BORDER_MEDIUM], + ['3', Border::BORDER_SLANTDASHDOT], + ['4', Border::BORDER_DASHED], + ['5', Border::BORDER_THICK], + ['6', Border::BORDER_DOUBLE], + ['7', Border::BORDER_DOTTED], + ['8', Border::BORDER_MEDIUMDASHED], + ['9', Border::BORDER_DASHDOT], + ['10', Border::BORDER_MEDIUMDASHDOT], + ['11', Border::BORDER_DASHDOTDOT], + ['12', Border::BORDER_MEDIUMDASHDOTDOT], + ['13', Border::BORDER_MEDIUMDASHDOTDOT], + ]; + } + + public function providerFillType(): array + { + return [ + ['1', Fill::FILL_SOLID], + ['2', Fill::FILL_PATTERN_DARKGRAY], + ['3', Fill::FILL_PATTERN_MEDIUMGRAY], + ['4', Fill::FILL_PATTERN_LIGHTGRAY], + ['5', Fill::FILL_PATTERN_GRAY125], + ['6', Fill::FILL_PATTERN_GRAY0625], + ['7', Fill::FILL_PATTERN_DARKHORIZONTAL], + ['8', Fill::FILL_PATTERN_DARKVERTICAL], + ['9', Fill::FILL_PATTERN_DARKDOWN], + ['10', Fill::FILL_PATTERN_DARKUP], + ['11', Fill::FILL_PATTERN_DARKGRID], + ['12', Fill::FILL_PATTERN_DARKTRELLIS], + ['13', Fill::FILL_PATTERN_LIGHTHORIZONTAL], + ['14', Fill::FILL_PATTERN_LIGHTVERTICAL], + ['15', Fill::FILL_PATTERN_LIGHTUP], + ['16', Fill::FILL_PATTERN_LIGHTDOWN], + ['17', Fill::FILL_PATTERN_LIGHTGRID], + ['18', Fill::FILL_PATTERN_LIGHTTRELLIS], + ]; + } + + public function providerHorizontal(): array + { + return [ + ['1', Alignment::HORIZONTAL_GENERAL], + ['2', Alignment::HORIZONTAL_LEFT], + ['4', Alignment::HORIZONTAL_RIGHT], + ['8', Alignment::HORIZONTAL_CENTER], + ['16', Alignment::HORIZONTAL_CENTER_CONTINUOUS], + ['32', Alignment::HORIZONTAL_JUSTIFY], + ['64', Alignment::HORIZONTAL_CENTER_CONTINUOUS], + ]; + } + + public function providerUnderline(): array + { + return [ + ['1', Font::UNDERLINE_SINGLE], + ['2', Font::UNDERLINE_DOUBLE], + ['3', Font::UNDERLINE_SINGLEACCOUNTING], + ['4', Font::UNDERLINE_DOUBLEACCOUNTING], + ]; + } + + public function providerVertical(): array + { + return [ + ['1', Alignment::VERTICAL_TOP], + ['2', Alignment::VERTICAL_BOTTOM], + ['4', Alignment::VERTICAL_CENTER], + ['8', Alignment::VERTICAL_JUSTIFY], + ]; + } + + public function providerDataType(): array + { + return [ + ['10', DataType::TYPE_NULL], + ['20', DataType::TYPE_BOOL], + ['30', DataType::TYPE_NUMERIC], // Integer doesn't exist in Excel + ['40', DataType::TYPE_NUMERIC], // Float + ['50', DataType::TYPE_ERROR], + ['60', DataType::TYPE_STRING], + //'70': // Cell Range + //'80': // Array + ]; + } +} From 262896086a9e60e2cb99cd15e13d1cf94a2ba025 Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 11:35:44 -0700 Subject: [PATCH 028/153] Improve Coverage for Sylk (#1514) * Improve Coverage for Sylk I believe that both BaseReader and Sylk Reader are now 100% covered. Documentation available for this format is sparse. It was always incomplete, and in some cases inaccurate. My goal was to use PhpSpreadsheet to load the test file, save it as Xlsx, and visually compare the two, then add a test loaded with assertions. Cell values and calculated values, and border styles were generally handled pretty well without changes. Other types of styling were not handled so well. I added a few cells to exercise some previously uncovered code. Sylk files must be ASCII. I have deprecated the use of the setEncoding and getEncoding functions, which had no test cases. --- samples/templates/SylkTest.slk | 3 +- src/PhpSpreadsheet/Reader/Slk.php | 605 +++++++++++-------- tests/PhpSpreadsheetTests/Reader/CsvTest.php | 4 + tests/PhpSpreadsheetTests/Reader/SlkTest.php | 134 ++++ 4 files changed, 493 insertions(+), 253 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/SlkTest.php diff --git a/samples/templates/SylkTest.slk b/samples/templates/SylkTest.slk index d5dd5bbe..95770d04 100644 --- a/samples/templates/SylkTest.slk +++ b/samples/templates/SylkTest.slk @@ -52,7 +52,7 @@ P;EArial;M200 P;EArial;M200;SI P;EArial;M200;SBI P;EArial;M200;SBU -P;EArial;M200;SBIU +P;EArial;M220;SBIU P;EArial;M200 P;EArial;M200;SI F;P0;DG0G8;M255 @@ -115,6 +115,7 @@ F;P19;FG0G;X4 C;Y7;X2;K2.34 C;X3;KFALSE C;Y8;X2;K3.45 +C;Y9;X2;K2.34;EMEDIAN(R[-3]C:R[-1]C) F;Y9;X1 F;X2 F;X3 diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php index f40eba74..0e147376 100644 --- a/src/PhpSpreadsheet/Reader/Slk.php +++ b/src/PhpSpreadsheet/Reader/Slk.php @@ -2,8 +2,10 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use InvalidArgumentException; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Border; @@ -38,6 +40,20 @@ class Slk extends BaseReader */ private $format = 0; + /** + * Fonts. + * + * @var array + */ + private $fonts = []; + + /** + * Font Count. + * + * @var int + */ + private $fontcount = 0; + /** * Create a new SYLK Reader instance. */ @@ -55,10 +71,9 @@ class Slk extends BaseReader */ public function canRead($pFilename) { - // Check if file exists try { $this->openFile($pFilename); - } catch (Exception $e) { + } catch (InvalidArgumentException $e) { return false; } @@ -78,12 +93,24 @@ class Slk extends BaseReader return $hasDelimiter && $hasId; } + private function canReadOrBust(string $pFilename): void + { + if (!$this->canRead($pFilename)) { + throw new ReaderException($pFilename . ' is an Invalid SYLK file.'); + } + $this->openFile($pFilename); + } + /** * Set input encoding. * + * @deprecated no use is made of this property + * * @param string $pValue Input encoding, eg: 'ANSI' * * @return $this + * + * @codeCoverageIgnore */ public function setInputEncoding($pValue) { @@ -95,7 +122,11 @@ class Slk extends BaseReader /** * Get input encoding. * + * @deprecated no use is made of this property + * * @return string + * + * @codeCoverageIgnore */ public function getInputEncoding() { @@ -112,22 +143,16 @@ class Slk extends BaseReader public function listWorksheetInfo($pFilename) { // Open file - if (!$this->canRead($pFilename)) { - throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); - } - $this->openFile($pFilename); + $this->canReadOrBust($pFilename); $fileHandle = $this->fileHandle; rewind($fileHandle); $worksheetInfo = []; - $worksheetInfo[0]['worksheetName'] = 'Worksheet'; - $worksheetInfo[0]['lastColumnLetter'] = 'A'; - $worksheetInfo[0]['lastColumnIndex'] = 0; - $worksheetInfo[0]['totalRows'] = 0; - $worksheetInfo[0]['totalColumns'] = 0; + $worksheetInfo[0]['worksheetName'] = basename($pFilename, '.slk'); // loop through one row (line) at a time in the file $rowIndex = 0; + $columnIndex = 0; while (($rowData = fgets($fileHandle)) !== false) { $columnIndex = 0; @@ -139,28 +164,26 @@ class Slk extends BaseReader $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData))))); $dataType = array_shift($rowData); - if ($dataType == 'C') { - // Read cell value data + if ($dataType == 'B') { foreach ($rowData as $rowDatum) { switch ($rowDatum[0]) { - case 'C': case 'X': $columnIndex = substr($rowDatum, 1) - 1; break; - case 'R': case 'Y': $rowIndex = substr($rowDatum, 1); break; } - - $worksheetInfo[0]['totalRows'] = max($worksheetInfo[0]['totalRows'], $rowIndex); - $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], $columnIndex); } + + break; } } + $worksheetInfo[0]['lastColumnIndex'] = $columnIndex; + $worksheetInfo[0]['totalRows'] = $rowIndex; $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1; @@ -186,6 +209,294 @@ class Slk extends BaseReader return $this->loadIntoExisting($pFilename, $spreadsheet); } + private $colorArray = [ + 'FF00FFFF', // 0 - cyan + 'FF000000', // 1 - black + 'FFFFFFFF', // 2 - white + 'FFFF0000', // 3 - red + 'FF00FF00', // 4 - green + 'FF0000FF', // 5 - blue + 'FFFFFF00', // 6 - yellow + 'FFFF00FF', // 7 - magenta + ]; + + private $fontStyleMappings = [ + 'B' => 'bold', + 'I' => 'italic', + 'U' => 'underline', + ]; + + private function processFormula(string $rowDatum, bool &$hasCalculatedValue, string &$cellDataFormula, string $row, string $column): void + { + $cellDataFormula = '=' . substr($rowDatum, 1); + // Convert R1C1 style references to A1 style references (but only when not quoted) + $temp = explode('"', $cellDataFormula); + $key = false; + foreach ($temp as &$value) { + // Only count/replace in alternate array entries + if ($key = !$key) { + preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); + // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way + // through the formula from left to right. Reversing means that we work right to left.through + // the formula + $cellReferences = array_reverse($cellReferences); + // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, + // then modify the formula to use that new reference + foreach ($cellReferences as $cellReference) { + $rowReference = $cellReference[2][0]; + // Empty R reference is the current row + if ($rowReference == '') { + $rowReference = $row; + } + // Bracketed R references are relative to the current row + if ($rowReference[0] == '[') { + $rowReference = $row + trim($rowReference, '[]'); + } + $columnReference = $cellReference[4][0]; + // Empty C reference is the current column + if ($columnReference == '') { + $columnReference = $column; + } + // Bracketed C references are relative to the current column + if ($columnReference[0] == '[') { + $columnReference = $column + trim($columnReference, '[]'); + } + $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference; + + $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); + } + } + } + unset($value); + // Then rebuild the formula string + $cellDataFormula = implode('"', $temp); + $hasCalculatedValue = true; + } + + private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void + { + // Read cell value data + $hasCalculatedValue = false; + $cellDataFormula = $cellData = ''; + foreach ($rowData as $rowDatum) { + switch ($rowDatum[0]) { + case 'C': + case 'X': + $column = substr($rowDatum, 1); + + break; + case 'R': + case 'Y': + $row = substr($rowDatum, 1); + + break; + case 'K': + $cellData = substr($rowDatum, 1); + + break; + case 'E': + $this->processFormula($rowDatum, $hasCalculatedValue, $cellDataFormula, $row, $column); + + break; + } + } + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + $cellData = Calculation::unwrapResult($cellData); + + // Set cell value + $this->processCFinal($spreadsheet, $hasCalculatedValue, $cellDataFormula, $cellData, "$columnLetter$row"); + } + + private function processCFinal(Spreadsheet &$spreadsheet, bool $hasCalculatedValue, string $cellDataFormula, string $cellData, string $coordinate): void + { + // Set cell value + $spreadsheet->getActiveSheet()->getCell($coordinate)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData); + if ($hasCalculatedValue) { + $cellData = Calculation::unwrapResult($cellData); + $spreadsheet->getActiveSheet()->getCell($coordinate)->setCalculatedValue($cellData); + } + } + + private function processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void + { + // Read cell formatting + $formatStyle = $columnWidth = ''; + $startCol = $endCol = ''; + $fontStyle = ''; + $styleData = []; + foreach ($rowData as $rowDatum) { + switch ($rowDatum[0]) { + case 'C': + case 'X': + $column = substr($rowDatum, 1); + + break; + case 'R': + case 'Y': + $row = substr($rowDatum, 1); + + break; + case 'P': + $formatStyle = $rowDatum; + + break; + case 'W': + [$startCol, $endCol, $columnWidth] = explode(' ', substr($rowDatum, 1)); + + break; + case 'S': + $this->styleSettings($rowDatum, $styleData, $fontStyle); + + break; + } + } + $this->addFormats($spreadsheet, $formatStyle, $row, $column); + $this->addFonts($spreadsheet, $fontStyle, $row, $column); + $this->addStyle($spreadsheet, $styleData, $row, $column); + $this->addWidth($spreadsheet, $columnWidth, $startCol, $endCol); + } + + private $styleSettingsFont = ['D' => 'bold', 'I' => 'italic']; + + private $styleSettingsBorder = [ + 'B' => 'bottom', + 'L' => 'left', + 'R' => 'right', + 'T' => 'top', + ]; + + private function styleSettings(string $rowDatum, array &$styleData, string &$fontStyle): void + { + $styleSettings = substr($rowDatum, 1); + $iMax = strlen($styleSettings); + for ($i = 0; $i < $iMax; ++$i) { + $char = $styleSettings[$i]; + if (array_key_exists($char, $this->styleSettingsFont)) { + $styleData['font'][$this->styleSettingsFont[$char]] = true; + } elseif (array_key_exists($char, $this->styleSettingsBorder)) { + $styleData['borders'][$this->styleSettingsBorder[$char]]['borderStyle'] = Border::BORDER_THIN; + } elseif ($char == 'S') { + $styleData['fill']['fillType'] = \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_PATTERN_GRAY125; + } elseif ($char == 'M') { + if (preg_match('/M([1-9]\\d*)/', $styleSettings, $matches)) { + $fontStyle = $matches[1]; + } + } + } + } + + private function addFormats(Spreadsheet &$spreadsheet, string $formatStyle, string $row, string $column): void + { + if ($formatStyle && $column > '' && $row > '') { + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + if (isset($this->formats[$formatStyle])) { + $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]); + } + } + } + + private function addFonts(Spreadsheet &$spreadsheet, string $fontStyle, string $row, string $column): void + { + if ($fontStyle && $column > '' && $row > '') { + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + if (isset($this->fonts[$fontStyle])) { + $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->fonts[$fontStyle]); + } + } + } + + private function addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column): void + { + if ((!empty($styleData)) && $column > '' && $row > '') { + $columnLetter = Coordinate::stringFromColumnIndex($column); + $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData); + } + } + + private function addWidth(Spreadsheet $spreadsheet, string $columnWidth, string $startCol, string $endCol): void + { + if ($columnWidth > '') { + if ($startCol == $endCol) { + $startCol = Coordinate::stringFromColumnIndex((int) $startCol); + $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth($columnWidth); + } else { + $startCol = Coordinate::stringFromColumnIndex($startCol); + $endCol = Coordinate::stringFromColumnIndex($endCol); + $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth); + do { + $spreadsheet->getActiveSheet()->getColumnDimension(++$startCol)->setWidth($columnWidth); + } while ($startCol != $endCol); + } + } + } + + private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void + { + // Read shared styles + $formatArray = []; + $fromFormats = ['\-', '\ ']; + $toFormats = ['-', ' ']; + foreach ($rowData as $rowDatum) { + switch ($rowDatum[0]) { + case 'P': + $formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, substr($rowDatum, 1)); + + break; + case 'E': + case 'F': + $formatArray['font']['name'] = substr($rowDatum, 1); + + break; + case 'M': + $formatArray['font']['size'] = substr($rowDatum, 1) / 20; + + break; + case 'L': + $this->processPColors($rowDatum, $formatArray); + + break; + case 'S': + $this->processPFontStyles($rowDatum, $formatArray); + + break; + } + } + $this->processPFinal($spreadsheet, $formatArray); + } + + private function processPColors(string $rowDatum, array &$formatArray): void + { + if (preg_match('/L([1-9]\\d*)/', $rowDatum, $matches)) { + $fontColor = $matches[1] % 8; + $formatArray['font']['color']['argb'] = $this->colorArray[$fontColor]; + } + } + + private function processPFontStyles(string $rowDatum, array &$formatArray): void + { + $styleSettings = substr($rowDatum, 1); + $iMax = strlen($styleSettings); + for ($i = 0; $i < $iMax; ++$i) { + if (array_key_exists($styleSettings[$i], $this->fontStyleMappings)) { + $formatArray['font'][$this->fontStyleMappings[$styleSettings[$i]]] = true; + } + } + } + + private function processPFinal(Spreadsheet &$spreadsheet, array $formatArray): void + { + if (array_key_exists('numberFormat', $formatArray)) { + $this->formats['P' . $this->format] = $formatArray; + ++$this->format; + } elseif (array_key_exists('font', $formatArray)) { + ++$this->fontcount; + $this->fonts[$this->fontcount] = $formatArray; + if ($this->fontcount === 1) { + $spreadsheet->getDefaultStyle()->applyFromArray($formatArray); + } + } + } + /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. * @@ -196,10 +507,7 @@ class Slk extends BaseReader public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) { // Open file - if (!$this->canRead($pFilename)) { - throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); - } - $this->openFile($pFilename); + $this->canReadOrBust($pFilename); $fileHandle = $this->fileHandle; rewind($fileHandle); @@ -208,251 +516,32 @@ class Slk extends BaseReader $spreadsheet->createSheet(); } $spreadsheet->setActiveSheetIndex($this->sheetIndex); - - $fromFormats = ['\-', '\ ']; - $toFormats = ['-', ' ']; + $spreadsheet->getActiveSheet()->setTitle(basename($pFilename, '.slk')); // Loop through file $column = $row = ''; // loop through one row (line) at a time in the file - while (($rowData = fgets($fileHandle)) !== false) { + while (($rowDataTxt = fgets($fileHandle)) !== false) { // convert SYLK encoded $rowData to UTF-8 - $rowData = StringHelper::SYLKtoUTF8($rowData); + $rowDataTxt = StringHelper::SYLKtoUTF8($rowDataTxt); // explode each row at semicolons while taking into account that literal semicolon (;) // is escaped like this (;;) - $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData))))); + $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowDataTxt))))); $dataType = array_shift($rowData); - // Read shared styles if ($dataType == 'P') { - $formatArray = []; - foreach ($rowData as $rowDatum) { - switch ($rowDatum[0]) { - case 'P': - $formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, substr($rowDatum, 1)); - - break; - case 'E': - case 'F': - $formatArray['font']['name'] = substr($rowDatum, 1); - - break; - case 'L': - $formatArray['font']['size'] = substr($rowDatum, 1); - - break; - case 'S': - $styleSettings = substr($rowDatum, 1); - $iMax = strlen($styleSettings); - for ($i = 0; $i < $iMax; ++$i) { - switch ($styleSettings[$i]) { - case 'I': - $formatArray['font']['italic'] = true; - - break; - case 'D': - $formatArray['font']['bold'] = true; - - break; - case 'T': - $formatArray['borders']['top']['borderStyle'] = Border::BORDER_THIN; - - break; - case 'B': - $formatArray['borders']['bottom']['borderStyle'] = Border::BORDER_THIN; - - break; - case 'L': - $formatArray['borders']['left']['borderStyle'] = Border::BORDER_THIN; - - break; - case 'R': - $formatArray['borders']['right']['borderStyle'] = Border::BORDER_THIN; - - break; - } - } - - break; - } - } - $this->formats['P' . $this->format++] = $formatArray; - // Read cell value data + // Read shared styles + $this->processPRecord($rowData, $spreadsheet); } elseif ($dataType == 'C') { - $hasCalculatedValue = false; - $cellData = $cellDataFormula = ''; - foreach ($rowData as $rowDatum) { - switch ($rowDatum[0]) { - case 'C': - case 'X': - $column = substr($rowDatum, 1); - - break; - case 'R': - case 'Y': - $row = substr($rowDatum, 1); - - break; - case 'K': - $cellData = substr($rowDatum, 1); - - break; - case 'E': - $cellDataFormula = '=' . substr($rowDatum, 1); - // Convert R1C1 style references to A1 style references (but only when not quoted) - $temp = explode('"', $cellDataFormula); - $key = false; - foreach ($temp as &$value) { - // Only count/replace in alternate array entries - if ($key = !$key) { - preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); - // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way - // through the formula from left to right. Reversing means that we work right to left.through - // the formula - $cellReferences = array_reverse($cellReferences); - // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, - // then modify the formula to use that new reference - foreach ($cellReferences as $cellReference) { - $rowReference = $cellReference[2][0]; - // Empty R reference is the current row - if ($rowReference == '') { - $rowReference = $row; - } - // Bracketed R references are relative to the current row - if ($rowReference[0] == '[') { - $rowReference = $row + trim($rowReference, '[]'); - } - $columnReference = $cellReference[4][0]; - // Empty C reference is the current column - if ($columnReference == '') { - $columnReference = $column; - } - // Bracketed C references are relative to the current column - if ($columnReference[0] == '[') { - $columnReference = $column + trim($columnReference, '[]'); - } - $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference; - - $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); - } - } - } - unset($value); - // Then rebuild the formula string - $cellDataFormula = implode('"', $temp); - $hasCalculatedValue = true; - - break; - } - } - $columnLetter = Coordinate::stringFromColumnIndex($column); - $cellData = Calculation::unwrapResult($cellData); - - // Set cell value - $spreadsheet->getActiveSheet()->getCell($columnLetter . $row)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData); - if ($hasCalculatedValue) { - $cellData = Calculation::unwrapResult($cellData); - $spreadsheet->getActiveSheet()->getCell($columnLetter . $row)->setCalculatedValue($cellData); - } - // Read cell formatting + // Read cell value data + $this->processCRecord($rowData, $spreadsheet, $row, $column); } elseif ($dataType == 'F') { - $formatStyle = $columnWidth = $styleSettings = ''; - $styleData = []; - foreach ($rowData as $rowDatum) { - switch ($rowDatum[0]) { - case 'C': - case 'X': - $column = substr($rowDatum, 1); - - break; - case 'R': - case 'Y': - $row = substr($rowDatum, 1); - - break; - case 'P': - $formatStyle = $rowDatum; - - break; - case 'W': - [$startCol, $endCol, $columnWidth] = explode(' ', substr($rowDatum, 1)); - - break; - case 'S': - $styleSettings = substr($rowDatum, 1); - $iMax = strlen($styleSettings); - for ($i = 0; $i < $iMax; ++$i) { - switch ($styleSettings[$i]) { - case 'I': - $styleData['font']['italic'] = true; - - break; - case 'D': - $styleData['font']['bold'] = true; - - break; - case 'T': - $styleData['borders']['top']['borderStyle'] = Border::BORDER_THIN; - - break; - case 'B': - $styleData['borders']['bottom']['borderStyle'] = Border::BORDER_THIN; - - break; - case 'L': - $styleData['borders']['left']['borderStyle'] = Border::BORDER_THIN; - - break; - case 'R': - $styleData['borders']['right']['borderStyle'] = Border::BORDER_THIN; - - break; - } - } - - break; - } - } - if (($formatStyle > '') && ($column > '') && ($row > '')) { - $columnLetter = Coordinate::stringFromColumnIndex($column); - if (isset($this->formats[$formatStyle])) { - $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]); - } - } - if ((!empty($styleData)) && ($column > '') && ($row > '')) { - $columnLetter = Coordinate::stringFromColumnIndex($column); - $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData); - } - if ($columnWidth > '') { - if ($startCol == $endCol) { - $startCol = Coordinate::stringFromColumnIndex($startCol); - $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth($columnWidth); - } else { - $startCol = Coordinate::stringFromColumnIndex($startCol); - $endCol = Coordinate::stringFromColumnIndex($endCol); - $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth($columnWidth); - do { - $spreadsheet->getActiveSheet()->getColumnDimension(++$startCol)->setWidth($columnWidth); - } while ($startCol != $endCol); - } - } + // Read cell formatting + $this->processFRecord($rowData, $spreadsheet, $row, $column); } else { - foreach ($rowData as $rowDatum) { - switch ($rowDatum[0]) { - case 'C': - case 'X': - $column = substr($rowDatum, 1); - - break; - case 'R': - case 'Y': - $row = substr($rowDatum, 1); - - break; - } - } + $this->columnRowFromRowData($rowData, $column, $row); } } @@ -463,6 +552,18 @@ class Slk extends BaseReader return $spreadsheet; } + private function columnRowFromRowData(array $rowData, string &$column, string &$row): void + { + foreach ($rowData as $rowDatum) { + $char0 = $rowDatum[0]; + if ($char0 === 'X' || $char0 == 'C') { + $column = substr($rowDatum, 1); + } elseif ($char0 === 'Y' || $char0 == 'R') { + $row = substr($rowDatum, 1); + } + } + } + /** * Get sheet index. * diff --git a/tests/PhpSpreadsheetTests/Reader/CsvTest.php b/tests/PhpSpreadsheetTests/Reader/CsvTest.php index e4ccd931..e11e0ff7 100644 --- a/tests/PhpSpreadsheetTests/Reader/CsvTest.php +++ b/tests/PhpSpreadsheetTests/Reader/CsvTest.php @@ -255,6 +255,10 @@ EOF; self::assertEquals('\'', $reader->getEnclosure()); $reader->setEnclosure(''); self::assertEquals('"', $reader->getEnclosure()); + // following tests from BaseReader + self::assertTrue($reader->getReadEmptyCells()); + self::assertFalse($reader->getIncludeCharts()); + self::assertNull($reader->getLoadSheetsOnly()); } public function testReadEmptyFileName(): void diff --git a/tests/PhpSpreadsheetTests/Reader/SlkTest.php b/tests/PhpSpreadsheetTests/Reader/SlkTest.php new file mode 100644 index 00000000..4c7cc513 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/SlkTest.php @@ -0,0 +1,134 @@ +listWorkSheetInfo(self::$testbook); + $info0 = $workSheetInfo[0]; + self::assertEquals('SylkTest', $info0['worksheetName']); + self::assertEquals('J', $info0['lastColumnLetter']); + self::assertEquals(9, $info0['lastColumnIndex']); + self::assertEquals(18, $info0['totalRows']); + self::assertEquals(10, $info0['totalColumns']); + } + + public function testBadFileName(): void + { + $this->expectException(ReaderException::class); + $reader = new Slk(); + self::assertNull($reader->setLoadSheetsOnly(null)->getLoadSheetsOnly()); + $reader->listWorkSheetInfo(self::$testbook . 'xxx'); + } + + public function testBadFileName2(): void + { + $reader = new Slk(); + self::assertFalse($reader->canRead(self::$testbook . 'xxx')); + } + + public function testNotSylkFile(): void + { + $this->expectException(ReaderException::class); + $reader = new Slk(); + $reader->listWorkSheetInfo(__FILE__); + } + + public function testLoadSlk(): void + { + $reader = new Slk(); + $spreadsheet = $reader->load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('SylkTest', $sheet->getTitle()); + + self::assertEquals('FFFF0000', $sheet->getCell('A1')->getStyle()->getFont()->getColor()->getARGB()); + self::assertEquals(Fill::FILL_PATTERN_GRAY125, $sheet->getCell('A2')->getStyle()->getFill()->getFillType()); + self::assertEquals(Font::UNDERLINE_SINGLE, $sheet->getCell('A4')->getStyle()->getFont()->getUnderline()); + self::assertEquals('Test with (;) in string', $sheet->getCell('A4')->getValue()); + + self::assertEquals(22269, $sheet->getCell('A10')->getValue()); + self::assertEquals('dd/mm/yyyy', $sheet->getCell('A10')->getStyle()->getNumberFormat()->getFormatCode()); + self::assertEquals('19/12/1960', $sheet->getCell('A10')->getFormattedValue()); + self::assertEquals(1.5, $sheet->getCell('A11')->getValue()); + self::assertEquals('# ?/?', $sheet->getCell('A11')->getStyle()->getNumberFormat()->getFormatCode()); + self::assertEquals('1 1/2', $sheet->getCell('A11')->getFormattedValue()); + + self::assertEquals('=B1+C1', $sheet->getCell('H1')->getValue()); + self::assertEquals('=E2&F2', $sheet->getCell('J2')->getValue()); + self::assertEquals('=SUM(C1:C4)', $sheet->getCell('I5')->getValue()); + self::assertEquals('=MEDIAN(B6:B8)', $sheet->getCell('B9')->getValue()); + + self::assertEquals(11, $sheet->getCell('E1')->getStyle()->getFont()->getSize()); + self::assertTrue($sheet->getCell('E1')->getStyle()->getFont()->getBold()); + self::assertTrue($sheet->getCell('E1')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_SINGLE, $sheet->getCell('E1')->getStyle()->getFont()->getUnderline()); + self::assertFalse($sheet->getCell('E2')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('E2')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('E2')->getStyle()->getFont()->getUnderline()); + self::assertTrue($sheet->getCell('E3')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('E3')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('E3')->getStyle()->getFont()->getUnderline()); + self::assertFalse($sheet->getCell('E4')->getStyle()->getFont()->getBold()); + self::assertTrue($sheet->getCell('E4')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('E4')->getStyle()->getFont()->getUnderline()); + + self::assertTrue($sheet->getCell('F1')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('F1')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_SINGLE, $sheet->getCell('F1')->getStyle()->getFont()->getUnderline()); + self::assertFalse($sheet->getCell('F2')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('F2')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('F2')->getStyle()->getFont()->getUnderline()); + self::assertTrue($sheet->getCell('F3')->getStyle()->getFont()->getBold()); + self::assertTrue($sheet->getCell('F3')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('F3')->getStyle()->getFont()->getUnderline()); + self::assertFalse($sheet->getCell('F4')->getStyle()->getFont()->getBold()); + self::assertFalse($sheet->getCell('F4')->getStyle()->getFont()->getItalic()); + self::assertEquals(Font::UNDERLINE_NONE, $sheet->getCell('F4')->getStyle()->getFont()->getUnderline()); + + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C10')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C10')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C10')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C10')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C12')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C12')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C12')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C12')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C14')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C14')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C14')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C14')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C16')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C16')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C16')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $sheet->getCell('C16')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C18')->getStyle()->getBorders()->getTop()->getBorderStyle()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C18')->getStyle()->getBorders()->getRight()->getBorderStyle()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C18')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('C18')->getStyle()->getBorders()->getLeft()->getBorderStyle()); + // Have not yet figured out how C6/C7 are centred + } + + public function testSheetIndex(): void + { + $reader = new Slk(); + $sheetIndex = 2; + $reader->setSheetIndex($sheetIndex); + self::assertEquals($sheetIndex, $reader->getSheetIndex()); + $spreadsheet = $reader->load(self::$testbook); + $sheet = $spreadsheet->setActiveSheetIndex($sheetIndex); + self::assertEquals('SylkTest', $sheet->getTitle()); + + self::assertEquals('FFFF0000', $sheet->getCell('A1')->getStyle()->getFont()->getColor()->getARGB()); + } +} From 35c04964cf63a06561a5aeb199853f4b8e59b135 Mon Sep 17 00:00:00 2001 From: tyomitch Date: Fri, 19 Jun 2020 20:38:06 +0200 Subject: [PATCH 029/153] #1504: Using non-UTC timezone corrupts dates imported from XML spreadsheet (#1506) --- src/PhpSpreadsheet/Reader/Xml.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 15fb34ad..f9ad5475 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -516,7 +516,7 @@ class Xml extends BaseReader break; case 'DateTime': $type = DataType::TYPE_NUMERIC; - $cellValue = Date::PHPToExcel(strtotime($cellValue)); + $cellValue = Date::PHPToExcel(strtotime($cellValue . ' UTC')); break; case 'Error': From d8b4c3b26e4905efb4cc89b44abf3c3541c4cc72 Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 11:40:28 -0700 Subject: [PATCH 030/153] Fix for #1533 (#1534) Code assumes that formula whose result starts with # indicates error. Change to check entire result against error list in Functions. --- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 2 +- .../Writer/Xlsx/StartsWithHashTest.php | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/StartsWithHashTest.php diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index d101bb40..be064256 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1112,7 +1112,7 @@ class Worksheet extends WriterPart { $calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $pCell->getCalculatedValue() : $cellValue; if (is_string($calculatedValue)) { - if (substr($calculatedValue, 0, 1) === '#') { + if (\PhpOffice\PhpSpreadsheet\Calculation\Functions::isError($calculatedValue)) { $this->writeCellError($objWriter, 'e', $cellValue, $calculatedValue); return; diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/StartsWithHashTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/StartsWithHashTest.php new file mode 100644 index 00000000..d4fe5b22 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/StartsWithHashTest.php @@ -0,0 +1,62 @@ +getActiveSheet(); + $sheet->setCellValueExplicit('A1', '#define M', DataType::TYPE_STRING); + $sheet->setCellValue('A2', '=A1'); + $sheet->setCellValue('A3', '=UNKNOWNFUNC()'); + + $writer = new Writer($spreadsheet); + $writer->save($outputFilename); + + $reader = new Reader(); + $sheet = $reader->load($outputFilename); + unlink($outputFilename); + + self::assertSame('#define M', $sheet->getActiveSheet()->getCell('A1')->getValue()); + self::assertSame('#define M', $sheet->getActiveSheet()->getCell('A2')->getCalculatedValue()); + self::assertSame('f', $sheet->getActiveSheet()->getCell('A3')->getDataType()); + self::assertSame('#NAME?', $sheet->getActiveSheet()->getCell('A3')->getCalculatedValue()); + self::assertSame('f', $sheet->getActiveSheet()->getCell('A3')->getDataType()); + } + + public function testStartWithHashReadRaw(): void + { + // Make sure raw data indicates A3 is an error, but A2 isn't. + $outputFilename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + Settings::setLibXmlLoaderOptions(null); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValueExplicit('A1', '#define M', DataType::TYPE_STRING); + $sheet->setCellValue('A2', '=A1'); + $sheet->setCellValue('A3', '=UNKNOWNFUNC()'); + + $writer = new Writer($spreadsheet); + $writer->save($outputFilename); + $zip = new ZipArchive(); + $zip->open($outputFilename); + $resultSheet1Raw = $zip->getFromName('xl/worksheets/sheet1.xml'); + $zip->close(); + unlink($outputFilename); + + self::assertStringContainsString('', $resultSheet1Raw); + self::assertStringContainsString('', $resultSheet1Raw); + } +} From 73c336ac96cbdbf7a3cd092849603a1ef6894c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20J=C3=B8rgensen?= Date: Fri, 19 Jun 2020 20:51:46 +0200 Subject: [PATCH 031/153] Fix exact MATCH on ranges with empty cells (#1520) Fixes a bug when doing exact match on ranges with empty cells. ```php getActiveSheet(); // Row: 1, null, 4, null, 8. $sheet->getCell('A1')->setValue(1); $sheet->getCell('A3')->setValue(4); $sheet->getCell('A5')->setValue(8); $sheet->getCell('B1')->setValue('=MATCH(4, A1:A5, 1)'); // Should echo 3, but echos '#N/A'. echo $sheet->getCell('B1')->getCalculatedValue() . PHP_EOL; // Row: 1, null, 4, null, null. $sheet->getCell('C1')->setValue(1); $sheet->getCell('C3')->setValue(4); $sheet->getCell('D1')->setValue('=MATCH(5, C1:C5, 1)'); // Should echo 3, but echos '#N/A'. echo $sheet->getCell('D1')->getCalculatedValue() . PHP_EOL; ``` --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Calculation/LookupRef.php | 16 ++++++++-------- tests/data/Calculation/LookupRef/MATCH.php | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f4f3dc..fba9738f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Resolve evaluation of utf-8 named ranges in calculation engine [#1522](https://github.com/PHPOffice/PhpSpreadsheet/pull/1522) +- Fix exact MATCH on ranges with empty cells [#1520](https://github.com/PHPOffice/PhpSpreadsheet/pull/1520) ## [1.13.0] - 2020-05-31 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index f8272404..09042e2c 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -485,6 +485,13 @@ class LookupRef return Functions::NA(); } + if ($matchType == 1) { + // If match_type is 1 the list has to be processed from last to first + + $lookupArray = array_reverse($lookupArray); + $keySet = array_reverse(array_keys($lookupArray)); + } + // Lookup_array should contain only number, text, or logical values, or empty (null) cells foreach ($lookupArray as $i => $lookupArrayValue) { // check the type of the value @@ -498,17 +505,10 @@ class LookupRef $lookupArray[$i] = StringHelper::strToLower($lookupArrayValue); } if (($lookupArrayValue === null) && (($matchType == 1) || ($matchType == -1))) { - $lookupArray = array_slice($lookupArray, 0, $i - 1); + unset($lookupArray[$i]); } } - if ($matchType == 1) { - // If match_type is 1 the list has to be processed from last to first - - $lookupArray = array_reverse($lookupArray); - $keySet = array_reverse(array_keys($lookupArray)); - } - // ** // find the match // ** diff --git a/tests/data/Calculation/LookupRef/MATCH.php b/tests/data/Calculation/LookupRef/MATCH.php index b39edb9f..d9f0a83d 100644 --- a/tests/data/Calculation/LookupRef/MATCH.php +++ b/tests/data/Calculation/LookupRef/MATCH.php @@ -97,6 +97,20 @@ return [ -1, ], + // match on ranges with empty cells + [ + 3, // Expected + 4, // Input + [1, null, 4, null, 8], + 1, + ], + [ + 3, // Expected + 5, // Input + [1, null, 4, null, null], + 1, + ], + // 0s are causing errors, because things like 0 == 'x' is true. Thanks PHP! [ 3, From 1a44ef91095ea4923b63c29d16887923760f6d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20J=C3=B8rgensen?= Date: Fri, 19 Jun 2020 20:54:04 +0200 Subject: [PATCH 032/153] Fix MATCH when comparing different numeric types (#1521) Let MATCH compare numerics of different type (e.g. integers and floats). ```php getActiveSheet(); // Row: 1, 2, 3, 4, 5. MATCH for 4.6. $sheet->getCell('A1')->setValue(1); $sheet->getCell('A2')->setValue(2); $sheet->getCell('A3')->setValue(3); $sheet->getCell('A4')->setValue(4); $sheet->getCell('A5')->setValue(5); $sheet->getCell('B1')->setValue('=MATCH(4.6, A1:A5, 1)'); // Should echo 4, but echos '#N/A'. echo $sheet->getCell('B1')->getCalculatedValue() . PHP_EOL; // Row: 1, 2, 3, 3.8, 5. MATCH for 4. $sheet->getCell('C1')->setValue(1); $sheet->getCell('C2')->setValue(2); $sheet->getCell('C3')->setValue(3); $sheet->getCell('C4')->setValue(3.8); $sheet->getCell('C5')->setValue(5); $sheet->getCell('D1')->setValue('=MATCH(4, C1:C5, 1)'); // Should echo 4, but echos 3. echo $sheet->getCell('D1')->getCalculatedValue() . PHP_EOL; ``` Co-authored-by: Mark Baker --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Calculation/LookupRef.php | 2 +- tests/data/Calculation/LookupRef/MATCH.php | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fba9738f..aee16380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Resolve evaluation of utf-8 named ranges in calculation engine [#1522](https://github.com/PHPOffice/PhpSpreadsheet/pull/1522) +- Fix MATCH when comparing different numeric types [#1521](https://github.com/PHPOffice/PhpSpreadsheet/pull/1521) - Fix exact MATCH on ranges with empty cells [#1520](https://github.com/PHPOffice/PhpSpreadsheet/pull/1520) ## [1.13.0] - 2020-05-31 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 09042e2c..904eb24b 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -515,7 +515,7 @@ class LookupRef if ($matchType === 0 || $matchType === 1) { foreach ($lookupArray as $i => $lookupArrayValue) { - $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); + $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) || (is_numeric($lookupValue) && is_numeric($lookupArrayValue))); $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue; $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue; $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch; diff --git a/tests/data/Calculation/LookupRef/MATCH.php b/tests/data/Calculation/LookupRef/MATCH.php index d9f0a83d..671056dd 100644 --- a/tests/data/Calculation/LookupRef/MATCH.php +++ b/tests/data/Calculation/LookupRef/MATCH.php @@ -179,6 +179,19 @@ return [ [true, false, 'a', 'z', 222222, 2, 99999999], -1, ], + // when mixing numeric types + [ + 4, // Expected + 4.6, + [1, 2, 3, 4, 5], + 1, + ], + [ + 4, // Expected + 4, + [1, 2, 3, 3.8, 5], + 1, + ], // if element of same data type met and it is < than searched one #N/A - no further processing [ '#N/A', // Expected From 38441863971872e5d291d80223697c28bd5a95eb Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 11:57:20 -0700 Subject: [PATCH 033/153] Fix for Issue 1495 (#1500) #1495 reports that ActiveSheet can change when calculation involves jumping around between sheets. Save index before calculation, restore after, add test. --- src/PhpSpreadsheet/Cell/Cell.php | 2 ++ tests/PhpSpreadsheetTests/Cell/CellTest.php | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 0bca2fc0..74ed9268 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -251,9 +251,11 @@ class Cell { if ($this->dataType == DataType::TYPE_FORMULA) { try { + $index = $this->getWorksheet()->getParent()->getActiveSheetIndex(); $result = Calculation::getInstance( $this->getWorksheet()->getParent() )->calculateCellValue($this, $resetLog); + $this->getWorksheet()->getParent()->setActiveSheetIndex($index); // We don't yet handle array returns if (is_array($result)) { while (is_array($result)) { diff --git a/tests/PhpSpreadsheetTests/Cell/CellTest.php b/tests/PhpSpreadsheetTests/Cell/CellTest.php index 8c95e864..0d9ce337 100644 --- a/tests/PhpSpreadsheetTests/Cell/CellTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CellTest.php @@ -46,4 +46,23 @@ class CellTest extends TestCase { return require 'tests/data/Cell/SetValueExplicitException.php'; } + + public function testNoChangeToActiveSheet(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Sheet 1'); + $sheet3 = $spreadsheet->createSheet(); + $sheet3->setTitle('Sheet 3'); + $sheet1->setCellValue('C1', 123); + $sheet1->setCellValue('D1', 124); + $sheet3->setCellValue('A1', "='Sheet 1'!C1+'Sheet 1'!D1"); + $sheet1->setCellValue('A1', "='Sheet 3'!A1"); + $cell = 'A1'; + $spreadsheet->setActiveSheetIndex(0); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); + $value = $spreadsheet->getActiveSheet()->getCell($cell)->getCalculatedValue(); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); + self::assertEquals(247, $value); + } } From 38fab4e6329df8ba857afa24fd686bc5369a116b Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 12:01:18 -0700 Subject: [PATCH 034/153] Fix for #1505 (#1525) This problem is the same as #1238, which was resolved by #1239. For that issue, the fix was to check in one place whether $this->mapCellXfIndex[$xfIndex] was set before using it. The sample spreadsheet supplied as a description for this problem had exactly the same problem in 2 other places in the code. In addition, there were 7 other places in the code where that particular item was used unchecked. This fix corrects all 9 locations. The spreadsheet supplied with the problem is used as the basis for some new tests, which particularly test column dimensions and styles, the problems involved in this case. --- src/PhpSpreadsheet/Reader/Xls.php | 22 ++++++++------ tests/PhpSpreadsheetTests/Reader/XlsTest.php | 30 +++++++++++++++++-- tests/data/Reader/XLS/bug1505.xls | Bin 0 -> 20992 bytes 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 tests/data/Reader/XLS/bug1505.xls diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index b206f8ac..3f383b9e 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -3622,7 +3622,9 @@ class Xls extends BaseReader $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden); $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level); $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed); - $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]); + if (isset($this->mapCellXfIndex[$xfIndex])) { + $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } } } } @@ -3731,7 +3733,7 @@ class Xls extends BaseReader $numValue = self::getIEEE754($rknum); $cell = $this->phpSheet->getCell($columnString . ($row + 1)); - if (!$this->readDataOnly) { + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { // add style information $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); } @@ -3866,7 +3868,7 @@ class Xls extends BaseReader // offset: var; size: 4; RK value $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2)); $cell = $this->phpSheet->getCell($columnString . ($row + 1)); - if (!$this->readDataOnly) { + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { // add style $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); } @@ -3910,7 +3912,7 @@ class Xls extends BaseReader $numValue = self::extractNumber(substr($recordData, 6, 8)); $cell = $this->phpSheet->getCell($columnString . ($row + 1)); - if (!$this->readDataOnly) { + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { // add cell style $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); } @@ -4018,7 +4020,7 @@ class Xls extends BaseReader } $cell = $this->phpSheet->getCell($columnString . ($row + 1)); - if (!$this->readDataOnly) { + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { // add cell style $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); } @@ -4156,7 +4158,7 @@ class Xls extends BaseReader break; } - if (!$this->readDataOnly) { + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { // add cell style $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); } @@ -4194,7 +4196,9 @@ class Xls extends BaseReader // Read cell? if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i); - $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]); + if (isset($this->mapCellXfIndex[$xfIndex])) { + $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } } } } @@ -4245,7 +4249,7 @@ class Xls extends BaseReader $cell = $this->phpSheet->getCell($columnString . ($row + 1)); $cell->setValueExplicit($value, DataType::TYPE_STRING); - if (!$this->readDataOnly) { + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { // add cell style $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); } @@ -4277,7 +4281,7 @@ class Xls extends BaseReader $xfIndex = self::getUInt2d($recordData, 4); // add style information - if (!$this->readDataOnly && $this->readEmptyCells) { + if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) { $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]); } } diff --git a/tests/PhpSpreadsheetTests/Reader/XlsTest.php b/tests/PhpSpreadsheetTests/Reader/XlsTest.php index 77ad91fa..da39f8b2 100644 --- a/tests/PhpSpreadsheetTests/Reader/XlsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/XlsTest.php @@ -3,9 +3,9 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader; use PhpOffice\PhpSpreadsheet\Reader\Xls; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; -class XlsTest extends TestCase +class XlsTest extends AbstractFunctional { /** * Test load Xls file. @@ -17,4 +17,30 @@ class XlsTest extends TestCase $spreadsheet = $reader->load($filename); self::assertEquals('Title', $spreadsheet->getSheet(0)->getCell('A1')->getValue()); } + + /** + * Test load Xls file with invalid xfIndex. + */ + public function testLoadXlsBug1505(): void + { + $filename = 'tests/data/Reader/XLS/bug1505.xls'; + $reader = new Xls(); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $col = $sheet->getHighestColumn(); + $row = $sheet->getHighestRow(); + + $newspreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $newsheet = $newspreadsheet->getActiveSheet(); + $newcol = $newsheet->getHighestColumn(); + $newrow = $newsheet->getHighestRow(); + + self::assertEquals($spreadsheet->getSheetCount(), $newspreadsheet->getSheetCount()); + self::assertEquals($sheet->getTitle(), $newsheet->getTitle()); + self::assertEquals($sheet->getColumnDimensions(), $newsheet->getColumnDimensions()); + self::assertEquals($col, $newcol); + self::assertEquals($row, $newrow); + self::assertEquals($sheet->getCell('A1')->getFormattedValue(), $newsheet->getCell('A1')->getFormattedValue()); + self::assertEquals($sheet->getCell("$col$row")->getFormattedValue(), $newsheet->getCell("$col$row")->getFormattedValue()); + } } diff --git a/tests/data/Reader/XLS/bug1505.xls b/tests/data/Reader/XLS/bug1505.xls new file mode 100644 index 0000000000000000000000000000000000000000..3440ceb911192e86029bfd95eb2a8d7093765865 GIT binary patch literal 20992 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 Date: Fri, 19 Jun 2020 21:06:41 +0200 Subject: [PATCH 035/153] Fix HLOOKUP on single row (#1512) Fixes a bug when doing a HLOOKUP on a single row. ```php getActiveSheet(); /** * Single row. */ $singleRow = "=HLOOKUP(10, {5, 10, 15}, 1, 0)"; $sheet->getCell('A1')->setValue($singleRow); // Should echo 10, but echos '#N/A' and some PHP notices and warnings. echo $sheet->getCell('A1')->getCalculatedValue() . PHP_EOL; /** * Multiple rows. */ $multipleRows = "=HLOOKUP(10, {5, 10, 15; 20, 25, 30}, 1, 0)"; $sheet->getCell('A2')->setValue($multipleRows); // Should echo: 10 and also does. echo $sheet->getCell('A2')->getCalculatedValue() . PHP_EOL; ``` Co-authored-by: Mark Baker --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Calculation/LookupRef.php | 2 +- tests/data/Calculation/LookupRef/HLOOKUP.php | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aee16380..11a8b2ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Resolve evaluation of utf-8 named ranges in calculation engine [#1522](https://github.com/PHPOffice/PhpSpreadsheet/pull/1522) +- Fix HLOOKUP on single row [#1512](https://github.com/PHPOffice/PhpSpreadsheet/pull/1512) - Fix MATCH when comparing different numeric types [#1521](https://github.com/PHPOffice/PhpSpreadsheet/pull/1521) - Fix exact MATCH on ranges with empty cells [#1520](https://github.com/PHPOffice/PhpSpreadsheet/pull/1520) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 904eb24b..6e817006 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -807,7 +807,7 @@ class LookupRef return Functions::REF(); } $f = array_keys($lookup_array); - $firstRow = array_pop($f); + $firstRow = reset($f); if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array))) { return Functions::REF(); } diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index 644ddeba..b880f247 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -275,6 +275,19 @@ return [ 2, true, ], + [ + 3, + 3, + [ + [ + 1, + 2, + 3, + ], + ], + 1, + true, + ], [ 5, 'x', From b3d30f4cbc30c45eb83111ac9d23b05afe10cab3 Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 19 Jun 2020 12:08:36 -0700 Subject: [PATCH 036/153] Xls Writer - Correct Timestamp Bug, Improve Coverage (#1493) * Xls Writer - Correct Timestamp Bug, Improve Coverage I believe that Xls Writer is 100% covered now. The Xls Writer sets its timestamp incorrectly. The problem is actually in Shared/Ole::localDateToOLE, which converts its timestamp using gmmktime; mktime is correct. If I save a file at 3:00 p.m. in San Francisco, this bug means the time is actually recorded as 3:00 p.m. UTC. A consequence of this is that if you use Phpspreadsheet to read the file and save it as a new Xls, the creation timestamp goes further and further back in time with each generation (or further forward if east of Greenwich). One of the tests added confirms that the creation timestamp is consistent with the start and end times of the test. The major change in coverage is adding tests to save GIF and BMP images, which aren't supported in Xls, but are converted to PNG in the PhpSpreadsheet code. --- samples/images/bmp.bmp | Bin 0 -> 30186 bytes samples/images/gif.gif | Bin 0 -> 1578 bytes src/PhpSpreadsheet/Shared/OLE.php | 9 +- src/PhpSpreadsheet/Writer/Xls.php | 339 ++++++++---------- .../Writer/Xls/XlsGifBmpTest.php | 84 +++++ 5 files changed, 235 insertions(+), 197 deletions(-) create mode 100644 samples/images/bmp.bmp create mode 100644 samples/images/gif.gif create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php diff --git a/samples/images/bmp.bmp b/samples/images/bmp.bmp new file mode 100644 index 0000000000000000000000000000000000000000..01fee85ebcd7ca3dda1783f0d0b2c59494e34ac6 GIT binary patch literal 30186 zcmeHQEpHu35cILYKtsboqr=gmkdu5Y(9kgC&;djLguuW+lamY$1N}219WgY_mECcv z)HBmNA3GcSX`hsr*_rO@>e`;!*?BqLAAkP$_tDemcYMFYpMUV@FZ}uK=osHeNB{mD zf4cbj^<)2Rs{nN0HsBuZ$FBMIMX+nWYree#+eh5%eAj$?1-6g4*ZHpb_6lqtaj)}T z^X(PbKH}lk`PJ1GlDfFK*j~)ah=-^7n>TNe)Jx9mC3CAR8)x>U_pZ59W$xwiu^piv zU0+|%jns>Ncy<2i(&lRp$B~p61`be>crvY-%r-*hr~S^M{9r5u8cB2dO!i z^p77uHtDv_KJy?o|NQwgOT9_+ZT6W5sX27+)4b2C>9!iTR+m>{-=|)mSF0e1((3Xm z?EBQ~^J*1j*IX6bHCHuS+kNWwd3A7Wb$J!`ed^I`>airEyb29lNm9KgM|w?_Wh!`6 zzUvxOy(U>8`}JTOh6PjfvS8|RR+vYtS5Gq}9GB!6wJ zzJvYx^{dK0SZ1u))?eu8xTs7?(#8H7RIR_zX{lGnYBj6T z9GG9ee9;Qu$`)h?6X#NbRWXB72O6uwA&*~<>^%^@+g(d8%>uA# zXH!o}bAV9Hz(&oQ;#L}yL#4el>vpH3o8wlBXJ=<4+lD|-#tm$cBh6gCt6UC(ce~Tk z&2jgV()WA^cu5?_4Qz0`X;#7+=^q5|cBdm6%3qfqvvCtajZ1BZsb;0PYQyIgwwKz! zR+}990Qod*Rfdir;Fb~hO5X9}HljJma3_z~s6JU@FXrAVn!&Op+!7H7#i-VE_U&Wj z#jRU&2o5qH!?gX?f6gtT^Ys1l!`&#{RAe0sj z(gu`gX9pkpTBC%Z3p*?q7a7=;pfsYh(M;XY>X)REMdk`%&vCbrqi>LA7S=k|9qnN0 z(1m?5R$!njl_Q3K4gp7e=IvM*(E{O{J=);HqR;WH^6FLDRs}#H-kcz4OqD@Pxxj!W zg1 z5f*uNx`96p0P^kIw^oL?x3`RLjTnLRBAyX|3UC;aHn@XK1O?Cjc?~Aw-Us!Wf_a@K zfVbDyO@+f@^cv0eVH%WrljgwL;N8<@t%+#;aOukyIYCP9G>98|*J7;+WO>c1tKc+#pPVm|9o+(>0;ZWMc zA=jWK8g8iOQlD!zk1&>c>0BzM>v_ZC<$iGBz5F-;K3F%xH%gV7D{Ur=#^QS} z6CZ3asNt}d;s>Z1<4A<=Xw<9^DO<>{An}dURXvp*I4M4R7)3$YX^l8Kp#~caYB-#C ziw{t>3!Y3skGa<@ESk<>TQ@M^Q`(kjl&tKPnrF=v z5C&5V9{Bb_My(5riZIyL<)ySOaUoPCTal26v(d}vL?8dwezx6$Gf#FhL1=B0$u^Dj z)Qe;)8^%9it2lv~-bz#gh~ac7u zQ%&VyZ8AxRDtnqVN4|@j6G-6}J*+mBWCAm0Y7LvDf&IK8K1OTyEm;~(npf^@xtBA2 zD!sDwtW($UwJ?Kl@es31_s_8-M=LeQqJgD@Ue-X(^M_HB!hL7G97xS2*Kotfv!ljq zl8qjx^o58u=R9&^)?BVQ{olQN{C|qsXGPdP#o&eA%lr55ySE2OBna(ZHlOoe{+rL} z0K9&PZvpGV))QFQGS(cim~U3-wN{UHlX~H3i<-Z1{;!eVuKD(NZP$F)e0v49kBB<| EA4~Nmpa1{> literal 0 HcmV?d00001 diff --git a/samples/images/gif.gif b/samples/images/gif.gif new file mode 100644 index 0000000000000000000000000000000000000000..4cf06035b8a0e830eafd83733c77eee7bcb06d0b GIT binary patch literal 1578 zcmXYwdr;8F5yzJpThh!p0w!rhQSCJ1q)yiEP-XP0#?BRI0Q%F7@UAp@Bkjc z6L%G&w15`T5?V$p zXcZlxLv)0W(Fr<559kp+p=b1hUNHa$#6TDrgJ4ipKt)tSWmG|pbVOq#0TD2PM6$>L z86-nwn2eB7vOpHe5?LlIWR)C{Lvlop$q6|n59E2OWAVpFl zWl|xH5Jbe0ff$%UA|h;n4YDCN%tqKKTVRW9i7m4gw#p9JAv%3OqGA*U18PtW zsbMvuM%98^R7+}Et*BLXpbpiMI#ws@R6VFi^`xHFi+a@n8c+jiU=5<-ezPcOk=qgS zXzu?u|9|eIZQz{;V*u{Lp!-+%{{+M;bU*ZD=HcCqC9w%9Q$9Jo=fv8+iQ}p=kG$Qq z;m0_y?UN(br#3x3di|4G@4eGpnKaQ)X?}0-={JVH(o&UmwC2oP!7~{89!TQ+x6@KnKYVGy#>!>$Rr;SIo-=<4{e(~msGhgPu zu=9fn-!E$4cWqkLt3`K`|5knW)6K6fzKeJEZFY}m+es^GV~ReX25qf7R=DJX&u7B; zmAeb#x1W4^ReD_M=C!@)LVW2DDvB!Bb^PYvJ4zq&MG5PBHNN@O2GiWIX=D7>+<^tp z))Y;ArO(aTiRJNeg>hxuzq1LpH8$(3(JxP}Kll33>jeYH%v+J$UfE~qob!drFa87eN^uyt9_byvOUc)^%y54fa^ozJIK?mhHs z{&2f%Kz;GdioFA7n@wd`9_y(1Q_rROyXW4#@b&1V&X$B-@o~5{=C5NEwV)k`U z@f`^zt=mr3)U7Ej{(H`aEj=AIjgQt%-t)qpH4EPF&9U<= 0.0 ? PHP_INT_MAX : PHP_INT_MIN; + // Overflow conditions can't happen on 64-bit system + return ($iTimestamp == $unixTimestamp) ? $iTimestamp : ($unixTimestamp >= 0.0 ? PHP_INT_MAX : PHP_INT_MIN); } } diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index cf87d5be..4f4b256a 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -23,7 +23,6 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; -use RuntimeException; class Xls extends BaseWriter { @@ -389,13 +388,94 @@ class Xls extends BaseWriter } } - /** - * Build the Escher object corresponding to the MSODRAWINGGROUP record. - */ - private function buildWorkbookEscher(): void + private function processMemoryDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing, string $renderingFunctionx): void { - $escher = null; + switch ($renderingFunctionx) { + case MemoryDrawing::RENDERING_JPEG: + $blipType = BSE::BLIPTYPE_JPEG; + $renderingFunction = 'imagejpeg'; + break; + default: + $blipType = BSE::BLIPTYPE_PNG; + $renderingFunction = 'imagepng'; + + break; + } + + ob_start(); + call_user_func($renderingFunction, $drawing->getImageResource()); + $blipData = ob_get_contents(); + ob_end_clean(); + + $blip = new Blip(); + $blip->setData($blipData); + + $BSE = new BSE(); + $BSE->setBlipType($blipType); + $BSE->setBlip($blip); + + $bstoreContainer->addBSE($BSE); + } + + private function processDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void + { + $blipData = ''; + $filename = $drawing->getPath(); + + [$imagesx, $imagesy, $imageFormat] = getimagesize($filename); + + switch ($imageFormat) { + case 1: // GIF, not supported by BIFF8, we convert to PNG + $blipType = BSE::BLIPTYPE_PNG; + ob_start(); + imagepng(imagecreatefromgif($filename)); + $blipData = ob_get_contents(); + ob_end_clean(); + + break; + case 2: // JPEG + $blipType = BSE::BLIPTYPE_JPEG; + $blipData = file_get_contents($filename); + + break; + case 3: // PNG + $blipType = BSE::BLIPTYPE_PNG; + $blipData = file_get_contents($filename); + + break; + case 6: // Windows DIB (BMP), we convert to PNG + $blipType = BSE::BLIPTYPE_PNG; + ob_start(); + imagepng(SharedDrawing::imagecreatefrombmp($filename)); + $blipData = ob_get_contents(); + ob_end_clean(); + + break; + } + if ($blipData) { + $blip = new Blip(); + $blip->setData($blipData); + + $BSE = new BSE(); + $BSE->setBlipType($blipType); + $BSE->setBlip($blip); + + $bstoreContainer->addBSE($BSE); + } + } + + private function processBaseDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void + { + if ($drawing instanceof Drawing) { + $this->processDrawing($bstoreContainer, $drawing); + } elseif ($drawing instanceof MemoryDrawing) { + $this->processMemoryDrawing($bstoreContainer, $drawing, $drawing->getRenderingFunction()); + } + } + + private function checkForDrawings(): bool + { // any drawings in this workbook? $found = false; foreach ($this->spreadsheet->getAllSheets() as $sheet) { @@ -406,8 +486,16 @@ class Xls extends BaseWriter } } + return $found; + } + + /** + * Build the Escher object corresponding to the MSODRAWINGGROUP record. + */ + private function buildWorkbookEscher(): void + { // nothing to do if there are no drawings - if (!$found) { + if (!$this->checkForDrawings()) { return; } @@ -429,17 +517,16 @@ class Xls extends BaseWriter foreach ($this->spreadsheet->getAllsheets() as $sheet) { $sheetCountShapes = 0; // count number of shapes (minus group shape), in sheet - if (count($sheet->getDrawingCollection()) > 0) { - ++$countDrawings; + $addCount = 0; + foreach ($sheet->getDrawingCollection() as $drawing) { + $addCount = 1; + ++$sheetCountShapes; + ++$totalCountShapes; - foreach ($sheet->getDrawingCollection() as $drawing) { - ++$sheetCountShapes; - ++$totalCountShapes; - - $spId = $sheetCountShapes | ($this->spreadsheet->getIndex($sheet) + 1) << 10; - $spIdMax = max($spId, $spIdMax); - } + $spId = $sheetCountShapes | ($this->spreadsheet->getIndex($sheet) + 1) << 10; + $spIdMax = max($spId, $spIdMax); } + $countDrawings += $addCount; } $dggContainer->setSpIdMax($spIdMax + 1); @@ -453,83 +540,7 @@ class Xls extends BaseWriter // the BSE's (all the images) foreach ($this->spreadsheet->getAllsheets() as $sheet) { foreach ($sheet->getDrawingCollection() as $drawing) { - if (!extension_loaded('gd')) { - throw new RuntimeException('Saving images in xls requires gd extension'); - } - if ($drawing instanceof Drawing) { - $filename = $drawing->getPath(); - - [$imagesx, $imagesy, $imageFormat] = getimagesize($filename); - - switch ($imageFormat) { - case 1: // GIF, not supported by BIFF8, we convert to PNG - $blipType = BSE::BLIPTYPE_PNG; - ob_start(); - imagepng(imagecreatefromgif($filename)); - $blipData = ob_get_contents(); - ob_end_clean(); - - break; - case 2: // JPEG - $blipType = BSE::BLIPTYPE_JPEG; - $blipData = file_get_contents($filename); - - break; - case 3: // PNG - $blipType = BSE::BLIPTYPE_PNG; - $blipData = file_get_contents($filename); - - break; - case 6: // Windows DIB (BMP), we convert to PNG - $blipType = BSE::BLIPTYPE_PNG; - ob_start(); - imagepng(SharedDrawing::imagecreatefrombmp($filename)); - $blipData = ob_get_contents(); - ob_end_clean(); - - break; - default: - continue 2; - } - - $blip = new Blip(); - $blip->setData($blipData); - - $BSE = new BSE(); - $BSE->setBlipType($blipType); - $BSE->setBlip($blip); - - $bstoreContainer->addBSE($BSE); - } elseif ($drawing instanceof MemoryDrawing) { - switch ($drawing->getRenderingFunction()) { - case MemoryDrawing::RENDERING_JPEG: - $blipType = BSE::BLIPTYPE_JPEG; - $renderingFunction = 'imagejpeg'; - - break; - case MemoryDrawing::RENDERING_GIF: - case MemoryDrawing::RENDERING_PNG: - case MemoryDrawing::RENDERING_DEFAULT: - $blipType = BSE::BLIPTYPE_PNG; - $renderingFunction = 'imagepng'; - - break; - } - - ob_start(); - call_user_func($renderingFunction, $drawing->getImageResource()); - $blipData = ob_get_contents(); - ob_end_clean(); - - $blip = new Blip(); - $blip->setData($blipData); - - $BSE = new BSE(); - $BSE->setBlipType($blipType); - $BSE->setBlip($blip); - - $bstoreContainer->addBSE($BSE); - } + $this->processBaseDrawing($bstoreContainer, $drawing); } } @@ -578,8 +589,8 @@ class Xls extends BaseWriter ++$dataSection_NumProps; // GKPIDDSI_CATEGORY : Category - if ($this->spreadsheet->getProperties()->getCategory()) { - $dataProp = $this->spreadsheet->getProperties()->getCategory(); + $dataProp = $this->spreadsheet->getProperties()->getCategory(); + if ($dataProp) { $dataSection[] = [ 'summary' => ['pack' => 'V', 'data' => 0x02], 'offset' => ['pack' => 'V'], @@ -707,11 +718,7 @@ class Xls extends BaseWriter $dataSection_Content_Offset += 4 + 4; } elseif ($dataProp['type']['data'] == 0x0B) { // Boolean - if ($dataProp['data']['data'] == false) { - $dataSection_Content .= pack('V', 0x0000); - } else { - $dataSection_Content .= pack('V', 0x0001); - } + $dataSection_Content .= pack('V', (int) $dataProp['data']['data']); $dataSection_Content_Offset += 4 + 4; } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length // Null-terminated string @@ -725,12 +732,12 @@ class Xls extends BaseWriter $dataSection_Content .= $dataProp['data']['data']; $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']); - } 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 { - // Data Type Not Used at the moment $dataSection_Content .= $dataProp['data']['data']; $dataSection_Content_Offset += 4 + $dataProp['data']['length']; @@ -752,6 +759,32 @@ class Xls extends BaseWriter return $data; } + private function writeSummaryPropOle(int $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void + { + if ($dataProp) { + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => $sumdata], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length + 'data' => ['data' => OLE::localDateToOLE($dataProp)], + ]; + ++$dataSection_NumProps; + } + } + + private function writeSummaryProp(string $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void + { + if ($dataProp) { + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => $sumdata], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + } + /** * Build the OLE Part for Summary Information. * @@ -792,94 +825,16 @@ class Xls extends BaseWriter ]; ++$dataSection_NumProps; - // Title - if ($this->spreadsheet->getProperties()->getTitle()) { - $dataProp = $this->spreadsheet->getProperties()->getTitle(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x02], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length - 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], - ]; - ++$dataSection_NumProps; - } - // Subject - if ($this->spreadsheet->getProperties()->getSubject()) { - $dataProp = $this->spreadsheet->getProperties()->getSubject(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x03], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length - 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], - ]; - ++$dataSection_NumProps; - } - // Author (Creator) - if ($this->spreadsheet->getProperties()->getCreator()) { - $dataProp = $this->spreadsheet->getProperties()->getCreator(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x04], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length - 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], - ]; - ++$dataSection_NumProps; - } - // Keywords - if ($this->spreadsheet->getProperties()->getKeywords()) { - $dataProp = $this->spreadsheet->getProperties()->getKeywords(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x05], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length - 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], - ]; - ++$dataSection_NumProps; - } - // Comments (Description) - if ($this->spreadsheet->getProperties()->getDescription()) { - $dataProp = $this->spreadsheet->getProperties()->getDescription(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x06], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length - 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], - ]; - ++$dataSection_NumProps; - } - // Last Saved By (LastModifiedBy) - if ($this->spreadsheet->getProperties()->getLastModifiedBy()) { - $dataProp = $this->spreadsheet->getProperties()->getLastModifiedBy(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x08], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length - 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], - ]; - ++$dataSection_NumProps; - } - // Created Date/Time - if ($this->spreadsheet->getProperties()->getCreated()) { - $dataProp = $this->spreadsheet->getProperties()->getCreated(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x0C], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x40], // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) - 'data' => ['data' => OLE::localDateToOLE($dataProp)], - ]; - ++$dataSection_NumProps; - } - // Modified Date/Time - if ($this->spreadsheet->getProperties()->getModified()) { - $dataProp = $this->spreadsheet->getProperties()->getModified(); - $dataSection[] = [ - 'summary' => ['pack' => 'V', 'data' => 0x0D], - 'offset' => ['pack' => 'V'], - 'type' => ['pack' => 'V', 'data' => 0x40], // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) - 'data' => ['data' => OLE::localDateToOLE($dataProp)], - ]; - ++$dataSection_NumProps; - } + $props = $this->spreadsheet->getProperties(); + $this->writeSummaryProp($props->getTitle(), $dataSection_NumProps, $dataSection, 0x02, 0x1e); + $this->writeSummaryProp($props->getSubject(), $dataSection_NumProps, $dataSection, 0x03, 0x1e); + $this->writeSummaryProp($props->getCreator(), $dataSection_NumProps, $dataSection, 0x04, 0x1e); + $this->writeSummaryProp($props->getKeywords(), $dataSection_NumProps, $dataSection, 0x05, 0x1e); + $this->writeSummaryProp($props->getDescription(), $dataSection_NumProps, $dataSection, 0x06, 0x1e); + $this->writeSummaryProp($props->getLastModifiedBy(), $dataSection_NumProps, $dataSection, 0x08, 0x1e); + $this->writeSummaryPropOle($props->getCreated(), $dataSection_NumProps, $dataSection, 0x0c, 0x40); + $this->writeSummaryPropOle($props->getModified(), $dataSection_NumProps, $dataSection, 0x0d, 0x40); + // Security $dataSection[] = [ 'summary' => ['pack' => 'V', 'data' => 0x13], diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php new file mode 100644 index 00000000..ceba7e54 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php @@ -0,0 +1,84 @@ +filename) { + unlink($this->filename); + } + $this->filename = ''; + } + + public function testBmp(): void + { + $pgmstart = time(); + $spreadsheet = new Spreadsheet(); + $filstart = $spreadsheet->getProperties()->getModified(); + self::assertLessThanOrEqual($filstart, $pgmstart); + + // Add a drawing to the worksheet + $drawing = new Drawing(); + $drawing->setName('Letters B, M, and P'); + $drawing->setDescription('Handwritten B, M, and P'); + $drawing->setPath(__DIR__ . '/../../../../samples/images/bmp.bmp'); + $drawing->setHeight(36); + $drawing->setWorksheet($spreadsheet->getActiveSheet()); + $drawing->setCoordinates('A1'); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $creationDatestamp = $reloadedSpreadsheet->getProperties()->getCreated(); + $filstart = $creationDatestamp; + $pSheet = $reloadedSpreadsheet->getActiveSheet(); + $drawings = $pSheet->getDrawingCollection(); + self::assertCount(1, $drawings); + foreach ($pSheet->getDrawingCollection() as $drawing) { + // See if Scrutinizer approves this + $mimeType = ($drawing instanceof MemoryDrawing) ? $drawing->getMimeType() : 'notmemorydrawing'; + self::assertEquals('image/png', $mimeType); + } + $pgmend = time(); + + self::assertLessThanOrEqual($pgmend, $pgmstart); + self::assertLessThanOrEqual($pgmend, $filstart); + self::assertLessThanOrEqual($filstart, $pgmstart); + } + + public function testGif(): void + { + $spreadsheet = new Spreadsheet(); + + // Add a drawing to the worksheet + $drawing = new Drawing(); + $drawing->setName('Letters G, I, and G'); + $drawing->setDescription('Handwritten G, I, and F'); + $drawing->setPath(__DIR__ . '/../../../../samples/images/gif.gif'); + $drawing->setHeight(36); + $drawing->setWorksheet($spreadsheet->getActiveSheet()); + $drawing->setCoordinates('A1'); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $pSheet = $reloadedSpreadsheet->getActiveSheet(); + $drawings = $pSheet->getDrawingCollection(); + self::assertCount(1, $drawings); + foreach ($pSheet->getDrawingCollection() as $drawing) { + $mimeType = ($drawing instanceof MemoryDrawing) ? $drawing->getMimeType() : 'notmemorydrawing'; + self::assertEquals('image/png', $mimeType); + } + } + + public function testInvalidTimestamp(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class); + \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE2LocalDate(' '); + } +} From c04d0185d969814cf792180b49b0a156d249e041 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 19 Jun 2020 22:11:07 +0200 Subject: [PATCH 037/153] Updates to changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a8b2ff..1b89e71a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix HLOOKUP on single row [#1512](https://github.com/PHPOffice/PhpSpreadsheet/pull/1512) - Fix MATCH when comparing different numeric types [#1521](https://github.com/PHPOffice/PhpSpreadsheet/pull/1521) - Fix exact MATCH on ranges with empty cells [#1520](https://github.com/PHPOffice/PhpSpreadsheet/pull/1520) +- Fix for Issue [#1516](https://github.com/PHPOffice/PhpSpreadsheet/issues/1516) (Cloning worksheet makes corrupted Xlsx) [#1530](https://github.com/PHPOffice/PhpSpreadsheet/pull/1530) +- Fix For Issue [#1509](https://github.com/PHPOffice/PhpSpreadsheet/issues/1509) (Can not set empty enclosure for CSV) [#1518](https://github.com/PHPOffice/PhpSpreadsheet/pull/1518) +- Fix for Issue [#1505](https://github.com/PHPOffice/PhpSpreadsheet/issues/1505) (TypeError : Argument 4 passed to PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet::writeAttributeIf() must be of the type string) [#1525](https://github.com/PHPOffice/PhpSpreadsheet/pull/1525) +- Fix for Issue [#1495](https://github.com/PHPOffice/PhpSpreadsheet/issues/1495) (Sheet index being changed when multiple sheets are used in formula) [#1500]((https://github.com/PHPOffice/PhpSpreadsheet/pull/1500)) +- Fix for Issue [#1533](https://github.com/PHPOffice/PhpSpreadsheet/issues/1533) (A reference to a cell containing a string starting with "#" leads to errors in the generated xlsx.) [#1534]((https://github.com/PHPOffice/PhpSpreadsheet/pull/1534) +- Xls Writer - Correct Timestamp Bug [#1493](https://github.com/PHPOffice/PhpSpreadsheet/pull/1493) ## [1.13.0] - 2020-05-31 From acd2ba01dfeac09e5e2b03d62c3175b36e05fe54 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 19 Jun 2020 22:38:31 +0200 Subject: [PATCH 038/153] Updates to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b89e71a..061b1ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix For Issue [#1509](https://github.com/PHPOffice/PhpSpreadsheet/issues/1509) (Can not set empty enclosure for CSV) [#1518](https://github.com/PHPOffice/PhpSpreadsheet/pull/1518) - Fix for Issue [#1505](https://github.com/PHPOffice/PhpSpreadsheet/issues/1505) (TypeError : Argument 4 passed to PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet::writeAttributeIf() must be of the type string) [#1525](https://github.com/PHPOffice/PhpSpreadsheet/pull/1525) - Fix for Issue [#1495](https://github.com/PHPOffice/PhpSpreadsheet/issues/1495) (Sheet index being changed when multiple sheets are used in formula) [#1500]((https://github.com/PHPOffice/PhpSpreadsheet/pull/1500)) -- Fix for Issue [#1533](https://github.com/PHPOffice/PhpSpreadsheet/issues/1533) (A reference to a cell containing a string starting with "#" leads to errors in the generated xlsx.) [#1534]((https://github.com/PHPOffice/PhpSpreadsheet/pull/1534) +- Fix for Issue [#1533](https://github.com/PHPOffice/PhpSpreadsheet/issues/1533) (A reference to a cell containing a string starting with "#" leads to errors in the generated xlsx.) [#1534](https://github.com/PHPOffice/PhpSpreadsheet/pull/1534) - Xls Writer - Correct Timestamp Bug [#1493](https://github.com/PHPOffice/PhpSpreadsheet/pull/1493) ## [1.13.0] - 2020-05-31 From ca506ba87f0cc06a127a25d694e6408c6acfe20b Mon Sep 17 00:00:00 2001 From: Christoph Ziegenberg Date: Sat, 20 Jun 2020 17:15:38 +0200 Subject: [PATCH 039/153] Corrected date time detection (#1492) * Corrected date time detection German and Swiss ZIP codes (special formats provided in German Excel versions) were detected as date time value, because the regular expression for date time formats falsely matched their formats ("\C\H\-00000" and "\D-00000"). --- src/PhpSpreadsheet/Shared/Date.php | 5 +++++ tests/data/Shared/Date/FormatCodes.php | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 9dc99292..cb37515d 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -387,6 +387,11 @@ class Date if ((substr($pFormatCode, 0, 1) == '_') || (substr($pFormatCode, 0, 2) == '0 ')) { return false; } + // Some "special formats" provided in German Excel versions were detected as date time value, + // so filter them out here - "\C\H\-00000" (Switzerland) and "\D-00000" (Germany). + if (\strpos($pFormatCode, '-00000') !== false) { + return false; + } // Try checking for any of the date formatting characters that don't appear within square braces if (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $pFormatCode)) { // We might also have a format mask containing quoted strings... diff --git a/tests/data/Shared/Date/FormatCodes.php b/tests/data/Shared/Date/FormatCodes.php index 64810de3..b558874c 100644 --- a/tests/data/Shared/Date/FormatCodes.php +++ b/tests/data/Shared/Date/FormatCodes.php @@ -150,4 +150,12 @@ return [ true, '"date " y-m-d', ], + [ + false, + '\C\H\-00000', + ], + [ + false, + '\D-00000', + ], ]; From 859bef1901d072d771efadf521e9d75f427cd852 Mon Sep 17 00:00:00 2001 From: Dawid Warmuz Date: Sat, 20 Jun 2020 18:21:19 +0200 Subject: [PATCH 040/153] Add support for IFS() logical function (#1442) * Add support for IFS() logical function * Use Exception as false value in IFS logical function, so it never collides with string in spreadsheet --- docs/references/function-list-by-category.md | 2 +- docs/references/function-list-by-name.md | 2 +- .../Calculation/Calculation.php | 2 +- src/PhpSpreadsheet/Calculation/Logical.php | 35 +++++++++++++ .../Calculation/functionlist.txt | 1 + .../Calculation/Functions/Logical/IfsTest.php | 32 ++++++++++++ tests/data/Calculation/Logical/IFS.php | 50 +++++++++++++++++++ 7 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfsTest.php create mode 100644 tests/data/Calculation/Logical/IFS.php diff --git a/docs/references/function-list-by-category.md b/docs/references/function-list-by-category.md index 49bb66c0..3cc1c2b8 100644 --- a/docs/references/function-list-by-category.md +++ b/docs/references/function-list-by-category.md @@ -213,7 +213,7 @@ FALSE | \PhpOffice\PhpSpreadsheet\Calculation\Logical::FALSE IF | \PhpOffice\PhpSpreadsheet\Calculation\Logical::statementIf IFERROR | \PhpOffice\PhpSpreadsheet\Calculation\Logical::IFERROR IFNA | \PhpOffice\PhpSpreadsheet\Calculation\Logical::IFNA -IFS | **Not yet Implemented** +IFS | \PhpOffice\PhpSpreadsheet\Calculation\Logical::IFS NOT | \PhpOffice\PhpSpreadsheet\Calculation\Logical::NOT OR | \PhpOffice\PhpSpreadsheet\Calculation\Logical::logicalOr SWITCH | \PhpOffice\PhpSpreadsheet\Calculation\Logical::statementSwitch diff --git a/docs/references/function-list-by-name.md b/docs/references/function-list-by-name.md index f5493d03..c787ee00 100644 --- a/docs/references/function-list-by-name.md +++ b/docs/references/function-list-by-name.md @@ -212,7 +212,7 @@ Excel Function | Category | PhpSpreadsheet Function IF | CATEGORY_LOGICAL | \PhpOffice\PhpSpreadsheet\Calculation\Logical::statementIf IFERROR | CATEGORY_LOGICAL | \PhpOffice\PhpSpreadsheet\Calculation\Logical::IFERROR IFNA | CATEGORY_LOGICAL | \PhpOffice\PhpSpreadsheet\Calculation\Logical::IFNA -IFS | CATEGORY_LOGICAL | **Not yet Implemented** +IFS | CATEGORY_LOGICAL | \PhpOffice\PhpSpreadsheet\Calculation\Logical::IFS IMABS | CATEGORY_ENGINEERING | \PhpOffice\PhpSpreadsheet\Calculation\Engineering::IMABS IMAGINARY | CATEGORY_ENGINEERING | \PhpOffice\PhpSpreadsheet\Calculation\Engineering::IMAGINARY IMARGUMENT | CATEGORY_ENGINEERING | \PhpOffice\PhpSpreadsheet\Calculation\Engineering::IMARGUMENT diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 848f9068..e65cd425 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1059,7 +1059,7 @@ class Calculation ], 'IFS' => [ 'category' => Category::CATEGORY_LOGICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Logical::class, 'IFS'], 'argumentCount' => '2+', ], 'IMABS' => [ diff --git a/src/PhpSpreadsheet/Calculation/Logical.php b/src/PhpSpreadsheet/Calculation/Logical.php index a362a275..69c543ce 100644 --- a/src/PhpSpreadsheet/Calculation/Logical.php +++ b/src/PhpSpreadsheet/Calculation/Logical.php @@ -352,4 +352,39 @@ class Logical return self::statementIf(Functions::isNa($testValue), $napart, $testValue); } + + /** + * IFS. + * + * Excel Function: + * =IFS(testValue1;returnIfTrue1;testValue2;returnIfTrue2;...;testValue_n;returnIfTrue_n) + * + * testValue1 ... testValue_n + * Conditions to Evaluate + * returnIfTrue1 ... returnIfTrue_n + * Value returned if corresponding testValue (nth) was true + * + * @param mixed ...$arguments Statement arguments + * + * @return mixed|string The value of returnIfTrue_n, if testValue_n was true. #N/A if none of testValues was true + */ + public static function IFS(...$arguments) + { + if (count($arguments) % 2 != 0) { + return Functions::NA(); + } + // We use instance of Exception as a falseValue in order to prevent string collision with value in cell + $falseValueException = new Exception(); + for ($i = 0; $i < count($arguments); $i += 2) { + $testValue = ($arguments[$i] === null) ? '' : Functions::flattenSingleValue($arguments[$i]); + $returnIfTrue = ($arguments[$i + 1] === null) ? '' : Functions::flattenSingleValue($arguments[$i + 1]); + $result = self::statementIf($testValue, $returnIfTrue, $falseValueException); + + if ($result !== $falseValueException) { + return $result; + } + } + + return Functions::NA(); + } } diff --git a/src/PhpSpreadsheet/Calculation/functionlist.txt b/src/PhpSpreadsheet/Calculation/functionlist.txt index 2556ec90..e71d18f4 100644 --- a/src/PhpSpreadsheet/Calculation/functionlist.txt +++ b/src/PhpSpreadsheet/Calculation/functionlist.txt @@ -163,6 +163,7 @@ HYPERLINK HYPGEOMDIST IF IFERROR +IFS IMABS IMAGINARY IMARGUMENT diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfsTest.php new file mode 100644 index 00000000..15687cd6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfsTest.php @@ -0,0 +1,32 @@ + Date: Sat, 20 Jun 2020 20:27:17 +0200 Subject: [PATCH 041/153] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 061b1ff8..02179db6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix for Issue [#1533](https://github.com/PHPOffice/PhpSpreadsheet/issues/1533) (A reference to a cell containing a string starting with "#" leads to errors in the generated xlsx.) [#1534](https://github.com/PHPOffice/PhpSpreadsheet/pull/1534) - Xls Writer - Correct Timestamp Bug [#1493](https://github.com/PHPOffice/PhpSpreadsheet/pull/1493) +### Added +- Add support for IFS() logical function [#1442](https://github.com/PHPOffice/PhpSpreadsheet/pull/1442) + ## [1.13.0] - 2020-05-31 ### Added From 10a4a95d678d674a612c9126f912acab2013f294 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 21 Jun 2020 14:41:51 +0200 Subject: [PATCH 042/153] Handle Ranges formatted as 3-d ranges, as long as the references are both to the same worksheet (#1540) --- src/PhpSpreadsheet/Calculation/Calculation.php | 14 +++++++++----- .../Calculation/Engine/RangeTest.php | 11 +++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index e65cd425..ed630354 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3666,13 +3666,17 @@ class Calculation if ($matches[2] == '') { // Otherwise, we 'inherit' the worksheet reference from the start cell reference // The start of the cell range reference should be the last entry in $output - $startCellRef = $output[count($output) - 1]['value']; - preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $startCellRef, $startMatches); - if ($startMatches[2] > '') { - $val = $startMatches[2] . '!' . $val; + $rangeStartCellRef = $output[count($output) - 1]['value']; + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches); + if ($rangeStartMatches[2] > '') { + $val = $rangeStartMatches[2] . '!' . $val; } } else { - return $this->raiseFormulaError('3D Range references are not yet supported'); + $rangeStartCellRef = $output[count($output) - 1]['value']; + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches); + if ($rangeStartMatches[2] !== $matches[2]) { + return $this->raiseFormulaError('3D Range references are not yet supported'); + } } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index e2e6e11d..9dfa4bf0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -60,9 +60,20 @@ class RangeTest extends TestCase ['=COUNT(A1:C1,A3:C3,B1:C3)', 12], ['=SUM(A1:C1,A3:C3 B1:C3)', 23], ['=COUNT(A1:C1,A3:C3 B1:C3)', 5], + ['=SUM(Worksheet!A1:B3,Worksheet!A1:C2)', 48], + ['=SUM(Worksheet!A1:Worksheet!B3,Worksheet!A1:Worksheet!C2)', 48], ]; } + public function test3dRangeEvaluation(): void + { + $workSheet = $this->spreadSheet->getActiveSheet(); + $workSheet->setCellValue('E1', '=SUM(Worksheet!A1:Worksheet2!B3)'); + + $this->expectExceptionMessage('3D Range references are not yet supported'); + $workSheet->getCell('E1')->getCalculatedValue(); + } + /** * @dataProvider providerNamedRangeEvaluation * From 6080c4561d69caafb71c1799b0ea77e04ad856fe Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Thu, 25 Jun 2020 22:42:38 -0700 Subject: [PATCH 043/153] Improve Coverage for HTML Reader Reader/Html is now covered except for 1 statement. There is some coverage of RichText when you know in advance that the html will expand into a single cell. It is a tougher nut, one that I have not yet cracked, to try to handle rich text while converting unkown html to multiple cells. The original author left this as a TODO, and so for now must I. It made sense to restructure some of the code. There are some changes. - Issue #1532 is fixed (links are now saved when using rowspan). - Colors can now be specified as html color name. To accomplish this, Helper/Html function colourNameLookup was changed from protected to public, and changed to static. - Superfluous empty lines were eliminated in a number of places, e.g.
  • A
  • B
  • C
had formerly caused a wrapped cell to be created with 2 empty lines followed by A, B, and C on separate lines; it will now just have the 3 A/B/C lines, which seems like a more sensible interpretation. - Img alt tag, which had been cast to float, is now used as a string. Private member "encoding" is not used. Functions getEncoding and setEncoding have therefore been marked deprecated. In fact, I was unable to get SecurityScanner to pass *any* html which is not UTF-8. There are possibly ways of getting around this (in Reader/Html - I have no intention of messing with Security Scanner), as can be seen in my companion pull request for Excel2003 Xml Reader. Doing this would be easier for ASCII-compatible character sets (like ISO-8859-1), than for non-compatible charsets (like UTF-16). I am not convinced that the effort is worth it, but am willing to investigate further. I added a number of tests, creating an Html directory, and moving HtmlTest to that directory. --- samples/Basic/42_RichText.php | 2 +- src/PhpSpreadsheet/Helper/Html.php | 4 +- src/PhpSpreadsheet/Reader/BaseReader.php | 23 +- src/PhpSpreadsheet/Reader/Html.php | 661 ++++++++++-------- .../Reader/Html/HtmlBorderTest.php | 110 +++ .../Reader/Html/HtmlHelper.php | 28 + .../Reader/Html/HtmlImageTest.php | 84 +++ .../Reader/Html/HtmlLoadStringTest.php | 92 +++ .../Reader/Html/HtmlTagsTest.php | 236 +++++++ .../Reader/{ => Html}/HtmlTest.php | 251 ++----- tests/data/Reader/HTML/badhtml.html | 1 + 11 files changed, 978 insertions(+), 514 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/HtmlBorderTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/HtmlHelper.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/HtmlTagsTest.php rename tests/PhpSpreadsheetTests/Reader/{ => Html}/HtmlTest.php (53%) create mode 100644 tests/data/Reader/HTML/badhtml.html diff --git a/samples/Basic/42_RichText.php b/samples/Basic/42_RichText.php index 43b35a62..d5fa85b4 100644 --- a/samples/Basic/42_RichText.php +++ b/samples/Basic/42_RichText.php @@ -30,7 +30,7 @@ $html1 = ' while this block uses an underline.

-

+

I want to eat healthy food pizza. '; diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index 252e14a5..6c4cbf9b 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -694,9 +694,9 @@ class Html return implode('', $values[0]); } - protected function colourNameLookup($rgb) + public static function colourNameLookup(string $rgb): string { - return self::$colourMap[$rgb]; + return self::$colourMap[$rgb] ?? ''; } protected function startFontTag($tag): void diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php index 77a6421b..eb0e3ba2 100644 --- a/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/BaseReader.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Shared\File; @@ -133,11 +134,7 @@ abstract class BaseReader implements IReader public function getSecurityScanner() { - if (property_exists($this, 'securityScanner')) { - return $this->securityScanner; - } - - return null; + return $this->securityScanner; } /** @@ -147,12 +144,18 @@ abstract class BaseReader implements IReader */ protected function openFile($pFilename): void { - File::assertFile($pFilename); + if ($pFilename) { + File::assertFile($pFilename); - // Open file - $this->fileHandle = fopen($pFilename, 'rb'); - if ($this->fileHandle === false) { - throw new Exception('Could not open file ' . $pFilename . ' for reading.'); + // Open file + $fileHandle = fopen($pFilename, 'rb'); + } else { + $fileHandle = false; + } + if ($fileHandle !== false) { + $this->fileHandle = $fileHandle; + } else { + throw new ReaderException('Could not open file ' . $pFilename . ' for reading.'); } } } diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 2fe85b6f..7cb14f49 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -16,6 +16,7 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use Throwable; /** PhpSpreadsheet root directory */ class Html extends BaseReader @@ -219,9 +220,13 @@ class Html extends BaseReader /** * Set input encoding. * + * @deprecated no use is made of this property + * * @param string $pValue Input encoding, eg: 'ANSI' * * @return $this + * + * @codeCoverageIgnore */ public function setInputEncoding($pValue) { @@ -233,7 +238,11 @@ class Html extends BaseReader /** * Get input encoding. * + * @deprecated no use is made of this property + * * @return string + * + * @codeCoverageIgnore */ public function getInputEncoding() { @@ -289,12 +298,319 @@ class Html extends BaseReader $cellContent = (string) ''; } - /** - * @param int $row - * @param string $column - * @param string $cellContent - */ - protected function processDomElement(DOMNode $element, Worksheet $sheet, &$row, &$column, &$cellContent): void + private function processDomElementBody(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void + { + $attributeArray = []; + foreach ($child->attributes as $attribute) { + $attributeArray[$attribute->name] = $attribute->value; + } + + if ($child->nodeName === 'body') { + $row = 1; + $column = 'A'; + $cellContent = ''; + $this->tableLevel = 0; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementTitle($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementTitle(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'title') { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $sheet->setTitle($cellContent, true, false); + $cellContent = ''; + } else { + $this->processDomElementSpanEtc($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private static $spanEtc = ['span', 'div', 'font', 'i', 'em', 'strong', 'b']; + + private function processDomElementSpanEtc(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if (in_array($child->nodeName, self::$spanEtc)) { + if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') { + $sheet->getComment($column . $row) + ->getText() + ->createTextRun($child->textContent); + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + } else { + $this->processDomElementHr($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementHr(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'hr') { + $this->flushCell($sheet, $column, $row, $cellContent); + ++$row; + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + ++$row; + } + // fall through to br + $this->processDomElementBr($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + + private function processDomElementBr(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'br' || $child->nodeName === 'hr') { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a \n and set the cell to wrap + $cellContent .= "\n"; + $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true); + } else { + // Otherwise flush our existing content and move the row cursor on + $this->flushCell($sheet, $column, $row, $cellContent); + ++$row; + } + } else { + $this->processDomElementA($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementA(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'a') { + foreach ($attributeArray as $attributeName => $attributeValue) { + switch ($attributeName) { + case 'href': + $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue); + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + + break; + case 'class': + if ($attributeValue === 'comment-indicator') { + break; // Ignore - it's just a red square. + } + } + } + // no idea why this should be needed + //$cellContent .= ' '; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementH1Etc($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private static $h1Etc = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p']; + + private function processDomElementH1Etc(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if (in_array($child->nodeName, self::$h1Etc)) { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a \n + $cellContent .= $cellContent ? "\n" : ''; + $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true); + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + if ($cellContent > '') { + $this->flushCell($sheet, $column, $row, $cellContent); + ++$row; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $this->flushCell($sheet, $column, $row, $cellContent); + + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + + ++$row; + $column = 'A'; + } + } else { + $this->processDomElementLi($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementLi(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'li') { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a \n + $cellContent .= $cellContent ? "\n" : ''; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + if ($cellContent > '') { + $this->flushCell($sheet, $column, $row, $cellContent); + } + ++$row; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $this->flushCell($sheet, $column, $row, $cellContent); + $column = 'A'; + } + } else { + $this->processDomElementImg($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementImg(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'img') { + $this->insertImage($sheet, $column, $row, $attributeArray); + } else { + $this->processDomElementTable($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementTable(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'table') { + $this->flushCell($sheet, $column, $row, $cellContent); + $column = $this->setTableStartColumn($column); + if ($this->tableLevel > 1) { + --$row; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $column = $this->releaseTableStartColumn(); + if ($this->tableLevel > 1) { + ++$column; + } else { + ++$row; + } + } else { + $this->processDomElementTr($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementTr(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'tr') { + $column = $this->getTableStartColumn(); + $cellContent = ''; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + if (isset($attributeArray['height'])) { + $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']); + } + + ++$row; + } else { + $this->processDomElementThTdOther($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementThTdOther(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName !== 'td' && $child->nodeName !== 'th') { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementThTd($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['bgcolor'])) { + $sheet->getStyle("$column$row")->applyFromArray( + [ + 'fill' => [ + 'fillType' => Fill::FILL_SOLID, + 'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])], + ], + ] + ); + } + } + + private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void + { + if (isset($attributeArray['width'])) { + $sheet->getColumnDimension($column)->setWidth($attributeArray['width']); + } + } + + private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void + { + if (isset($attributeArray['height'])) { + $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']); + } + } + + private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['align'])) { + $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']); + } + } + + private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['valign'])) { + $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']); + } + } + + private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['data-format'])) { + $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']); + } + } + + private function processDomElementThTd(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + while (isset($this->rowspan[$column . $row])) { + ++$column; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + // apply inline style + $this->applyInlineStyle($sheet, $row, $column, $attributeArray); + + $this->flushCell($sheet, $column, $row, $cellContent); + + $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray); + $this->processDomElementWidth($sheet, $column, $attributeArray); + $this->processDomElementHeight($sheet, $row, $attributeArray); + $this->processDomElementAlign($sheet, $row, $column, $attributeArray); + $this->processDomElementVAlign($sheet, $row, $column, $attributeArray); + $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray); + + if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { + //create merging rowspan and colspan + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + ++$columnTo; + } + $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1); + foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { + $this->rowspan[$value] = true; + } + $sheet->mergeCells($range); + $column = $columnTo; + } elseif (isset($attributeArray['rowspan'])) { + //create merging rowspan + $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1); + foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { + $this->rowspan[$value] = true; + } + $sheet->mergeCells($range); + } elseif (isset($attributeArray['colspan'])) { + //create merging colspan + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + ++$columnTo; + } + $sheet->mergeCells($column . $row . ':' . $columnTo . $row); + $column = $columnTo; + } + + ++$column; + } + + protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void { foreach ($element->childNodes as $child) { if ($child instanceof DOMText) { @@ -306,267 +622,7 @@ class Html extends BaseReader // but if we have a rich text run instead, we need to append it correctly // TODO } elseif ($child instanceof DOMElement) { - $attributeArray = []; - foreach ($child->attributes as $attribute) { - $attributeArray[$attribute->name] = $attribute->value; - } - - switch ($child->nodeName) { - case 'meta': - foreach ($attributeArray as $attributeName => $attributeValue) { - // Extract character set, so we can convert to UTF-8 if required - if ($attributeName === 'charset') { - $this->setInputEncoding($attributeValue); - } - } - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - - break; - case 'title': - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - $sheet->setTitle($cellContent, true, false); - $cellContent = ''; - - break; - case 'span': - case 'div': - case 'font': - case 'i': - case 'em': - case 'strong': - case 'b': - if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') { - $sheet->getComment($column . $row) - ->getText() - ->createTextRun($child->textContent); - - break; - } - - if ($cellContent > '') { - $cellContent .= ' '; - } - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - if ($cellContent > '') { - $cellContent .= ' '; - } - - if (isset($this->formats[$child->nodeName])) { - $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); - } - - break; - case 'hr': - $this->flushCell($sheet, $column, $row, $cellContent); - ++$row; - if (isset($this->formats[$child->nodeName])) { - $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); - } else { - $cellContent = '----------'; - $this->flushCell($sheet, $column, $row, $cellContent); - } - ++$row; - // Add a break after a horizontal rule, simply by allowing the code to dropthru - // no break - case 'br': - if ($this->tableLevel > 0) { - // If we're inside a table, replace with a \n and set the cell to wrap - $cellContent .= "\n"; - $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true); - } else { - // Otherwise flush our existing content and move the row cursor on - $this->flushCell($sheet, $column, $row, $cellContent); - ++$row; - } - - break; - case 'a': - foreach ($attributeArray as $attributeName => $attributeValue) { - switch ($attributeName) { - case 'href': - $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue); - if (isset($this->formats[$child->nodeName])) { - $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); - } - - break; - case 'class': - if ($attributeValue === 'comment-indicator') { - break; // Ignore - it's just a red square. - } - } - } - $cellContent .= ' '; - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - - break; - case 'h1': - case 'h2': - case 'h3': - case 'h4': - case 'h5': - case 'h6': - case 'ol': - case 'ul': - case 'p': - if ($this->tableLevel > 0) { - // If we're inside a table, replace with a \n - $cellContent .= "\n"; - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - } else { - if ($cellContent > '') { - $this->flushCell($sheet, $column, $row, $cellContent); - ++$row; - } - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - $this->flushCell($sheet, $column, $row, $cellContent); - - if (isset($this->formats[$child->nodeName])) { - $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); - } - - ++$row; - $column = 'A'; - } - - break; - case 'li': - if ($this->tableLevel > 0) { - // If we're inside a table, replace with a \n - $cellContent .= "\n"; - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - } else { - if ($cellContent > '') { - $this->flushCell($sheet, $column, $row, $cellContent); - } - ++$row; - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - $this->flushCell($sheet, $column, $row, $cellContent); - $column = 'A'; - } - - break; - case 'img': - $this->insertImage($sheet, $column, $row, $attributeArray); - - break; - case 'table': - $this->flushCell($sheet, $column, $row, $cellContent); - $column = $this->setTableStartColumn($column); - if ($this->tableLevel > 1) { - --$row; - } - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - $column = $this->releaseTableStartColumn(); - if ($this->tableLevel > 1) { - ++$column; - } else { - ++$row; - } - - break; - case 'thead': - case 'tbody': - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - - break; - case 'tr': - $column = $this->getTableStartColumn(); - $cellContent = ''; - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - - if (isset($attributeArray['height'])) { - $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']); - } - - ++$row; - - break; - case 'th': - case 'td': - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - - while (isset($this->rowspan[$column . $row])) { - ++$column; - } - - // apply inline style - $this->applyInlineStyle($sheet, $row, $column, $attributeArray); - - $this->flushCell($sheet, $column, $row, $cellContent); - - if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { - //create merging rowspan and colspan - $columnTo = $column; - for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { - ++$columnTo; - } - $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1); - foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { - $this->rowspan[$value] = true; - } - $sheet->mergeCells($range); - $column = $columnTo; - } elseif (isset($attributeArray['rowspan'])) { - //create merging rowspan - $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1); - foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { - $this->rowspan[$value] = true; - } - $sheet->mergeCells($range); - } elseif (isset($attributeArray['colspan'])) { - //create merging colspan - $columnTo = $column; - for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { - ++$columnTo; - } - $sheet->mergeCells($column . $row . ':' . $columnTo . $row); - $column = $columnTo; - } elseif (isset($attributeArray['bgcolor'])) { - $sheet->getStyle($column . $row)->applyFromArray( - [ - 'fill' => [ - 'fillType' => Fill::FILL_SOLID, - 'color' => ['rgb' => $attributeArray['bgcolor']], - ], - ] - ); - } - - if (isset($attributeArray['width'])) { - $sheet->getColumnDimension($column)->setWidth($attributeArray['width']); - } - - if (isset($attributeArray['height'])) { - $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']); - } - - if (isset($attributeArray['align'])) { - $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']); - } - - if (isset($attributeArray['valign'])) { - $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']); - } - - if (isset($attributeArray['data-format'])) { - $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']); - } - - ++$column; - - break; - case 'body': - $row = 1; - $column = 'A'; - $cellContent = ''; - $this->tableLevel = 0; - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - - break; - default: - $this->processDomElement($child, $sheet, $row, $column, $cellContent); - } + $this->processDomElementBody($element, $sheet, $row, $column, $cellContent, $child); } } } @@ -588,7 +644,11 @@ class Html extends BaseReader // Create a new DOM object $dom = new DOMDocument(); // Reload the HTML file into the DOM object - $loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scanFile($pFilename), 'HTML-ENTITIES', 'UTF-8')); + try { + $loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scanFile($pFilename), 'HTML-ENTITIES', 'UTF-8')); + } catch (Throwable $e) { + $loaded = false; + } if ($loaded === false) { throw new Exception('Failed to load ' . $pFilename . ' as a DOM Document'); } @@ -606,7 +666,11 @@ class Html extends BaseReader // Create a new DOM object $dom = new DOMDocument(); // Reload the HTML file into the DOM object - $loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8')); + try { + $loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8')); + } catch (Throwable $e) { + $loaded = false; + } if ($loaded === false) { throw new Exception('Failed to load content as a DOM Document'); } @@ -837,7 +901,7 @@ class Html extends BaseReader return substr($value, 1); } - return null; + return \PhpOffice\PhpSpreadsheet\Helper\Html::colourNameLookup((string) $value); } /** @@ -853,7 +917,7 @@ class Html extends BaseReader $src = urldecode($attributes['src']); $width = isset($attributes['width']) ? (float) $attributes['width'] : null; $height = isset($attributes['height']) ? (float) $attributes['height'] : null; - $name = isset($attributes['alt']) ? (float) $attributes['alt'] : null; + $name = $attributes['alt'] ?? null; $drawing = new Drawing(); $drawing->setPath($src); @@ -884,6 +948,28 @@ class Html extends BaseReader ); } + private static $borderMappings = [ + 'dash-dot' => Border::BORDER_DASHDOT, + 'dash-dot-dot' => Border::BORDER_DASHDOTDOT, + 'dashed' => Border::BORDER_DASHED, + 'dotted' => Border::BORDER_DOTTED, + 'double' => Border::BORDER_DOUBLE, + 'hair' => Border::BORDER_HAIR, + 'medium' => Border::BORDER_MEDIUM, + 'medium-dashed' => Border::BORDER_MEDIUMDASHED, + 'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT, + 'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT, + 'none' => Border::BORDER_NONE, + 'slant-dash-dot' => Border::BORDER_SLANTDASHDOT, + 'solid' => Border::BORDER_THIN, + 'thick' => Border::BORDER_THICK, + ]; + + public static function getBorderMappings(): array + { + return self::$borderMappings; + } + /** * Map html border style to PhpSpreadsheet border style. * @@ -893,38 +979,7 @@ class Html extends BaseReader */ public function getBorderStyle($style) { - switch ($style) { - case 'solid': - return Border::BORDER_THIN; - case 'dashed': - return Border::BORDER_DASHED; - case 'dotted': - return Border::BORDER_DOTTED; - case 'medium': - return Border::BORDER_MEDIUM; - case 'thick': - return Border::BORDER_THICK; - case 'none': - return Border::BORDER_NONE; - case 'dash-dot': - return Border::BORDER_DASHDOT; - case 'dash-dot-dot': - return Border::BORDER_DASHDOTDOT; - case 'double': - return Border::BORDER_DOUBLE; - case 'hair': - return Border::BORDER_HAIR; - case 'medium-dash-dot': - return Border::BORDER_MEDIUMDASHDOT; - case 'medium-dash-dot-dot': - return Border::BORDER_MEDIUMDASHDOTDOT; - case 'medium-dashed': - return Border::BORDER_MEDIUMDASHED; - case 'slant-dash-dot': - return Border::BORDER_SLANTDASHDOT; - } - - return null; + return (array_key_exists($style, self::$borderMappings)) ? self::$borderMappings[$style] : null; } /** diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlBorderTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlBorderTest.php new file mode 100644 index 00000000..58a0b5d7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlBorderTest.php @@ -0,0 +1,110 @@ + + + Thin border + Border bottom + Border top + Border left + Border right + + + '; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + $style = $firstSheet->getCell('A1')->getStyle(); + $borders = $style->getBorders(); + + /** @var Border $border */ + foreach ([$borders->getTop(), $borders->getBottom(), $borders->getLeft(), $borders->getRight()] as $border) { + self::assertEquals('333333', $border->getColor()->getRGB()); + self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); + } + + $style = $firstSheet->getCell('B1')->getStyle(); + $border = $style->getBorders()->getBottom(); + self::assertEquals('333333', $border->getColor()->getRGB()); + self::assertEquals(Border::BORDER_DASHED, $border->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $style->getBorders()->getTop()->getBorderStyle()); + + $style = $firstSheet->getCell('C1')->getStyle(); + $border = $style->getBorders()->getTop(); + self::assertEquals('333333', $border->getColor()->getRGB()); + self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $style->getBorders()->getBottom()->getBorderStyle()); + + $style = $firstSheet->getCell('D1')->getStyle(); + $border = $style->getBorders()->getLeft(); + self::assertEquals('00ff00', $border->getColor()->getRGB()); + self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $style->getBorders()->getBottom()->getBorderStyle()); + + $style = $firstSheet->getCell('E1')->getStyle(); + $border = $style->getBorders()->getRight(); + self::assertEquals('333333', $border->getColor()->getRGB()); + self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); + self::assertEquals(Border::BORDER_NONE, $style->getBorders()->getBottom()->getBorderStyle()); + + $style = $firstSheet->getCell('F1')->getStyle(); + $borders = $style->getBorders(); + foreach ([$borders->getTop(), $borders->getBottom(), $borders->getLeft(), $borders->getRight()] as $border) { + self::assertEquals(Border::BORDER_NONE, $border->getBorderStyle()); + } + } + + /** + * @dataProvider providerBorderStyle + */ + public function testBorderStyle(string $style, string $expectedResult): void + { + $borders = Html::getBorderMappings(); + self::assertEquals($expectedResult, $borders[$style]); + } + + public function testBorderStyleCoverage(): void + { + $expected = Html::getBorderMappings(); + $covered = []; + foreach ($expected as $key => $val) { + $covered[$key] = 0; + } + $tests = $this->providerBorderStyle(); + foreach ($tests as $test) { + $covered[$test[0]] = 1; + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "Borderstyle $key not tested"); + } + } + + public function providerBorderStyle(): array + { + return [ + ['dash-dot', Border::BORDER_DASHDOT], + ['dash-dot-dot', Border::BORDER_DASHDOTDOT], + ['dashed', Border::BORDER_DASHED], + ['dotted', Border::BORDER_DOTTED], + ['double', Border::BORDER_DOUBLE], + ['hair', Border::BORDER_HAIR], + ['medium', Border::BORDER_MEDIUM], + ['medium-dashed', Border::BORDER_MEDIUMDASHED], + ['medium-dash-dot', Border::BORDER_MEDIUMDASHDOT], + ['medium-dash-dot-dot', Border::BORDER_MEDIUMDASHDOTDOT], + ['none', Border::BORDER_NONE], + ['slant-dash-dot', Border::BORDER_SLANTDASHDOT], + ['solid', Border::BORDER_THIN], + ['thick', Border::BORDER_THICK], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlHelper.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlHelper.php new file mode 100644 index 00000000..c09902ff --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlHelper.php @@ -0,0 +1,28 @@ +load($filename); + if ($unlink) { + unlink($filename); + } + + return $spreadsheet; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php new file mode 100644 index 00000000..cf4157e3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php @@ -0,0 +1,84 @@ + + + test image + + '; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + + /** @var Drawing $drawing */ + $drawing = $firstSheet->getDrawingCollection()[0]; + self::assertEquals($imagePath, $drawing->getPath()); + self::assertEquals('A1', $drawing->getCoordinates()); + self::assertEquals('test image', $drawing->getName()); + self::assertEquals('100', $drawing->getWidth()); + self::assertEquals('100', $drawing->getHeight()); + } + + public function testCanInsertImageWidth(): void + { + $imagePath = realpath(__DIR__ . '/../../../data/Reader/HTML/image.jpg'); + + $html = ' + + + +
test image
'; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + + /** @var Drawing $drawing */ + $drawing = $firstSheet->getDrawingCollection()[0]; + self::assertEquals('50', $drawing->getWidth()); + self::assertEquals('50', $drawing->getHeight()); + } + + public function testCanInsertImageHeight(): void + { + $imagePath = realpath(__DIR__ . '/../../../data/Reader/HTML/image.jpg'); + + $html = ' + + + +
'; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + + /** @var Drawing $drawing */ + $drawing = $firstSheet->getDrawingCollection()[0]; + self::assertEquals('', $drawing->getName()); + self::assertEquals('75', $drawing->getWidth()); + self::assertEquals('75', $drawing->getHeight()); + } + + public function testImageWithourSrc(): void + { + $html = ' + + + +
'; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + + self::assertCount(0, $firstSheet->getDrawingCollection()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php new file mode 100644 index 00000000..e1041507 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php @@ -0,0 +1,92 @@ + + + Hello World + + + Hello
World + + + Hello
World + + '; + $spreadsheet = (new Html())->loadFromString($html); + $firstSheet = $spreadsheet->getSheet(0); + + $cellStyle = $firstSheet->getStyle('A1'); + self::assertFalse($cellStyle->getAlignment()->getWrapText()); + + $cellStyle = $firstSheet->getStyle('A2'); + self::assertTrue($cellStyle->getAlignment()->getWrapText()); + $cellValue = $firstSheet->getCell('A2')->getValue(); + self::assertStringContainsString("\n", $cellValue); + + $cellStyle = $firstSheet->getStyle('A3'); + self::assertTrue($cellStyle->getAlignment()->getWrapText()); + $cellValue = $firstSheet->getCell('A3')->getValue(); + self::assertStringContainsString("\n", $cellValue); + } + + public function testLoadInvalidString(): void + { + $this->expectException(ReaderException::class); + $html = ''; + $spreadsheet = (new Html())->loadFromString($html); + $firstSheet = $spreadsheet->getSheet(0); + $cellStyle = $firstSheet->getStyle('A1'); + self::assertFalse($cellStyle->getAlignment()->getWrapText()); + } + + public function testCanLoadFromStringIntoExistingSpreadsheet(): void + { + $html = ' + + + + + + + + + +
Hello World
Hello
World
Hello
World
'; + $reader = new Html(); + $spreadsheet = $reader->loadFromString($html); + $firstSheet = $spreadsheet->getSheet(0); + + $cellStyle = $firstSheet->getStyle('A1'); + self::assertFalse($cellStyle->getAlignment()->getWrapText()); + + $cellStyle = $firstSheet->getStyle('A2'); + self::assertTrue($cellStyle->getAlignment()->getWrapText()); + $cellValue = $firstSheet->getCell('A2')->getValue(); + self::assertStringContainsString("\n", $cellValue); + + $cellStyle = $firstSheet->getStyle('A3'); + self::assertTrue($cellStyle->getAlignment()->getWrapText()); + $cellValue = $firstSheet->getCell('A3')->getValue(); + self::assertStringContainsString("\n", $cellValue); + + $reader->setSheetIndex(1); + $html = ' + + + +
Goodbye World
'; + + self::assertEquals(1, $spreadsheet->getSheetCount()); + $spreadsheet = $reader->loadFromString($html, $spreadsheet); + self::assertEquals(2, $spreadsheet->getSheetCount()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTagsTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTagsTest.php new file mode 100644 index 00000000..c0c206f9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTagsTest.php @@ -0,0 +1,236 @@ + +123 +
hyperlink5


6 +789 +101112 + +
+ + + + + +
123
456
789
  • A
  • B
  • C
1112
+
  • D
  • E
  • F
+
+ + + + + + +
M + + + +
NO
PQ
+
R
STU
+EOF; + $robj = $reader->loadFromString($html1); + $sheet = $robj->getActiveSheet(); + + self::assertEquals('www.google.com', $sheet->getCell('A2')->getHyperlink()->getUrl()); + self::assertEquals('hyperlink', $sheet->getCell('A2')->getValue()); + self::assertEquals(-1, $sheet->getRowDimension(11)->getRowHeight()); + self::assertEquals(20, $sheet->getRowDimension(12)->getRowHeight()); + self::assertEquals(5, $sheet->getCell('B2')->getValue()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('B3')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + self::assertEquals(6, $sheet->getCell('C4')->getValue()); + self::assertEquals(Border::BORDER_THIN, $sheet->getCell('A9')->getStyle()->getBorders()->getBottom()->getBorderStyle()); + + self::assertEquals(2, $sheet->getCell('B11')->getValue()); + self::assertTrue($sheet->getCell('B11')->getStyle()->getFont()->getItalic()); + + // list within table + self::assertEquals("A\nB\nC", $sheet->getCell('A14')->getValue()); + self::assertTrue($sheet->getCell('A14')->getStyle()->getAlignment()->getWrapText()); + // list outside of table + self::assertEquals('D', $sheet->getCell('A17')->getValue()); + self::assertEquals('E', $sheet->getCell('A18')->getValue()); + self::assertEquals('F', $sheet->getCell('A19')->getValue()); + + // embedded table + self::assertEquals('M', $sheet->getCell('A21')->getValue()); + self::assertEquals('N', $sheet->getCell('B20')->getValue()); + self::assertEquals('O', $sheet->getCell('C20')->getValue()); + self::assertEquals('P', $sheet->getCell('B21')->getValue()); + self::assertEquals('Q', $sheet->getCell('C21')->getValue()); + self::assertEquals('R', $sheet->getCell('C23')->getValue()); + self::assertEquals('S', $sheet->getCell('A24')->getValue()); + } + + public static function testTagsRowColSpans(): void + { + $reader = new Html(); + $html1 = << + + Month + Savings + Expenses + + + January + $100 + $50 + + + February + $80 + + + Away in March + $30 + + + $40 + + +EOF; + $robj = $reader->loadFromString($html1); + $sheet = $robj->getActiveSheet(); + + self::assertEquals(['C2:C3' => 'C2:C3', 'A4:B5' => 'A4:B5'], $sheet->getMergeCells()); + self::assertEquals('Away in March', $sheet->getCell('A4')->getValue()); + self::assertEquals('00FFFF', $sheet->getCell('A4')->getStyle()->getFill()->getEndColor()->getRGB()); + } + + public static function testDoublyEmbeddedTable(): void + { + $reader = new Html(); + $html1 = << +123 +456 +789 + + + +M + + + + + + + +
N + + + +
1011
1213
+
Y
PQX
+ +R + +STU + +EOF; + $robj = $reader->loadFromString($html1); + $sheet = $robj->getActiveSheet(); + + self::assertEquals('1', $sheet->getCell('A1')->getValue()); + self::assertEquals('2', $sheet->getCell('B1')->getValue()); + self::assertEquals('3', $sheet->getCell('C1')->getValue()); + self::assertEquals('4', $sheet->getCell('A2')->getValue()); + self::assertEquals('5', $sheet->getCell('B2')->getValue()); + self::assertEquals('6', $sheet->getCell('C2')->getValue()); + self::assertEquals('7', $sheet->getCell('A3')->getValue()); + self::assertEquals('8', $sheet->getCell('B3')->getValue()); + self::assertEquals('9', $sheet->getCell('C3')->getValue()); + self::assertEquals('10', $sheet->getCell('C5')->getValue()); + self::assertEquals('11', $sheet->getCell('D5')->getValue()); + self::assertEquals('12', $sheet->getCell('C6')->getValue()); + self::assertEquals('13', $sheet->getCell('D6')->getValue()); + self::assertEquals('N', $sheet->getCell('B6')->getValue()); + self::assertEquals('M', $sheet->getCell('A7')->getValue()); + self::assertEquals('Y', $sheet->getCell('E7')->getValue()); + self::assertEquals('P', $sheet->getCell('B8')->getValue()); + self::assertEquals('Q', $sheet->getCell('C8')->getValue()); + self::assertEquals('X', $sheet->getCell('D8')->getValue()); + self::assertEquals('R', $sheet->getCell('C10')->getValue()); + self::assertEquals('S', $sheet->getCell('A11')->getValue()); + self::assertEquals('T', $sheet->getCell('B11')->getValue()); + self::assertEquals('U', $sheet->getCell('C11')->getValue()); + } + + public static function testTagsOutsideTable(): void + { + $reader = new Html(); + $html1 = <<Here comes a list +
    +
  1. Item 1
  2. +
  3. Item 2
  4. +
  5. Item 3
  6. +
  7. Item 4
  8. +
+And here's another +
    +
  • Item A
  • +
  • Item B
  • +
+
    +Content before list +
  1. Item I
  2. +
  3. Item II
  4. +
  5. This is rich text
  6. +
+ +EOF; + $robj = $reader->loadFromString($html1); + $sheet = $robj->getActiveSheet(); + + self::assertTrue($sheet->getCell('A1')->getStyle()->getFont()->getBold()); + self::assertEquals('Here comes a list', $sheet->getCell('A1')->getValue()); + self::assertEquals('Item 1', $sheet->getCell('A3')->getValue()); + self::assertEquals('Item 2', $sheet->getCell('A4')->getValue()); + self::assertEquals('Item 3', $sheet->getCell('A5')->getValue()); + self::assertEquals('Item 4', $sheet->getCell('A6')->getValue()); + self::assertEquals('And here\'s another', $sheet->getCell('A7')->getValue()); + self::assertEquals('Item A', $sheet->getCell('A9')->getValue()); + self::assertEquals('Item B', $sheet->getCell('A10')->getValue()); + self::assertEquals('Content before list', $sheet->getCell('A11')->getValue()); + self::assertEquals('Item I', $sheet->getCell('A12')->getValue()); + self::assertEquals('Item II', $sheet->getCell('A13')->getValue()); + // TODO Rich Text not yet supported + } + + public static function testHyperlinksWithRowspan(): void + { + $reader = new Html(); + $html1 = << + + Title + Link 1 + + + Link 2 + + + Link 3 + + +EOF; + $robj = $reader->loadFromString($html1); + $sheet = $robj->getActiveSheet(); + self::assertEquals('https://google.com', $sheet->getCell('B1')->getHyperlink()->getUrl()); + self::assertEquals('https://google.com', $sheet->getCell('B2')->getHyperlink()->getUrl()); + self::assertEquals('https://google.com', $sheet->getCell('B3')->getHyperlink()->getUrl()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php similarity index 53% rename from tests/PhpSpreadsheetTests/Reader/HtmlTest.php rename to tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php index b0994a33..e87e636b 100644 --- a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php @@ -1,12 +1,11 @@ canRead($filename)); } + public function testBadHtml(): void + { + $this->expectException(ReaderException::class); + $filename = 'tests/data/Reader/HTML/badhtml.html'; + $reader = new Html(); + self::assertTrue($reader->canRead($filename)); + $spreadsheet = $reader->load($filename); + self::assertTrue(false); + } + + public function testNonHtml(): void + { + $this->expectException(ReaderException::class); + $filename = __FILE__; + $reader = new Html(); + self::assertFalse($reader->canRead($filename)); + $spreadsheet = $reader->load($filename); + self::assertTrue(false); + } + + public function testInvalidFilename(): void + { + $reader = new Html(); + self::assertEquals(0, $reader->getSheetIndex()); + self::assertFalse($reader->canRead('')); + } + public function providerCanReadVerySmallFile() { $padding = str_repeat('a', 2048); @@ -38,7 +64,7 @@ class HtmlTest extends TestCase */ public function testCanReadVerySmallFile($expected, $content): void { - $filename = $this->createHtml($content); + $filename = HtmlHelper::createHtml($content); $reader = new Html(); $actual = $reader->canRead($filename); @@ -51,63 +77,21 @@ class HtmlTest extends TestCase { $html = ' - + +
Blue backgroundBlue backgroundUnknown fore/background
'; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $style = $firstSheet->getCell('A1')->getStyle(); - self::assertEquals('FFFFFF', $style->getFont()->getColor()->getRGB()); - - unlink($filename); - } - - public function testCanApplyInlineBordersStyles(): void - { - $html = ' - - - - - - - -
Thin borderBorder bottomBorder topBorder leftBorder right
'; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); - $firstSheet = $spreadsheet->getSheet(0); - $style = $firstSheet->getCell('A1')->getStyle(); - $borders = $style->getBorders(); - - /** @var Border $border */ - foreach ([$borders->getTop(), $borders->getBottom(), $borders->getLeft(), $borders->getRight()] as $border) { - self::assertEquals('333333', $border->getColor()->getRGB()); - self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); - } - + self::assertEquals('0000FF', $style->getFill()->getStartColor()->getRGB()); + self::assertEquals('0000FF', $style->getFill()->getEndColor()->getRGB()); $style = $firstSheet->getCell('B1')->getStyle(); - $border = $style->getBorders()->getBottom(); - self::assertEquals('333333', $border->getColor()->getRGB()); - self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); - - $style = $firstSheet->getCell('C1')->getStyle(); - $border = $style->getBorders()->getTop(); - self::assertEquals('333333', $border->getColor()->getRGB()); - self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); - - $style = $firstSheet->getCell('D1')->getStyle(); - $border = $style->getBorders()->getLeft(); - self::assertEquals('333333', $border->getColor()->getRGB()); - self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); - - $style = $firstSheet->getCell('E1')->getStyle(); - $border = $style->getBorders()->getRight(); - self::assertEquals('333333', $border->getColor()->getRGB()); - self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle()); - - unlink($filename); + self::assertEquals('000000', $style->getFont()->getColor()->getRGB()); + self::assertEquals('000000', $style->getFill()->getEndColor()->getRGB()); + self::assertEquals('FFFFFF', $style->getFill()->getstartColor()->getRGB()); } public function testCanApplyInlineFontStyles(): void @@ -122,8 +106,8 @@ class HtmlTest extends TestCase Line through '; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $style = $firstSheet->getCell('A1')->getStyle(); @@ -143,8 +127,6 @@ class HtmlTest extends TestCase $style = $firstSheet->getCell('F1')->getStyle(); self::assertTrue($style->getFont()->getStrikethrough()); - - unlink($filename); } public function testCanApplyInlineWidth(): void @@ -155,8 +137,8 @@ class HtmlTest extends TestCase 100px '; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $dimension = $firstSheet->getColumnDimension('A'); @@ -164,8 +146,6 @@ class HtmlTest extends TestCase $dimension = $firstSheet->getColumnDimension('B'); self::assertEquals(100, $dimension->getWidth()); - - unlink($filename); } public function testCanApplyInlineHeight(): void @@ -178,8 +158,8 @@ class HtmlTest extends TestCase 2 '; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $dimension = $firstSheet->getRowDimension(1); @@ -187,8 +167,6 @@ class HtmlTest extends TestCase $dimension = $firstSheet->getRowDimension(2); self::assertEquals(100, $dimension->getRowHeight()); - - unlink($filename); } public function testCanApplyAlignment(): void @@ -203,8 +181,8 @@ class HtmlTest extends TestCase Wraptext '; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $style = $firstSheet->getCell('A1')->getStyle(); @@ -224,8 +202,6 @@ class HtmlTest extends TestCase $style = $firstSheet->getCell('F1')->getStyle(); self::assertTrue($style->getAlignment()->getWrapText()); - - unlink($filename); } public function testCanApplyInlineDataFormat(): void @@ -235,35 +211,12 @@ class HtmlTest extends TestCase 2019-02-02 12:34:00 '; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $style = $firstSheet->getCell('A1')->getStyle(); self::assertEquals('mmm-yy', $style->getNumberFormat()->getFormatCode()); - - unlink($filename); - } - - public function testCanInsertImage(): void - { - $imagePath = realpath(__DIR__ . '/../../data/Reader/HTML/image.jpg'); - - $html = ' - - - -
'; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); - $firstSheet = $spreadsheet->getSheet(0); - - /** @var Drawing $drawing */ - $drawing = $firstSheet->getDrawingCollection()[0]; - self::assertEquals($imagePath, $drawing->getPath()); - self::assertEquals('A1', $drawing->getCoordinates()); - - unlink($filename); } public function testCanApplyCellWrapping(): void @@ -279,8 +232,8 @@ class HtmlTest extends TestCase Hello
World '; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $cellStyle = $firstSheet->getStyle('A1'); @@ -295,103 +248,6 @@ class HtmlTest extends TestCase self::assertTrue($cellStyle->getAlignment()->getWrapText()); $cellValue = $firstSheet->getCell('A3')->getValue(); self::assertStringContainsString("\n", $cellValue); - - unlink($filename); - } - - public function testCanLoadFromString(): void - { - $html = ' - - - - - - - - - -
Hello World
Hello
World
Hello
World
'; - $spreadsheet = (new Html())->loadFromString($html); - $firstSheet = $spreadsheet->getSheet(0); - - $cellStyle = $firstSheet->getStyle('A1'); - self::assertFalse($cellStyle->getAlignment()->getWrapText()); - - $cellStyle = $firstSheet->getStyle('A2'); - self::assertTrue($cellStyle->getAlignment()->getWrapText()); - $cellValue = $firstSheet->getCell('A2')->getValue(); - self::assertStringContainsString("\n", $cellValue); - - $cellStyle = $firstSheet->getStyle('A3'); - self::assertTrue($cellStyle->getAlignment()->getWrapText()); - $cellValue = $firstSheet->getCell('A3')->getValue(); - self::assertStringContainsString("\n", $cellValue); - } - - public function testCanLoadFromStringIntoExistingSpreadsheet(): void - { - $html = ' - - - - - - - - - -
Hello World
Hello
World
Hello
World
'; - $reader = new Html(); - $spreadsheet = $reader->loadFromString($html); - $firstSheet = $spreadsheet->getSheet(0); - - $cellStyle = $firstSheet->getStyle('A1'); - self::assertFalse($cellStyle->getAlignment()->getWrapText()); - - $cellStyle = $firstSheet->getStyle('A2'); - self::assertTrue($cellStyle->getAlignment()->getWrapText()); - $cellValue = $firstSheet->getCell('A2')->getValue(); - self::assertStringContainsString("\n", $cellValue); - - $cellStyle = $firstSheet->getStyle('A3'); - self::assertTrue($cellStyle->getAlignment()->getWrapText()); - $cellValue = $firstSheet->getCell('A3')->getValue(); - self::assertStringContainsString("\n", $cellValue); - - $reader->setSheetIndex(1); - $html = ' - - - -
Goodbye World
'; - - self::assertEquals(1, $spreadsheet->getSheetCount()); - $spreadsheet = $reader->loadFromString($html, $spreadsheet); - self::assertEquals(2, $spreadsheet->getSheetCount()); - } - - /** - * @param string $html - * - * @return string - */ - private function createHtml($html) - { - $filename = tempnam(sys_get_temp_dir(), 'html'); - file_put_contents($filename, $html); - - return $filename; - } - - /** - * @param $filename - * - * @return \PhpOffice\PhpSpreadsheet\Spreadsheet - */ - private function loadHtmlIntoSpreadsheet($filename) - { - return (new Html())->load($filename); } public function testRowspanInRendering(): void @@ -417,11 +273,10 @@ class HtmlTest extends TestCase Text Indent '; - $filename = $this->createHtml($html); - $spreadsheet = $this->loadHtmlIntoSpreadsheet($filename); + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $style = $firstSheet->getCell('C2')->getStyle(); self::assertEquals(10, $style->getAlignment()->getIndent()); - unlink($filename); } } diff --git a/tests/data/Reader/HTML/badhtml.html b/tests/data/Reader/HTML/badhtml.html new file mode 100644 index 00000000..6816fb08 --- /dev/null +++ b/tests/data/Reader/HTML/badhtml.html @@ -0,0 +1 @@ + From 752a0a5a6c00f89f8cea6f3ea3584bd5ebac4086 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Thu, 25 Jun 2020 23:11:30 -0700 Subject: [PATCH 044/153] Scrutinizer Recommendations Two unneeded assignments in tests, one unused parameter in source code. --- src/PhpSpreadsheet/Reader/Html.php | 4 ++-- tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 7cb14f49..a83ed807 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -506,7 +506,7 @@ class Html extends BaseReader if ($child->nodeName !== 'td' && $child->nodeName !== 'th') { $this->processDomElement($child, $sheet, $row, $column, $cellContent); } else { - $this->processDomElementThTd($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray); } } @@ -559,7 +559,7 @@ class Html extends BaseReader } } - private function processDomElementThTd(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { while (isset($this->rowspan[$column . $row])) { ++$column; diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php index e87e636b..91e60d3e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php @@ -23,7 +23,7 @@ class HtmlTest extends TestCase $filename = 'tests/data/Reader/HTML/badhtml.html'; $reader = new Html(); self::assertTrue($reader->canRead($filename)); - $spreadsheet = $reader->load($filename); + $reader->load($filename); self::assertTrue(false); } @@ -33,7 +33,7 @@ class HtmlTest extends TestCase $filename = __FILE__; $reader = new Html(); self::assertFalse($reader->canRead($filename)); - $spreadsheet = $reader->load($filename); + $reader->load($filename); self::assertTrue(false); } From 9f1a33cc8a386b97d2c330ec8cef1f6250a1e7e0 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Thu, 25 Jun 2020 23:39:28 -0700 Subject: [PATCH 045/153] Scrutinizer Again Cascading series of unused unused parameters. --- src/PhpSpreadsheet/Reader/Html.php | 48 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index a83ed807..1e34de0e 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -298,7 +298,7 @@ class Html extends BaseReader $cellContent = (string) ''; } - private function processDomElementBody(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void + private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void { $attributeArray = []; foreach ($child->attributes as $attribute) { @@ -312,24 +312,24 @@ class Html extends BaseReader $this->tableLevel = 0; $this->processDomElement($child, $sheet, $row, $column, $cellContent); } else { - $this->processDomElementTitle($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementTitle(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'title') { $this->processDomElement($child, $sheet, $row, $column, $cellContent); $sheet->setTitle($cellContent, true, false); $cellContent = ''; } else { - $this->processDomElementSpanEtc($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray); } } private static $spanEtc = ['span', 'div', 'font', 'i', 'em', 'strong', 'b']; - private function processDomElementSpanEtc(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if (in_array($child->nodeName, self::$spanEtc)) { if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') { @@ -343,11 +343,11 @@ class Html extends BaseReader $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); } } else { - $this->processDomElementHr($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementHr(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'hr') { $this->flushCell($sheet, $column, $row, $cellContent); @@ -358,10 +358,10 @@ class Html extends BaseReader ++$row; } // fall through to br - $this->processDomElementBr($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray); } - private function processDomElementBr(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'br' || $child->nodeName === 'hr') { if ($this->tableLevel > 0) { @@ -374,11 +374,11 @@ class Html extends BaseReader ++$row; } } else { - $this->processDomElementA($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementA(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'a') { foreach ($attributeArray as $attributeName => $attributeValue) { @@ -400,13 +400,13 @@ class Html extends BaseReader //$cellContent .= ' '; $this->processDomElement($child, $sheet, $row, $column, $cellContent); } else { - $this->processDomElementH1Etc($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray); } } private static $h1Etc = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p']; - private function processDomElementH1Etc(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if (in_array($child->nodeName, self::$h1Etc)) { if ($this->tableLevel > 0) { @@ -430,11 +430,11 @@ class Html extends BaseReader $column = 'A'; } } else { - $this->processDomElementLi($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementLi(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'li') { if ($this->tableLevel > 0) { @@ -451,20 +451,20 @@ class Html extends BaseReader $column = 'A'; } } else { - $this->processDomElementImg($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementImg(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'img') { $this->insertImage($sheet, $column, $row, $attributeArray); } else { - $this->processDomElementTable($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementTable(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'table') { $this->flushCell($sheet, $column, $row, $cellContent); @@ -480,11 +480,11 @@ class Html extends BaseReader ++$row; } } else { - $this->processDomElementTr($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementTr(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'tr') { $column = $this->getTableStartColumn(); @@ -497,11 +497,11 @@ class Html extends BaseReader ++$row; } else { - $this->processDomElementThTdOther($element, $sheet, $row, $column, $cellContent, $child, $attributeArray); + $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray); } } - private function processDomElementThTdOther(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName !== 'td' && $child->nodeName !== 'th') { $this->processDomElement($child, $sheet, $row, $column, $cellContent); @@ -622,7 +622,7 @@ class Html extends BaseReader // but if we have a rich text run instead, we need to append it correctly // TODO } elseif ($child instanceof DOMElement) { - $this->processDomElementBody($element, $sheet, $row, $column, $cellContent, $child); + $this->processDomElementBody($sheet, $row, $column, $cellContent, $child); } } } From a264cafe4c2541c8a3f0d6cb2147ae6843b5aa3a Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 27 Jun 2020 23:03:25 +0200 Subject: [PATCH 046/153] Helper class for the conversion of cell addresses between A1 and R1C1 formats, and vice-versa (#1558) * Helper class for the conversion of cell addresses between A1 and R1C1 formats, and vice-versa --- src/PhpSpreadsheet/Cell/AddressHelper.php | 88 ++++++++++++++ .../Cell/AddressHelperTest.php | 108 ++++++++++++++++++ .../data/Cell/A1ConversionToR1C1Absolute.php | 15 +++ .../data/Cell/A1ConversionToR1C1Exception.php | 5 + .../data/Cell/A1ConversionToR1C1Relative.php | 19 +++ .../data/Cell/R1C1ConversionToA1Absolute.php | 14 +++ .../data/Cell/R1C1ConversionToA1Exception.php | 9 ++ .../data/Cell/R1C1ConversionToA1Relative.php | 14 +++ 8 files changed, 272 insertions(+) create mode 100644 src/PhpSpreadsheet/Cell/AddressHelper.php create mode 100644 tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php create mode 100644 tests/data/Cell/A1ConversionToR1C1Absolute.php create mode 100644 tests/data/Cell/A1ConversionToR1C1Exception.php create mode 100644 tests/data/Cell/A1ConversionToR1C1Relative.php create mode 100644 tests/data/Cell/R1C1ConversionToA1Absolute.php create mode 100644 tests/data/Cell/R1C1ConversionToA1Exception.php create mode 100644 tests/data/Cell/R1C1ConversionToA1Relative.php diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php new file mode 100644 index 00000000..77a521b0 --- /dev/null +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -0,0 +1,88 @@ +expectException(Exception::class); + + AddressHelper::convertToA1($address); + } + + public function providerR1C1ConversionToA1Exception() + { + return require 'tests/data/Cell/R1C1ConversionToA1Exception.php'; + } + + /** + * @dataProvider providerA1ConversionToR1C1Absolute + */ + public function testA1ConversionToR1C1Absolute(string $expectedValue, string $address): void + { + $actualValue = AddressHelper::convertToR1C1($address); + + self::assertSame($expectedValue, $actualValue); + } + + public function providerA1ConversionToR1C1Absolute() + { + return require 'tests/data/Cell/A1ConversionToR1C1Absolute.php'; + } + + /** + * @dataProvider providerA1ConversionToR1C1Relative + */ + public function testA1ConversionToR1C1Relative(string $expectedValue, string $address, ?int $row = null, ?int $column = null): void + { + $actualValue = AddressHelper::convertToR1C1($address, $row, $column); + + self::assertSame($expectedValue, $actualValue); + } + + public function providerA1ConversionToR1C1Relative() + { + return require 'tests/data/Cell/A1ConversionToR1C1Relative.php'; + } + + /** + * @dataProvider providerA1ConversionToR1C1Exception + */ + public function testA1ConversionToR1C1Exception(string $address): void + { + $this->expectException(Exception::class); + + AddressHelper::convertToR1C1($address); + } + + public function providerA1ConversionToR1C1Exception() + { + return require 'tests/data/Cell/A1ConversionToR1C1Exception.php'; + } +} diff --git a/tests/data/Cell/A1ConversionToR1C1Absolute.php b/tests/data/Cell/A1ConversionToR1C1Absolute.php new file mode 100644 index 00000000..171672bb --- /dev/null +++ b/tests/data/Cell/A1ConversionToR1C1Absolute.php @@ -0,0 +1,15 @@ + Date: Sat, 27 Jun 2020 23:10:28 +0200 Subject: [PATCH 047/153] And now phpcs is telling me to fix my grammar --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02179db6..68aad0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Add support for IFS() logical function [#1442](https://github.com/PHPOffice/PhpSpreadsheet/pull/1442) +- Add Cell Address Helper to provide conversions between the R1C1 and A1 address formats [#1558](https://github.com/PHPOffice/PhpSpreadsheet/pull/1558) ## [1.13.0] - 2020-05-31 From 321dfc7a3da7f982084f0959d71f6ab4ea01561e Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 28 Jun 2020 16:29:13 +0900 Subject: [PATCH 048/153] Upgrad PHP deps --- composer.json | 4 +- composer.lock | 886 ++++++++++-------- src/PhpSpreadsheet/Chart/Renderer/JpGraph.php | 2 + src/PhpSpreadsheet/Reader/Xls.php | 3 + src/PhpSpreadsheet/Shared/Font.php | 4 + src/PhpSpreadsheet/Shared/OLE/PPS.php | 1 + src/PhpSpreadsheet/Writer/Xls/Parser.php | 2 + 7 files changed, 535 insertions(+), 367 deletions(-) diff --git a/composer.json b/composer.json index 6ce3a728..b480c082 100644 --- a/composer.json +++ b/composer.json @@ -53,10 +53,10 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^1.4", "markbaker/matrix": "^1.2", - "psr/simple-cache": "^1.0", - "maennchen/zipstream-php": "^2.0" + "psr/simple-cache": "^1.0" }, "require-dev": { "dompdf/dompdf": "^0.8.5", diff --git a/composer.lock b/composer.lock index 41ec1b9b..9f5ae4aa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ab06908c3ff8187971def16c578f1ced", + "content-hash": "4103c8180a2e28881d3dbb45e835e863", "packages": [ { "name": "maennchen/zipstream-php", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95" + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9ceee828f9620b2e5c075e551ec7ed8a7035ac95", - "reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", "shasum": "" }, "require": { - "ext-mbstring": "*", "myclabs/php-enum": "^1.5", "php": ">= 7.1", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { "ext-zip": "*", @@ -65,7 +65,7 @@ "stream", "zip" ], - "time": "2020-02-23T01:48:39+00:00" + "time": "2020-05-30T13:11:16+00:00" }, { "name": "markbaker/complex", @@ -374,6 +374,69 @@ "simple-cache" ], "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.17.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7110338d81ce1cbc3e273136e4574663627037a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7110338d81ce1cbc3e273136e4574663627037a7", + "reference": "7110338d81ce1cbc3e273136e4574663627037a7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2020-06-06T08:46:27+00:00" } ], "packages-dev": [ @@ -440,16 +503,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7" + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", "shasum": "" }, "require": { @@ -480,32 +543,26 @@ "Xdebug", "performance" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - } - ], - "time": "2020-03-01T12:26:26+00:00" + "time": "2020-06-04T11:16:35+00:00" }, { "name": "doctrine/annotations", - "version": "1.10.2", + "version": "1.10.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "b9d758e831c70751155c698c2f7df4665314a1cb" + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/b9d758e831c70751155c698c2f7df4665314a1cb", - "reference": "b9d758e831c70751155c698c2f7df4665314a1cb", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", "shasum": "" }, "require": { "doctrine/lexer": "1.*", "ext-tokenizer": "*", - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/cache": "1.*", @@ -555,24 +612,24 @@ "docblock", "parser" ], - "time": "2020-04-20T09:18:32+00:00" + "time": "2020-05-25T17:24:27+00:00" }, { "name": "doctrine/instantiator", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -611,24 +668,24 @@ "constructor", "instantiate" ], - "time": "2019-10-21T16:45:58+00:00" + "time": "2020-05-29T17:27:14+00:00" }, { "name": "doctrine/lexer", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", "shasum": "" }, "require": { - "php": "^7.2" + "php": "^7.2 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -673,7 +730,7 @@ "parser", "php" ], - "time": "2019-10-30T14:39:59+00:00" + "time": "2020-05-25T17:44:05+00:00" }, { "name": "dompdf/dompdf", @@ -743,16 +800,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.3", + "version": "v2.16.4", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "83baf823a33a1cbd5416c8626935cf3f843c10b0" + "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/83baf823a33a1cbd5416c8626935cf3f843c10b0", - "reference": "83baf823a33a1cbd5416c8626935cf3f843c10b0", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", + "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", "shasum": "" }, "require": { @@ -784,12 +841,12 @@ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", "phpunitgoodpractices/traits": "^1.8", - "symfony/phpunit-bridge": "^4.3 || ^5.0", + "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, "suggest": { "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "ext-mbstring": "For handling non-UTF8 characters.", "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." @@ -830,13 +887,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2020-04-15T18:51:10+00:00" + "time": "2020-06-27T23:57:46+00:00" }, { "name": "jpgraph/jpgraph", @@ -876,20 +927,21 @@ "jpgraph", "pie" ], + "abandoned": true, "time": "2017-02-23T09:44:15+00:00" }, { "name": "mpdf/mpdf", - "version": "v8.0.5", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "bad32aa9cd5958175aef185c02e032ddbadc56ea" + "reference": "d27aa93513b915896fa7cb53901d3122e286f811" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/bad32aa9cd5958175aef185c02e032ddbadc56ea", - "reference": "bad32aa9cd5958175aef185c02e032ddbadc56ea", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/d27aa93513b915896fa7cb53901d3122e286f811", + "reference": "d27aa93513b915896fa7cb53901d3122e286f811", "shasum": "" }, "require": { @@ -945,7 +997,7 @@ "php", "utf-8" ], - "time": "2020-02-05T08:43:46+00:00" + "time": "2020-05-25T09:08:39+00:00" }, { "name": "myclabs/deep-copy", @@ -1330,25 +1382,25 @@ }, { "name": "phpdocumentor/reflection-common", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b" + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b", - "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "php": ">=7.1" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -1375,7 +1427,7 @@ "reflection", "static analysis" ], - "time": "2020-04-27T09:25:28+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -1432,30 +1484,29 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.1.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", "shasum": "" }, "require": { - "php": "^7.2", + "php": "^7.2 || ^8.0", "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "^7.2", - "mockery/mockery": "~1" + "ext-tokenizer": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-1.x": "1.x-dev" } }, "autoload": { @@ -1474,7 +1525,7 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-02-18T18:59:58+00:00" + "time": "2020-06-27T10:12:23+00:00" }, { "name": "phpspec/prophecy", @@ -1793,16 +1844,16 @@ }, { "name": "phpunit/phpunit", - "version": "8.5.4", + "version": "8.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8474e22d7d642f665084ba5ec780626cbd1efd23" + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8474e22d7d642f665084ba5ec780626cbd1efd23", - "reference": "8474e22d7d642f665084ba5ec780626cbd1efd23", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", "shasum": "" }, "require": { @@ -1872,17 +1923,7 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-04-23T04:39:42+00:00" + "time": "2020-06-22T07:06:58+00:00" }, { "name": "psr/container", @@ -2028,16 +2069,16 @@ }, { "name": "sabberworm/php-css-parser", - "version": "8.3.0", + "version": "8.3.1", "source": { "type": "git", "url": "https://github.com/sabberworm/PHP-CSS-Parser.git", - "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f" + "reference": "d217848e1396ef962fb1997cf3e2421acba7f796" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f", - "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f", + "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/d217848e1396ef962fb1997cf3e2421acba7f796", + "reference": "d217848e1396ef962fb1997cf3e2421acba7f796", "shasum": "" }, "require": { @@ -2069,7 +2110,7 @@ "parser", "stylesheet" ], - "time": "2019-02-22T07:42:52+00:00" + "time": "2020-06-01T09:10:00+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2688,16 +2729,16 @@ }, { "name": "setasign/fpdi", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "527761458f504882ab844f15754523825647f291" + "reference": "50c388860a73191e010810ed57dbed795578e867" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/527761458f504882ab844f15754523825647f291", - "reference": "527761458f504882ab844f15754523825647f291", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/50c388860a73191e010810ed57dbed795578e867", + "reference": "50c388860a73191e010810ed57dbed795578e867", "shasum": "" }, "require": { @@ -2745,13 +2786,7 @@ "fpdi", "pdf" ], - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", - "type": "tidelift" - } - ], - "time": "2020-03-23T15:53:59+00:00" + "time": "2020-04-28T12:40:35+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -2806,26 +2841,29 @@ }, { "name": "symfony/console", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935" + "reference": "34ac555a3627e324b660e318daa07572e1140123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5fa1caadc8cdaa17bcfb25219f3b53fe294a9935", - "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935", + "url": "https://api.github.com/repos/symfony/console/zipball/34ac555a3627e324b660e318daa07572e1140123", + "reference": "34ac555a3627e324b660e318daa07572e1140123", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", - "symfony/service-contracts": "^1.1|^2" + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" }, "conflict": { "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", "symfony/event-dispatcher": "<4.4", "symfony/lock": "<4.4", "symfony/process": "<4.4" @@ -2851,7 +2889,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -2878,39 +2916,73 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-03-30T11:42:42+00:00" + "time": "2020-06-15T12:59:21+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v5.0.7", + "name": "symfony/deprecation-contracts", + "version": "v2.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/24f40d95385774ed5c71dbf014edd047e2f2f3dc", - "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", + "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", "shasum": "" }, "require": { - "php": "^7.2.5", - "symfony/event-dispatcher-contracts": "^2" + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "time": "2020-05-27T08:34:37+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "cc0d059e2e997e79ca34125a52f3e33de4424ac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/cc0d059e2e997e79ca34125a52f3e33de4424ac7", + "reference": "cc0d059e2e997e79ca34125a52f3e33de4424ac7", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/event-dispatcher-contracts": "^2", + "symfony/polyfill-php80": "^1.15" }, "conflict": { "symfony/dependency-injection": "<4.4" @@ -2935,7 +3007,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -2962,38 +3034,24 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.0.1", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "af23c2584d4577d54661c434446fb8fbed6025dd" + "reference": "405952c4e90941a17e52ef7489a2bd94870bb290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/af23c2584d4577d54661c434446fb8fbed6025dd", - "reference": "af23c2584d4577d54661c434446fb8fbed6025dd", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/405952c4e90941a17e52ef7489a2bd94870bb290", + "reference": "405952c4e90941a17e52ef7489a2bd94870bb290", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "psr/event-dispatcher": "^1" }, "suggest": { @@ -3002,7 +3060,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -3034,30 +3092,30 @@ "interoperability", "standards" ], - "time": "2019-11-18T17:27:11+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/filesystem", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "ca3b87dd09fff9b771731637f5379965fbfab420" + "reference": "6e4320f06d5f2cce0d96530162491f4465179157" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ca3b87dd09fff9b771731637f5379965fbfab420", - "reference": "ca3b87dd09fff9b771731637f5379965fbfab420", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", + "reference": "6e4320f06d5f2cce0d96530162491f4465179157", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3084,43 +3142,29 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/finder", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "600a52c29afc0d1caa74acbec8d3095ca7e9910d" + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/600a52c29afc0d1caa74acbec8d3095ca7e9910d", - "reference": "600a52c29afc0d1caa74acbec8d3095ca7e9910d", + "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3147,43 +3191,31 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d" + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/09dccfffd24b311df7f184aa80ee7b61ad61ed8d", - "reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php80": "^1.15" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3215,34 +3247,20 @@ "configuration", "options" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-23T13:08:13+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.15.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" + "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", + "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", "shasum": "" }, "require": { @@ -3254,7 +3272,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -3287,51 +3309,41 @@ "polyfill", "portable" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2020-06-06T08:46:27+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.15.0", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.17.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "6e4dbcf5e81eba86e36731f94fe56b1726835846" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/6e4dbcf5e81eba86e36731f94fe56b1726835846", + "reference": "6e4dbcf5e81eba86e36731f94fe56b1726835846", "shasum": "" }, "require": { "php": ">=5.3.3" }, "suggest": { - "ext-mbstring": "For best performance" + "ext-intl": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" }, "files": [ "bootstrap.php" @@ -3351,43 +3363,97 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", + "grapheme", + "intl", "polyfill", "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-03-09T19:04:49+00:00" + "time": "2020-06-06T08:46:27+00:00" }, { - "name": "symfony/polyfill-php70", - "version": "v1.15.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.17.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "2a18e37a489803559284416df58c71ccebe50bf0" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "40309d1700e8f72447bb9e7b54af756eeea35620" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0", - "reference": "2a18e37a489803559284416df58c71ccebe50bf0", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/40309d1700e8f72447bb9e7b54af756eeea35620", + "reference": "40309d1700e8f72447bb9e7b54af756eeea35620", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "time": "2020-06-14T14:40:37+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.17.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "471b096aede7025bace8eb356b9ac801aaba7e2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/471b096aede7025bace8eb356b9ac801aaba7e2d", + "reference": "471b096aede7025bace8eb356b9ac801aaba7e2d", "shasum": "" }, "require": { @@ -3397,7 +3463,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -3433,20 +3503,20 @@ "portable", "shim" ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2020-06-06T08:46:27+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.15.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "37b0976c78b94856543260ce09b460a7bc852747" + "reference": "f048e612a3905f34931127360bdd2def19a5e582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747", - "reference": "37b0976c78b94856543260ce09b460a7bc852747", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582", + "reference": "f048e612a3905f34931127360bdd2def19a5e582", "shasum": "" }, "require": { @@ -3455,7 +3525,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" } }, "autoload": { @@ -3488,34 +3558,20 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2020-05-12T16:47:27+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.15.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7" + "reference": "fa0837fe02d617d31fbb25f990655861bb27bd1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", - "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fa0837fe02d617d31fbb25f990655861bb27bd1a", + "reference": "fa0837fe02d617d31fbb25f990655861bb27bd1a", "shasum": "" }, "require": { @@ -3524,7 +3580,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -3560,43 +3620,96 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2020-06-06T08:46:27+00:00" }, { - "name": "symfony/process", - "version": "v5.0.7", + "name": "symfony/polyfill-php80", + "version": "v1.17.1", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4a5b6bba3259902e386eb80dd1956181ee90b5b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e", - "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4a5b6bba3259902e386eb80dd1956181ee90b5b2", + "reference": "4a5b6bba3259902e386eb80dd1956181ee90b5b2", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.0.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2020-06-06T08:46:27+00:00" + }, + { + "name": "symfony/process", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1", + "reference": "7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" } }, "autoload": { @@ -3623,38 +3736,24 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.0.1", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "144c5e51266b281231e947b51223ba14acf1a749" + "reference": "66a8f0957a3ca54e4f724e49028ab19d75a8918b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749", - "reference": "144c5e51266b281231e947b51223ba14acf1a749", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/66a8f0957a3ca54e4f724e49028ab19d75a8918b", + "reference": "66a8f0957a3ca54e4f724e49028ab19d75a8918b", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "psr/container": "^1.0" }, "suggest": { @@ -3663,7 +3762,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -3695,30 +3794,30 @@ "interoperability", "standards" ], - "time": "2019-11-18T17:27:11+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73" + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/a1d86d30d4522423afc998f32404efa34fcf5a73", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/service-contracts": "^1.0|^2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3745,21 +3844,78 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "funding": [ + "time": "2020-05-20T17:43:50+00:00" + }, + { + "name": "symfony/string", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "ac70459db781108db7c6d8981dd31ce0e29e3298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/ac70459db781108db7c6d8981dd31ce0e29e3298", + "reference": "ac70459db781108db7c6d8981dd31ce0e29e3298", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "time": "2020-03-27T16:56:45+00:00" + "description": "Symfony String component", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "time": "2020-06-11T12:16:36+00:00" }, { "name": "tecnickcom/tcpdf", @@ -3865,16 +4021,16 @@ }, { "name": "webmozart/assert", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6" + "reference": "9dc4f203e36f2b486149058bade43c851dd97451" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6", + "url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451", + "reference": "9dc4f203e36f2b486149058bade43c851dd97451", "shasum": "" }, "require": { @@ -3882,6 +4038,7 @@ "symfony/polyfill-ctype": "^1.8" }, "conflict": { + "phpstan/phpstan": "<0.12.20", "vimeo/psalm": "<3.9.1" }, "require-dev": { @@ -3909,7 +4066,7 @@ "check", "validate" ], - "time": "2020-04-18T12:12:48+00:00" + "time": "2020-06-16T10:16:42+00:00" } ], "aliases": [], @@ -3933,6 +4090,5 @@ "ext-zip": "*", "ext-zlib": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php index bd931c66..5546da58 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php @@ -219,9 +219,11 @@ class JpGraph implements IRenderer break; case 't': $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top + break; case 'b': $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom + break; default: $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 3f383b9e..c7c5f77f 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4944,6 +4944,7 @@ class Xls extends BaseReader // offset: var; size: $sz1; formula data for first condition (without size field) $formula1 = substr($recordData, $offset, $sz1); $formula1 = pack('v', $sz1) . $formula1; // prepend the length + try { $formula1 = $this->getFormulaFromStructure($formula1); @@ -4966,6 +4967,7 @@ class Xls extends BaseReader // offset: var; size: $sz2; formula data for second condition (without size field) $formula2 = substr($recordData, $offset, $sz2); $formula2 = pack('v', $sz2) . $formula2; // prepend the length + try { $formula2 = $this->getFormulaFromStructure($formula2); } catch (PhpSpreadsheetException $e) { @@ -5763,6 +5765,7 @@ class Xls extends BaseReader $size = 9; $data = self::extractNumber(substr($formulaData, 1)); $data = str_replace(',', '.', (string) $data); // in case non-English locale + break; case 0x20: // array constant case 0x40: diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index ca94529c..ee1f8aba 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -318,21 +318,25 @@ class Font // value 8.26 was found via interpolation by inspecting real Excel files with Calibri 11 font. $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText)); $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size + break; case 'Arial': // value 8 was set because of experience in different exports at Arial 10 font. $columnWidth = (int) (8 * StringHelper::countCharacters($columnText)); $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + break; case 'Verdana': // value 8 was found via interpolation by inspecting real Excel files with Verdana 10 font. $columnWidth = (int) (8 * StringHelper::countCharacters($columnText)); $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + break; default: // just assume Calibri $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText)); $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size + break; } diff --git a/src/PhpSpreadsheet/Shared/OLE/PPS.php b/src/PhpSpreadsheet/Shared/OLE/PPS.php index 0407e0d1..7aa42a14 100644 --- a/src/PhpSpreadsheet/Shared/OLE/PPS.php +++ b/src/PhpSpreadsheet/Shared/OLE/PPS.php @@ -192,6 +192,7 @@ class PPS . pack('V', isset($this->startBlock) ? $this->startBlock : 0) // 120 . pack('V', $this->Size) // 124 . pack('V', 0); // 128 + return $ret; } diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 5e3316ab..621853c0 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -1250,6 +1250,7 @@ class Parser throw new WriterException("')' token expected."); } $this->advance(); // eat the ")" + return $result; } // if it's a reference @@ -1350,6 +1351,7 @@ class Parser $result = $this->createTree($function, $result, $num_args); $this->advance(); // eat the ")" + return $result; } From 2896e6ceb969297f8ff3669bb8d07a91a910a08d Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 28 Jun 2020 17:34:32 +0900 Subject: [PATCH 049/153] Consistent regexp escaping --- samples/Pdf/21b_Pdf.php | 2 +- tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index 142fc344..c67ff3d2 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -8,7 +8,7 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf\Tcpdf; function replaceBody(string $html): string { $lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; - $bodystring = '@.*@ms'; + $bodystring = '~.*~ms'; $bodyrepl = <<

Serif

diff --git a/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php index 388dbd0e..94c201a7 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/CallbackTest.php @@ -20,7 +20,7 @@ body { EOF; - return preg_replace('@@', "$newstyle", $html); + return preg_replace('~~', "$newstyle", $html); } public function testSetAndReset(): void From 5e64479c06b5880182e2b6276f94ba9fd3c75106 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 28 Jun 2020 17:34:50 +0900 Subject: [PATCH 050/153] Document the callback --- src/PhpSpreadsheet/Writer/Html.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 752f286f..f3b40fa1 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -208,9 +208,15 @@ class Html extends BaseWriter return $html; } - public function setEditHtmlCallback(?callable $cbk): void + /** + * Set a callback to edit the entire HTML. + * + * The callback must accept the HTML as string as first parameter, + * and it must return the edited HTML as string. + */ + public function setEditHtmlCallback(?callable $callback): void { - $this->editHtmlCallback = $cbk; + $this->editHtmlCallback = $callback; } const VALIGN_ARR = [ From 14a0fa4cd0601da933616e06dfc95537f822ec80 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 28 Jun 2020 17:35:10 +0900 Subject: [PATCH 051/153] New members should always be private --- src/PhpSpreadsheet/Writer/Html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index f3b40fa1..eb50c456 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -136,7 +136,7 @@ class Html extends BaseWriter * * @var null|callable */ - protected $editHtmlCallback; + private $editHtmlCallback; /** * Create a new HTML. From f1fb8dcf1fe0f3e00ae4ff6d22308394f2d2fdbb Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 28 Jun 2020 21:56:12 +0900 Subject: [PATCH 052/153] Don't ouput row and columns without any cells in HTML writer If row or column dimensions are accessed, then HTML writer would still generate lots of empty cells, to show nothing at all. This now ignore row and column dimensions to only output cell that actually exists (even if those cells are empty). Fixes #1235 Close #1537 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Writer/Html.php | 41 ++++---- .../Html/ExtendForChartsAndImagesTest.php | 94 +++++++++++++++++++ 3 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/ExtendForChartsAndImagesTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 68aad0c1..1867f097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix for Issue [#1495](https://github.com/PHPOffice/PhpSpreadsheet/issues/1495) (Sheet index being changed when multiple sheets are used in formula) [#1500]((https://github.com/PHPOffice/PhpSpreadsheet/pull/1500)) - Fix for Issue [#1533](https://github.com/PHPOffice/PhpSpreadsheet/issues/1533) (A reference to a cell containing a string starting with "#" leads to errors in the generated xlsx.) [#1534](https://github.com/PHPOffice/PhpSpreadsheet/pull/1534) - Xls Writer - Correct Timestamp Bug [#1493](https://github.com/PHPOffice/PhpSpreadsheet/pull/1493) +- Don't ouput row and columns without any cells in HTML writer [#1235](https://github.com/PHPOffice/PhpSpreadsheet/issues/1235) ### Added - Add support for IFS() logical function [#1442](https://github.com/PHPOffice/PhpSpreadsheet/pull/1442) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index eb50c456..ec8fec7d 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -449,22 +449,19 @@ class Html extends BaseWriter foreach ($sheets as $sheet) { // Write table header $html .= $this->generateTableHeader($sheet); + // Get worksheet dimension - $dimension = explode(':', $sheet->calculateWorksheetDimension()); - $dimension[0] = Coordinate::coordinateFromString($dimension[0]); - $dimension[0][0] = Coordinate::columnIndexFromString($dimension[0][0]); - $dimension[1] = Coordinate::coordinateFromString($dimension[1]); - $dimension[1][0] = Coordinate::columnIndexFromString($dimension[1][0]); + [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); + [$minCol, $minRow] = Coordinate::coordinateFromString($min); + $minCol = Coordinate::columnIndexFromString($minCol); + [$maxCol, $maxRow] = Coordinate::coordinateFromString($max); + $maxCol = Coordinate::columnIndexFromString($maxCol); - // row min,max - $rowMin = $dimension[0][1]; - $rowMax = $dimension[1][1]; - - [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $rowMin); + [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); // Loop through cells - $row = $rowMin - 1; - while ($row++ < $rowMax) { + $row = $minRow - 1; + while ($row++ < $maxRow) { [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart); $html .= $startTag; @@ -473,8 +470,8 @@ class Html extends BaseWriter // Start a new rowData $rowData = []; // Loop through columns - $column = $dimension[0][0]; - while ($column <= $dimension[1][0]) { + $column = $minCol; + while ($column <= $maxCol) { // Cell exists? if ($sheet->cellExistsByColumnAndRow($column, $row)) { $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row; @@ -557,7 +554,7 @@ class Html extends BaseWriter * * @codeCoverageIgnore */ - private function extendRowsForCharts(Worksheet $pSheet, $row) + private function extendRowsForCharts(Worksheet $pSheet, int $row) { $rowMax = $row; $colMax = 'A'; @@ -582,7 +579,7 @@ class Html extends BaseWriter return [$rowMax, $colMax, $anyfound]; } - private function extendRowsForChartsAndImages(Worksheet $pSheet, $row) + private function extendRowsForChartsAndImages(Worksheet $pSheet, int $row): string { [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($pSheet, $row); @@ -1169,7 +1166,7 @@ class Html extends BaseWriter * Generate table header. * * @param Worksheet $pSheet The worksheet for the table we are writing - * @param bool $showid whether or not to add id to table tag + * @param bool $showid whether or not to add id to table tag * * @return string */ @@ -1182,8 +1179,6 @@ class Html extends BaseWriter $id = $showid ? "id='sheet$sheetIndex'" : ''; if ($showid) { $html .= "
\n"; - //} elseif ($this->useInlineCss) { - // $html .= "
\n"; } else { $html .= "
\n"; } @@ -1621,11 +1616,11 @@ class Html extends BaseWriter /** * Get use embedded CSS? * - * @deprecated no longer used - * * @return bool * * @codeCoverageIgnore + * + * @deprecated no longer used */ public function getUseEmbeddedCSS() { @@ -1635,13 +1630,13 @@ class Html extends BaseWriter /** * Set use embedded CSS? * - * @deprecated no longer used - * * @param bool $pValue * * @return $this * * @codeCoverageIgnore + * + * @deprecated no longer used */ public function setUseEmbeddedCSS($pValue) { diff --git a/tests/PhpSpreadsheetTests/Writer/Html/ExtendForChartsAndImagesTest.php b/tests/PhpSpreadsheetTests/Writer/Html/ExtendForChartsAndImagesTest.php new file mode 100644 index 00000000..e3d23230 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/ExtendForChartsAndImagesTest.php @@ -0,0 +1,94 @@ +assertMaxColumnAndMaxRow($spreadsheet, 1, 1); + } + + public function testSimpleSheet(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B3', 'foo'); + + $this->assertMaxColumnAndMaxRow($spreadsheet, 2, 3); + } + + public function testSheetWithExtraColumnDimensions(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B3', 'foo'); + + // Artificially expend the sheet column count without any real cells + $sheet->getColumnDimension('E'); + + $this->assertMaxColumnAndMaxRow($spreadsheet, 2, 3); + } + + public function testSheetWithExtraRowDimensions(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B3', 'foo'); + + // Artificially expend the sheet row count without any real cells + $sheet->getRowDimension(5); + + $this->assertMaxColumnAndMaxRow($spreadsheet, 2, 3); + } + + public function testSheetWithImageBelowData(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B3', 'foo'); + + // Add a drawing to the worksheet + $drawing = new Drawing(); + $drawing->setPath('foo.png', false); + $drawing->setCoordinates('A5'); + $drawing->setWorksheet($sheet); + + $this->assertMaxColumnAndMaxRow($spreadsheet, 2, 5); + } + + public function testSheetWithImageRightOfData(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B3', 'foo'); + + // Add a drawing to the worksheet + $drawing = new Drawing(); + $drawing->setPath('foo.png', false); + $drawing->setCoordinates('E1'); + $drawing->setWorksheet($sheet); + + $this->assertMaxColumnAndMaxRow($spreadsheet, 5, 3); + } + + private function assertMaxColumnAndMaxRow(Spreadsheet $spreadsheet, int $expectedColumnCount, int $expectedRowCount): void + { + $writer = new Html($spreadsheet); + $html = $writer->generateHtmlAll(); + + $rowCount = substr_count($html, ' Date: Sun, 28 Jun 2020 22:03:17 +0900 Subject: [PATCH 053/153] Update CHANGELOG --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1867f097..610615d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). ## [Unreleased] +### Added + +- Add support for IFS() logical function [#1442](https://github.com/PHPOffice/PhpSpreadsheet/pull/1442) +- Add Cell Address Helper to provide conversions between the R1C1 and A1 address formats [#1558](https://github.com/PHPOffice/PhpSpreadsheet/pull/1558) +- Add ability to edit Html/Pdf before saving [#1499](https://github.com/PHPOffice/PhpSpreadsheet/pull/1499) + ### Fixed - Resolve evaluation of utf-8 named ranges in calculation engine [#1522](https://github.com/PHPOffice/PhpSpreadsheet/pull/1522) @@ -21,10 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Xls Writer - Correct Timestamp Bug [#1493](https://github.com/PHPOffice/PhpSpreadsheet/pull/1493) - Don't ouput row and columns without any cells in HTML writer [#1235](https://github.com/PHPOffice/PhpSpreadsheet/issues/1235) -### Added -- Add support for IFS() logical function [#1442](https://github.com/PHPOffice/PhpSpreadsheet/pull/1442) -- Add Cell Address Helper to provide conversions between the R1C1 and A1 address formats [#1558](https://github.com/PHPOffice/PhpSpreadsheet/pull/1558) - ## [1.13.0] - 2020-05-31 ### Added From 6caa0cb4f5b6f71eae4bfffff6a0772df6122d29 Mon Sep 17 00:00:00 2001 From: Pavel Alazankin Date: Sun, 28 Jun 2020 17:39:38 +0300 Subject: [PATCH 054/153] add ability to set codepage explicitly for BIFF5 (#1484) If BIFF5 excel 95 file doesn't have codepage record, the default codepage CP1252 is used and can't be change. That causes to problems with decoding cyrillic text. --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Xls.php | 11 ++++++++++- src/PhpSpreadsheet/Shared/CodePage.php | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 610615d5..b97431c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add support for IFS() logical function [#1442](https://github.com/PHPOffice/PhpSpreadsheet/pull/1442) - Add Cell Address Helper to provide conversions between the R1C1 and A1 address formats [#1558](https://github.com/PHPOffice/PhpSpreadsheet/pull/1558) - Add ability to edit Html/Pdf before saving [#1499](https://github.com/PHPOffice/PhpSpreadsheet/pull/1499) +- Add ability to set codepage explicitly for BIFF5 [#1018](https://github.com/PHPOffice/PhpSpreadsheet/issues/1018) ### Fixed diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index c7c5f77f..11a6195c 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -439,6 +439,15 @@ class Xls extends BaseReader } } + public function setCodepage(string $codepage): void + { + if (!CodePage::validate($codepage)) { + throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage); + } + + $this->codepage = $codepage; + } + /** * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object. * @@ -640,7 +649,7 @@ class Xls extends BaseReader // initialize $this->pos = 0; - $this->codepage = 'CP1252'; + $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE; $this->formats = []; $this->objFonts = []; $this->palette = []; diff --git a/src/PhpSpreadsheet/Shared/CodePage.php b/src/PhpSpreadsheet/Shared/CodePage.php index 97cbfbbe..1d5d8933 100644 --- a/src/PhpSpreadsheet/Shared/CodePage.php +++ b/src/PhpSpreadsheet/Shared/CodePage.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; class CodePage { + public const DEFAULT_CODE_PAGE = 'CP1252'; + private static $pageArray = [ 0 => 'CP1252', // CodePage is not always correctly set when the xls file was saved by Apple's Numbers program 367 => 'ASCII', // ASCII @@ -65,6 +67,11 @@ class CodePage 65001 => 'UTF-8', // Unicode (UTF-8) ]; + public static function validate(string $codePage): bool + { + return in_array($codePage, self::$pageArray, true); + } + /** * Convert Microsoft Code Page Identifier to Code Page Name which iconv * and mbstring understands. From e74ef3a802640b9a790bae131a7a0b2e64ccba56 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Mon, 29 Jun 2020 10:15:42 +0900 Subject: [PATCH 055/153] Generate API doc on tag only Because it doesn't need to be run on every PR and documenting only release is exactly what we want --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 05e964a1..7a17dd93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ jobs: - php ocular.phar code-coverage:upload --format=php-clover tests/coverage-clover.xml - stage: API documentations - if: branch = master + if: tag is present php: 7.4 before_script: - curl -LO https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.0.0-rc/phpDocumentor.phar From 7f23ccb69d1957e0f968acbc08b695656cf5f5ab Mon Sep 17 00:00:00 2001 From: paulkned Date: Mon, 29 Jun 2020 03:17:58 +0200 Subject: [PATCH 056/153] Added support for the WEBSERVICE function (#1409) Co-authored-by: Paul Kievits --- CHANGELOG.md | 1 + composer.json | 3 +- composer.lock | 294 +++++++++++++++++- .../Calculation/Calculation.php | 5 + src/PhpSpreadsheet/Calculation/Category.php | 1 + src/PhpSpreadsheet/Calculation/Web.php | 53 ++++ src/PhpSpreadsheet/Settings.php | 29 ++ .../Functions/Web/WebServiceTest.php | 54 ++++ .../DocumentGeneratorTest.php | 5 + tests/data/Calculation/Web/WEBSERVICE.php | 28 ++ 10 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Web.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php create mode 100644 tests/data/Calculation/Web/WEBSERVICE.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b97431c5..f667053f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add Cell Address Helper to provide conversions between the R1C1 and A1 address formats [#1558](https://github.com/PHPOffice/PhpSpreadsheet/pull/1558) - Add ability to edit Html/Pdf before saving [#1499](https://github.com/PHPOffice/PhpSpreadsheet/pull/1499) - Add ability to set codepage explicitly for BIFF5 [#1018](https://github.com/PHPOffice/PhpSpreadsheet/issues/1018) +- Added support for the WEBSERVICE function ### Fixed diff --git a/composer.json b/composer.json index b480c082..d4810ce6 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,8 @@ "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^1.4", "markbaker/matrix": "^1.2", - "psr/simple-cache": "^1.0" + "psr/simple-cache": "^1.0", + "guzzlehttp/guzzle": "^7.0" }, "require-dev": { "dompdf/dompdf": "^0.8.5", diff --git a/composer.lock b/composer.lock index 9f5ae4aa..99de82bf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,211 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4103c8180a2e28881d3dbb45e835e863", + "content-hash": "b7ea4dea7ce2e1c2299029fe978d2173", "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "2d9d3c186a6637a43193e66b097c50e4451eaab2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/2d9d3c186a6637a43193e66b097c50e4451eaab2", + "reference": "2d9d3c186a6637a43193e66b097c50e4451eaab2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": "^7.2.5", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.0", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-phpunit8", + "phpunit/phpunit": "^8.5.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "time": "2020-06-27T10:33:25+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2019-07-01T23:21:34+00:00" + }, { "name": "maennchen/zipstream-php", "version": "2.1.0", @@ -277,6 +480,55 @@ ], "time": "2020-02-14T08:15:52+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "496a823ef742b632934724bf769560c2a5c7c44e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/496a823ef742b632934724bf769560c2a5c7c44e", + "reference": "496a823ef742b632934724bf769560c2a5c7c44e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "time": "2018-10-30T23:29:13+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -375,6 +627,46 @@ ], "time": "2017-10-23T01:57:42+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.17.1", diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index ed630354..a8f93a88 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2184,6 +2184,11 @@ class Calculation 'functionCall' => [LookupRef::class, 'VLOOKUP'], 'argumentCount' => '3,4', ], + 'WEBSERVICE' => [ + 'category' => Category::CATEGORY_WEB, + 'functionCall' => [Web::class, 'WEBSERVICE'], + 'argumentCount' => '1', + ], 'WEEKDAY' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, 'functionCall' => [DateTime::class, 'WEEKDAY'], diff --git a/src/PhpSpreadsheet/Calculation/Category.php b/src/PhpSpreadsheet/Calculation/Category.php index 7574cb47..96bb72ad 100644 --- a/src/PhpSpreadsheet/Calculation/Category.php +++ b/src/PhpSpreadsheet/Calculation/Category.php @@ -16,4 +16,5 @@ abstract class Category const CATEGORY_MATH_AND_TRIG = 'Math and Trig'; const CATEGORY_STATISTICAL = 'Statistical'; const CATEGORY_TEXT_AND_DATA = 'Text and Data'; + const CATEGORY_WEB = 'Web'; } diff --git a/src/PhpSpreadsheet/Calculation/Web.php b/src/PhpSpreadsheet/Calculation/Web.php new file mode 100644 index 00000000..9f0faf99 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Web.php @@ -0,0 +1,53 @@ + 2048) { + return Functions::VALUE(); // Invalid URL length + } + + if (!preg_match('/^http[s]?:\/\//', $url)) { + return Functions::VALUE(); // Invalid protocol + } + + // Get results from the the webservice + $client = Settings::getHttpClient(); + $request = new Request('GET', $url); + + try { + $response = $client->sendRequest($request); + } catch (ClientExceptionInterface $e) { + return Functions::VALUE(); // cURL error + } + + if ($response->getStatusCode() != 200) { + return Functions::VALUE(); // cURL error + } + + $output = (string) $response->getBody(); + if (strlen($output) > 32767) { + return Functions::VALUE(); // Output not a string or too long + } + + return $output; + } +} diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 4e0c91ef..15218c72 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -2,9 +2,11 @@ namespace PhpOffice\PhpSpreadsheet; +use GuzzleHttp\Client; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Chart\Renderer\IRenderer; use PhpOffice\PhpSpreadsheet\Collection\Memory; +use Psr\Http\Client\ClientInterface; use Psr\SimpleCache\CacheInterface; class Settings @@ -42,6 +44,13 @@ class Settings */ private static $cache; + /** + * The HTTP client implementation to be used for network request. + * + * @var ClientInterface + */ + private static $client; + /** * Set the locale code to use for formula translations and any special formatting. * @@ -156,4 +165,24 @@ class Settings return self::$cache; } + + /** + * Set the HTTP client implementation to be used for network request. + */ + public static function setHttpClient(ClientInterface $httpClient): void + { + self::$client = $httpClient; + } + + /** + * Get the HTTP client implementation to be used for network request. + */ + public static function getHttpClient(): ClientInterface + { + if (!self::$client) { + self::$client = new Client(); + } + + return self::$client; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php new file mode 100644 index 00000000..acc83cff --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php @@ -0,0 +1,54 @@ + $handlerStack]); + } + + protected function setUp(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + + /** + * @dataProvider providerWEBSERVICE + */ + public function testWEBSERVICE(string $expectedResult, string $url): void + { + Settings::setHttpClient(self::$client); + $result = Web::WEBSERVICE($url); + self::assertEquals($expectedResult, $result); + } + + public function providerWEBSERVICE(): array + { + return require 'tests/data/Calculation/Web/WEBSERVICE.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php b/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php index ac9af838..8a34f1c0 100644 --- a/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php @@ -127,6 +127,11 @@ Excel Function | PhpSpreadsheet Function Excel Function | PhpSpreadsheet Function --------------------|------------------------------------------- +## CATEGORY_WEB + +Excel Function | PhpSpreadsheet Function +--------------------|------------------------------------------- + EXPECTED ], diff --git a/tests/data/Calculation/Web/WEBSERVICE.php b/tests/data/Calculation/Web/WEBSERVICE.php new file mode 100644 index 00000000..6d9934da --- /dev/null +++ b/tests/data/Calculation/Web/WEBSERVICE.php @@ -0,0 +1,28 @@ + Date: Mon, 29 Jun 2020 10:23:33 +0900 Subject: [PATCH 057/153] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f667053f..a945169e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add Cell Address Helper to provide conversions between the R1C1 and A1 address formats [#1558](https://github.com/PHPOffice/PhpSpreadsheet/pull/1558) - Add ability to edit Html/Pdf before saving [#1499](https://github.com/PHPOffice/PhpSpreadsheet/pull/1499) - Add ability to set codepage explicitly for BIFF5 [#1018](https://github.com/PHPOffice/PhpSpreadsheet/issues/1018) -- Added support for the WEBSERVICE function +- Added support for the WEBSERVICE function [#1409](https://github.com/PHPOffice/PhpSpreadsheet/pull/1409) ### Fixed From 73fe58fdc9f3977619bec6b26e867eb2ac059dcd Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Mon, 29 Jun 2020 10:51:21 +0900 Subject: [PATCH 058/153] 1.14.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a945169e..ebdbf775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [1.14.0] - 2020-06-29 ### Added From 39f1a611207ce74c87ee51bf2bcb9afb9450e221 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Mon, 29 Jun 2020 10:52:36 +0900 Subject: [PATCH 059/153] CHANGELOG placeholder --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebdbf775..48ee6293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] + +### Added + +- nothing + +### Fixed + +- nothing + +### Changed + +- nothing + ## [1.14.0] - 2020-06-29 ### Added From 8712fb0424cc7240405c6c708a571f0f7ac3bfee Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Mon, 29 Jun 2020 11:28:05 +0900 Subject: [PATCH 060/153] Drop polyfill that are not needed since PHP 7.2 --- src/PhpSpreadsheet/Chart/Renderer/JpGraph.php | 2 -- src/PhpSpreadsheet/Chart/Renderer/Polyfill.php | 9 --------- 2 files changed, 11 deletions(-) delete mode 100644 src/PhpSpreadsheet/Chart/Renderer/Polyfill.php diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php index 5546da58..02fbfed7 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php @@ -21,8 +21,6 @@ use ScatterPlot; use Spline; use StockPlot; -require_once __DIR__ . '/Polyfill.php'; - class JpGraph implements IRenderer { private static $width = 640; diff --git a/src/PhpSpreadsheet/Chart/Renderer/Polyfill.php b/src/PhpSpreadsheet/Chart/Renderer/Polyfill.php deleted file mode 100644 index 5e233abd..00000000 --- a/src/PhpSpreadsheet/Chart/Renderer/Polyfill.php +++ /dev/null @@ -1,9 +0,0 @@ - Date: Mon, 29 Jun 2020 20:15:41 +0200 Subject: [PATCH 061/153] Support pageOrder in page setup for Xlsx Reader and Writer --- src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php | 3 +++ src/PhpSpreadsheet/Worksheet/PageSetup.php | 19 +++++++++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 3 +++ 3 files changed, 25 insertions(+) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php index bfb7a1f5..b556703c 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php @@ -69,6 +69,9 @@ class PageSetup extends BaseParserClass self::boolean((string) $xmlSheet->pageSetup['useFirstPageNumber'])) { $docPageSetup->setFirstPageNumber((int) ($xmlSheet->pageSetup['firstPageNumber'])); } + if (isset($xmlSheet->pageSetup['pageOrder'])) { + $docPageSetup->setPageOrder((string) $xmlSheet->pageSetup['pageOrder']); + } $relAttributes = $xmlSheet->pageSetup->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'); if (isset($relAttributes['id'])) { diff --git a/src/PhpSpreadsheet/Worksheet/PageSetup.php b/src/PhpSpreadsheet/Worksheet/PageSetup.php index f29dbad5..f52b414a 100644 --- a/src/PhpSpreadsheet/Worksheet/PageSetup.php +++ b/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -156,6 +156,9 @@ class PageSetup const SETPRINTRANGE_OVERWRITE = 'O'; const SETPRINTRANGE_INSERT = 'I'; + const PAGEORDER_OVER_THEN_DOWN = 'overThenDown'; + const PAGEORDER_DOWN_THEN_OVER = 'downThenOver'; + /** * Paper size. * @@ -246,6 +249,8 @@ class PageSetup */ private $firstPageNumber; + private $pageOrder; + /** * Create a new PageSetup. */ @@ -818,6 +823,20 @@ class PageSetup return $this->setFirstPageNumber(null); } + public function getPageOrder(): string + { + return $this->pageOrder; + } + + public function setPageOrder(?string $pageOrder): self + { + if ($pageOrder === null || $pageOrder === self::PAGEORDER_DOWN_THEN_OVER || $pageOrder === self::PAGEORDER_OVER_THEN_DOWN) { + $this->pageOrder = $pageOrder; + } + + return $this; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index be064256..2a8a3add 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -875,6 +875,9 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('firstPageNumber', $pSheet->getPageSetup()->getFirstPageNumber()); $objWriter->writeAttribute('useFirstPageNumber', '1'); } + if ($pSheet->getPageSetup()->getPageOrder() !== null) { + $objWriter->writeAttribute('pageOrder', $pSheet->getPageSetup()->getPageOrder()); + } $getUnparsedLoadedData = $pSheet->getParent()->getUnparsedLoadedData(); if (isset($getUnparsedLoadedData['sheets'][$pSheet->getCodeName()]['pageSetupRelId'])) { From 21b34f0afbabb30da2bd19980bb052ccf1295129 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 29 Jun 2020 20:27:47 +0200 Subject: [PATCH 062/153] pageOrder is nullable --- src/PhpSpreadsheet/Worksheet/PageSetup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Worksheet/PageSetup.php b/src/PhpSpreadsheet/Worksheet/PageSetup.php index f52b414a..290b8349 100644 --- a/src/PhpSpreadsheet/Worksheet/PageSetup.php +++ b/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -823,7 +823,7 @@ class PageSetup return $this->setFirstPageNumber(null); } - public function getPageOrder(): string + public function getPageOrder(): ?string { return $this->pageOrder; } From 362b18ca122412eb12d2287aa9f329dba328cd54 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 00:53:10 +0200 Subject: [PATCH 063/153] Read Print Settings Page Order in Xls Reader And eliminate switch for simple boolean portrait/landscape option --- src/PhpSpreadsheet/Reader/Xls.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 11a6195c..c60ea772 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -3444,6 +3444,9 @@ class Xls extends BaseReader // offset: 10; size: 2; option flags + // bit: 0; mask: 0x0001; 0=landscape, 1=portrait + $isOverThenDown= (0x0001 & self::getUInt2d($recordData, 10)); + // bit: 1; mask: 0x0002; 0=landscape, 1=portrait $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1; @@ -3453,16 +3456,8 @@ class Xls extends BaseReader if (!$isNotInit) { $this->phpSheet->getPageSetup()->setPaperSize($paperSize); - switch ($isPortrait) { - case 0: - $this->phpSheet->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); - - break; - case 1: - $this->phpSheet->getPageSetup()->setOrientation(PageSetup::ORIENTATION_PORTRAIT); - - break; - } + $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER); + $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE); $this->phpSheet->getPageSetup()->setScale($scale, false); $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages); From 4060cdec7e9e73919e154e8e89d0873def567588 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 01:11:53 +0200 Subject: [PATCH 064/153] Fix comment --- src/PhpSpreadsheet/Reader/Xls.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index c60ea772..5e4ee225 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -3444,7 +3444,7 @@ class Xls extends BaseReader // offset: 10; size: 2; option flags - // bit: 0; mask: 0x0001; 0=landscape, 1=portrait + // bit: 0; mask: 0x0001; 0=down then over, 1=over then down $isOverThenDown= (0x0001 & self::getUInt2d($recordData, 10)); // bit: 1; mask: 0x0002; 0=landscape, 1=portrait From fbb04c1f8203420e9aa7171b8f2a87fe1c55b642 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 07:01:51 +0200 Subject: [PATCH 065/153] Xls Writer changes to save print order --- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 0dce7ba5..1a4504ed 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -1729,11 +1729,12 @@ class Worksheet extends BIFFwriter $numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin $iCopies = 0x01; // Number of copies - $fLeftToRight = 0x0; // Print over then down - + // Order of printing pages + $fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() == PageSetup::PAGEORDER_OVER_THEN_DOWN + ? 0x0 : 0x1; // Page orientation - $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) ? - 0x0 : 0x1; + $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) + ? 0x0 : 0x1; $fNoPls = 0x0; // Setup not read from printer $fNoColor = 0x0; // Print black and white From 1f865c84c0d960c09b08d731edbac20797131cad Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 07:24:12 +0200 Subject: [PATCH 066/153] Keep phpcs happy --- src/PhpSpreadsheet/Reader/Xls.php | 2 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 5e4ee225..cdac07d0 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -3445,7 +3445,7 @@ class Xls extends BaseReader // offset: 10; size: 2; option flags // bit: 0; mask: 0x0001; 0=down then over, 1=over then down - $isOverThenDown= (0x0001 & self::getUInt2d($recordData, 10)); + $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10)); // bit: 1; mask: 0x0002; 0=landscape, 1=portrait $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1; diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 1a4504ed..eb6b479d 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -1730,8 +1730,8 @@ class Worksheet extends BIFFwriter $iCopies = 0x01; // Number of copies // Order of printing pages - $fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() == PageSetup::PAGEORDER_OVER_THEN_DOWN - ? 0x0 : 0x1; + $fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() === PageSetup::PAGEORDER_DOWN_THEN_OVER + ? 0x1 : 0x0; // Page orientation $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) ? 0x0 : 0x1; From 3780072ae96fbe8f61c1b2109d217e62616c4f1e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 19:07:56 +0200 Subject: [PATCH 067/153] Retrieve basic print settings in the Ods Reader --- src/PhpSpreadsheet/Reader/Ods.php | 89 ++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index d638d1fb..d3482716 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -19,11 +19,19 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use XMLReader; use ZipArchive; class Ods extends BaseReader { + private $pageLayoutStyles = []; + + private $masterStylesCrossReference = []; + + private $masterPrintStylesCrossReference = []; + /** * Create a new Ods Reader instance. */ @@ -276,7 +284,45 @@ class Ods extends BaseReader (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta); - // Content + // Styles + + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->securityScanner->scan($zip->getFromName('styles.xml')), + Settings::getLibXmlLoaderOptions() + ); + $officeNs = $dom->lookupNamespaceUri('office'); + $stylesNs = $dom->lookupNamespaceUri('style'); + + $styles = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles') + ->item(0) + ->getElementsByTagNameNS($stylesNs, 'page-layout'); + + foreach ($styles as $styleSet) { + $styleName = $styleSet->getAttributeNS($stylesNs, 'name'); + $pageLayoutProperties = $styleSet->getElementsByTagNameNS($stylesNs, 'page-layout-properties')[0]; + $styleOrientation = $pageLayoutProperties->getAttributeNS($stylesNs, 'print-orientation'); + $styleScale = $pageLayoutProperties->getAttributeNS($stylesNs, 'scale-to'); + $stylePrintOrder = $pageLayoutProperties->getAttributeNS($stylesNs, 'print-page-order'); + + $this->pageLayoutStyles[$styleName] = (object) [ + 'orientation' => $styleOrientation, + 'scale' => $styleScale, + 'printOrder' => $stylePrintOrder, + ]; + } + + $styleMasterLookup = $dom->getElementsByTagNameNS($officeNs, 'master-styles') + ->item(0) + ->getElementsByTagNameNS($stylesNs, 'master-page'); + + foreach ($styleMasterLookup as $styleMasterSet) { + $styleMasterName = $styleMasterSet->getAttributeNS($stylesNs, 'name'); + $pageLayoutName = $styleMasterSet->getAttributeNS($stylesNs, 'page-layout-name'); + $this->masterPrintStylesCrossReference[$styleMasterName] = $pageLayoutName; + } + + // Main Content $dom = new DOMDocument('1.01', 'UTF-8'); $dom->loadXML( @@ -289,6 +335,20 @@ class Ods extends BaseReader $textNs = $dom->lookupNamespaceUri('text'); $xlinkNs = $dom->lookupNamespaceUri('xlink'); + $styleXReferences = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles') + ->item(0) + ->getElementsByTagNameNS($stylesNs, 'style'); + + foreach ($styleXReferences as $styleXreferenceSet) { + $styleXRefName = $styleXreferenceSet->getAttributeNS($stylesNs, 'name'); + $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($stylesNs, 'master-page-name'); + if (!empty($stylePageLayoutName)) { + $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; + } + } + + // Content + $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body') ->item(0) ->getElementsByTagNameNS($officeNs, 'spreadsheet'); @@ -309,6 +369,8 @@ class Ods extends BaseReader continue; } + $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name'); + // Create sheet if ($worksheetID > 0) { $spreadsheet->createSheet(); // First sheet is added by default @@ -319,7 +381,7 @@ class Ods extends BaseReader // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in // formula cells... during the load, all formulae should be correct, and we're simply // bringing the worksheet name in line with the formula, not the reverse - $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false); } // Go through every child of table element @@ -641,6 +703,7 @@ class Ods extends BaseReader break; } } + $this->getPrintSettings($spreadsheet->getActiveSheet(), $worksheetStyleName); ++$worksheetID; } } @@ -649,6 +712,28 @@ class Ods extends BaseReader return $spreadsheet; } + private function getPrintSettings(Worksheet $worksheet, string $styleName) + { + if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { + return; + } + $masterStyleName = $this->masterStylesCrossReference[$styleName]; + + if (!array_key_exists($masterStyleName, $this->masterPrintStylesCrossReference)) { + return; + } + $printSettingsIndex = $this->masterPrintStylesCrossReference[$masterStyleName]; + + if (!array_key_exists($printSettingsIndex, $this->pageLayoutStyles)) { + return; + } + $printSettings = $this->pageLayoutStyles[$printSettingsIndex]; + + $worksheet->getPageSetup()->setOrientation($printSettings->orientation ?? PageSetup::ORIENTATION_DEFAULT); + $worksheet->getPageSetup()->setScale((int) trim($printSettings->scale, '%')); + $worksheet->getPageSetup()->setPageOrder($printSettings->printOrder === 'ltr' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER); + } + /** * Recursively scan element. * From 13a3363410e44c7d30814be3e7c94c2e68ddbff7 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 19:15:08 +0200 Subject: [PATCH 068/153] Return void, just to keep phpcs happy --- src/PhpSpreadsheet/Reader/Ods.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index d3482716..040bfffa 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -712,7 +712,7 @@ class Ods extends BaseReader return $spreadsheet; } - private function getPrintSettings(Worksheet $worksheet, string $styleName) + private function getPrintSettings(Worksheet $worksheet, string $styleName): void { if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { return; From 4dadf4a5c8d816e2f9ff04d9f803b5b1ad18f680 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 22:23:33 +0200 Subject: [PATCH 069/153] Refactor reading Ods Page Settings into a separate class --- src/PhpSpreadsheet/Reader/Ods.php | 69 +----------- .../Reader/Ods/PageSettings.php | 104 ++++++++++++++++++ 2 files changed, 108 insertions(+), 65 deletions(-) create mode 100644 src/PhpSpreadsheet/Reader/Ods/PageSettings.php diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 040bfffa..ad87192f 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -11,6 +11,7 @@ use DOMNode; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings; use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -19,8 +20,6 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use XMLReader; use ZipArchive; @@ -291,36 +290,8 @@ class Ods extends BaseReader $this->securityScanner->scan($zip->getFromName('styles.xml')), Settings::getLibXmlLoaderOptions() ); - $officeNs = $dom->lookupNamespaceUri('office'); - $stylesNs = $dom->lookupNamespaceUri('style'); - $styles = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles') - ->item(0) - ->getElementsByTagNameNS($stylesNs, 'page-layout'); - - foreach ($styles as $styleSet) { - $styleName = $styleSet->getAttributeNS($stylesNs, 'name'); - $pageLayoutProperties = $styleSet->getElementsByTagNameNS($stylesNs, 'page-layout-properties')[0]; - $styleOrientation = $pageLayoutProperties->getAttributeNS($stylesNs, 'print-orientation'); - $styleScale = $pageLayoutProperties->getAttributeNS($stylesNs, 'scale-to'); - $stylePrintOrder = $pageLayoutProperties->getAttributeNS($stylesNs, 'print-page-order'); - - $this->pageLayoutStyles[$styleName] = (object) [ - 'orientation' => $styleOrientation, - 'scale' => $styleScale, - 'printOrder' => $stylePrintOrder, - ]; - } - - $styleMasterLookup = $dom->getElementsByTagNameNS($officeNs, 'master-styles') - ->item(0) - ->getElementsByTagNameNS($stylesNs, 'master-page'); - - foreach ($styleMasterLookup as $styleMasterSet) { - $styleMasterName = $styleMasterSet->getAttributeNS($stylesNs, 'name'); - $pageLayoutName = $styleMasterSet->getAttributeNS($stylesNs, 'page-layout-name'); - $this->masterPrintStylesCrossReference[$styleMasterName] = $pageLayoutName; - } + $pageSettings = new PageSettings($dom); // Main Content @@ -335,17 +306,7 @@ class Ods extends BaseReader $textNs = $dom->lookupNamespaceUri('text'); $xlinkNs = $dom->lookupNamespaceUri('xlink'); - $styleXReferences = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles') - ->item(0) - ->getElementsByTagNameNS($stylesNs, 'style'); - - foreach ($styleXReferences as $styleXreferenceSet) { - $styleXRefName = $styleXreferenceSet->getAttributeNS($stylesNs, 'name'); - $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($stylesNs, 'master-page-name'); - if (!empty($stylePageLayoutName)) { - $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; - } - } + $pageSettings->readStyleCrossReferences($dom); // Content @@ -703,7 +664,7 @@ class Ods extends BaseReader break; } } - $this->getPrintSettings($spreadsheet->getActiveSheet(), $worksheetStyleName); + $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); ++$worksheetID; } } @@ -712,28 +673,6 @@ class Ods extends BaseReader return $spreadsheet; } - private function getPrintSettings(Worksheet $worksheet, string $styleName): void - { - if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { - return; - } - $masterStyleName = $this->masterStylesCrossReference[$styleName]; - - if (!array_key_exists($masterStyleName, $this->masterPrintStylesCrossReference)) { - return; - } - $printSettingsIndex = $this->masterPrintStylesCrossReference[$masterStyleName]; - - if (!array_key_exists($printSettingsIndex, $this->pageLayoutStyles)) { - return; - } - $printSettings = $this->pageLayoutStyles[$printSettingsIndex]; - - $worksheet->getPageSetup()->setOrientation($printSettings->orientation ?? PageSetup::ORIENTATION_DEFAULT); - $worksheet->getPageSetup()->setScale((int) trim($printSettings->scale, '%')); - $worksheet->getPageSetup()->setPageOrder($printSettings->printOrder === 'ltr' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER); - } - /** * Recursively scan element. * diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php new file mode 100644 index 00000000..5ff1d58b --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -0,0 +1,104 @@ +setDomNameSpaces($styleDom); + $this->readPageSettingStyles($styleDom); + $this->readStyleMasterLookup($styleDom); + } + + private function setDomNameSpaces(DOMDocument $styleDom): void + { + $this->officeNs = $styleDom->lookupNamespaceUri('office'); + $this->stylesNs = $styleDom->lookupNamespaceUri('style'); + } + + private function readPageSettingStyles(DOMDocument $styleDom): void + { + $styles = $styleDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles') + ->item(0) + ->getElementsByTagNameNS($this->stylesNs, 'page-layout'); + + foreach ($styles as $styleSet) { + $styleName = $styleSet->getAttributeNS($this->stylesNs, 'name'); + $pageLayoutProperties = $styleSet->getElementsByTagNameNS($this->stylesNs, 'page-layout-properties')[0]; + $styleOrientation = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-orientation'); + $styleScale = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'scale-to'); + $stylePrintOrder = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-page-order'); + + $this->pageLayoutStyles[$styleName] = (object) [ + 'orientation' => $styleOrientation, + 'scale' => $styleScale, + 'printOrder' => $stylePrintOrder, + ]; + } + } + + private function readStyleMasterLookup(DOMDocument $styleDom): void + { + $styleMasterLookup = $styleDom->getElementsByTagNameNS($this->officeNs, 'master-styles') + ->item(0) + ->getElementsByTagNameNS($this->stylesNs, 'master-page'); + + foreach ($styleMasterLookup as $styleMasterSet) { + $styleMasterName = $styleMasterSet->getAttributeNS($this->stylesNs, 'name'); + $pageLayoutName = $styleMasterSet->getAttributeNS($this->stylesNs, 'page-layout-name'); + $this->masterPrintStylesCrossReference[$styleMasterName] = $pageLayoutName; + } + } + + public function readStyleCrossReferences(DOMDocument $contentDom): void + { + $styleXReferences = $contentDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles') + ->item(0) + ->getElementsByTagNameNS($this->stylesNs, 'style'); + + foreach ($styleXReferences as $styleXreferenceSet) { + $styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name'); + $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name'); + if (!empty($stylePageLayoutName)) { + $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; + } + } + } + + public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { + return; + } + $masterStyleName = $this->masterStylesCrossReference[$styleName]; + + if (!array_key_exists($masterStyleName, $this->masterPrintStylesCrossReference)) { + return; + } + $printSettingsIndex = $this->masterPrintStylesCrossReference[$masterStyleName]; + + if (!array_key_exists($printSettingsIndex, $this->pageLayoutStyles)) { + return; + } + $printSettings = $this->pageLayoutStyles[$printSettingsIndex]; + + $worksheet->getPageSetup() + ->setOrientation($printSettings->orientation ?? PageSetup::ORIENTATION_DEFAULT) + ->setPageOrder($printSettings->printOrder === 'ltr' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) + ->setScale((int) trim($printSettings->scale, '%')); + } +} From 736d9ffd3b03667a2038a12876b8a0c5df0e54d9 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 30 Jun 2020 22:45:08 +0200 Subject: [PATCH 070/153] Gotta keep phpcs happy, even if it is only a blank line between property declarations --- src/PhpSpreadsheet/Reader/Ods/PageSettings.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php index 5ff1d58b..6810ef5f 100644 --- a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PageSettings { private $officeNs; + private $stylesNs; private $pageLayoutStyles = []; From 163da065054345959b25550702c532738b046d5e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 1 Jul 2020 14:50:13 +0200 Subject: [PATCH 071/153] Horizontal an dVertical centering for the Ods Reader --- src/PhpSpreadsheet/Reader/Ods/PageSettings.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php index 6810ef5f..4a1fc065 100644 --- a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -43,11 +43,14 @@ class PageSettings $styleOrientation = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-orientation'); $styleScale = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'scale-to'); $stylePrintOrder = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-page-order'); + $centered = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'table-centering'); $this->pageLayoutStyles[$styleName] = (object) [ 'orientation' => $styleOrientation, 'scale' => $styleScale, 'printOrder' => $stylePrintOrder, + 'horizontalCentered' => $centered === 'horizontal' || $centered === 'both', + 'verticalCentered' => $centered === 'vertical' || $centered === 'both', ]; } } @@ -100,6 +103,8 @@ class PageSettings $worksheet->getPageSetup() ->setOrientation($printSettings->orientation ?? PageSetup::ORIENTATION_DEFAULT) ->setPageOrder($printSettings->printOrder === 'ltr' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) - ->setScale((int) trim($printSettings->scale, '%')); + ->setScale((int) trim($printSettings->scale, '%')) + ->setHorizontalCentered($printSettings->horizontalCentered) + ->setVerticalCentered($printSettings->verticalCentered); } } From e644cc72d03df950e211c0b711e0a8b9f550c9fe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 1 Jul 2020 19:55:25 +0200 Subject: [PATCH 072/153] Additional print information for Gnumeric --- src/PhpSpreadsheet/Reader/Gnumeric.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 81096730..4d4988e9 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -17,6 +17,7 @@ use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; +use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; use XMLReader; @@ -431,15 +432,35 @@ class Gnumeric extends BaseReader } } + private function printInformation(SimpleXMLElement $sheet) + { + if (!$this->readDataOnly && isset($sheet->PrintInformation)) { + $printInformation = $sheet->PrintInformation[0]; + $scale = $printInformation->Scale->attributes()['percentage']; + $pageOrder = (string) $printInformation->order; + $orientation = (string) $printInformation->orientation; + $horizontalCentered = (bool) $printInformation->hcenter; + $verticalCentered = (bool) $printInformation->vcenter; + + $this->spreadsheet->getActiveSheet()->getPageSetup() + ->setPageOrder($pageOrder === 'r_then_d' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) + ->setScale((int) $scale) + ->setOrientation($orientation ?? PageSetup::ORIENTATION_DEFAULT) + ->setHorizontalCentered($horizontalCentered) + ->setVerticalCentered($verticalCentered); + } + } + private function sheetMargins(SimpleXMLElement $sheet): void { if (!$this->readDataOnly && isset($sheet->PrintInformation, $sheet->PrintInformation->Margins)) { foreach ($sheet->PrintInformation->Margins->children($this->gnm, true) as $key => $margin) { $marginAttributes = $margin->attributes(); - $marginSize = 72 / 100; // Default + $marginSize = $marginAttributes['Points'] ?? 72 / 100; // Default switch ($marginAttributes['PrefUnit']) { + case 'inch': case 'mm': - $marginSize = (int) ($marginAttributes['Points']) / 100; + $marginSize = ($marginAttributes['Points']) / 100; break; } @@ -513,6 +534,7 @@ class Gnumeric extends BaseReader // name in line with the formula, not the reverse $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + $this->printInformation($sheet); $this->sheetMargins($sheet); foreach ($sheet->Cells->Cell as $cell) { From c288c11d0150521e9383cc9cfa217f6891bf733b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 1 Jul 2020 20:06:36 +0200 Subject: [PATCH 073/153] Failed to avoid the void trap --- src/PhpSpreadsheet/Reader/Gnumeric.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 4d4988e9..d96d562e 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -432,7 +432,7 @@ class Gnumeric extends BaseReader } } - private function printInformation(SimpleXMLElement $sheet) + private function printInformation(SimpleXMLElement $sheet): void { if (!$this->readDataOnly && isset($sheet->PrintInformation)) { $printInformation = $sheet->PrintInformation[0]; From aecef1372f9582153b42df9b0e4fb8d67f5ef21c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 3 Jul 2020 19:26:57 +0200 Subject: [PATCH 074/153] Read Ods Margins --- src/PhpSpreadsheet/Reader/Ods.php | 6 ----- .../Reader/Ods/PageSettings.php | 27 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index ad87192f..4f164b6f 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -25,12 +25,6 @@ use ZipArchive; class Ods extends BaseReader { - private $pageLayoutStyles = []; - - private $masterStylesCrossReference = []; - - private $masterPrintStylesCrossReference = []; - /** * Create a new Ods Reader instance. */ diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php index 4a1fc065..05095752 100644 --- a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -12,6 +12,8 @@ class PageSettings private $stylesNs; + private $stylesFo; + private $pageLayoutStyles = []; private $masterStylesCrossReference = []; @@ -29,6 +31,7 @@ class PageSettings { $this->officeNs = $styleDom->lookupNamespaceUri('office'); $this->stylesNs = $styleDom->lookupNamespaceUri('style'); + $this->stylesFo = $styleDom->lookupNamespaceUri('fo'); } private function readPageSettingStyles(DOMDocument $styleDom): void @@ -44,6 +47,16 @@ class PageSettings $styleScale = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'scale-to'); $stylePrintOrder = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-page-order'); $centered = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'table-centering'); + $marginLeft = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-left'); + $marginRight = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-right'); + $marginTop = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-top'); + $marginBottom = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-bottom'); + $header = $styleSet->getElementsByTagNameNS($this->stylesNs, 'header-style')[0]; + $headerProperties = $header->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')[0]; + $marginHeader = $headerProperties->getAttributeNS($this->stylesFo, 'min-height'); + $footer = $styleSet->getElementsByTagNameNS($this->stylesNs, 'footer-style')[0]; + $footerProperties = $footer->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')[0]; + $marginFooter = $footerProperties->getAttributeNS($this->stylesFo, 'min-height'); $this->pageLayoutStyles[$styleName] = (object) [ 'orientation' => $styleOrientation, @@ -51,6 +64,12 @@ class PageSettings 'printOrder' => $stylePrintOrder, 'horizontalCentered' => $centered === 'horizontal' || $centered === 'both', 'verticalCentered' => $centered === 'vertical' || $centered === 'both', + 'marginLeft' => round((float) $marginLeft ?? 0.7, 5), + 'marginRight' => round((float) $marginRight ?? 0.7, 5), + 'marginTop' => round((float) $marginTop ?? 0.7, 5), + 'marginBottom' => round((float) $marginBottom ?? 0.7, 5), + 'marginHeader' => round((float) $marginHeader ?? 0.0, 5), + 'marginFooter' => round((float) $marginFooter ?? 0.0, 5), ]; } } @@ -106,5 +125,13 @@ class PageSettings ->setScale((int) trim($printSettings->scale, '%')) ->setHorizontalCentered($printSettings->horizontalCentered) ->setVerticalCentered($printSettings->verticalCentered); + + $worksheet->getPageMargins() + ->setLeft($printSettings->marginLeft) + ->setRight($printSettings->marginRight) + ->setTop($printSettings->marginTop) + ->setBottom($printSettings->marginBottom) + ->setHeader($printSettings->marginHeader) + ->setFooter($printSettings->marginFooter); } } From 84ba21400c5043b7bc68addbc7a6fc8ba7f6a2d7 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 4 Jul 2020 00:28:16 +0200 Subject: [PATCH 075/153] Fix datatype conversion for Gnumeric values --- src/PhpSpreadsheet/Reader/Gnumeric.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index d96d562e..8375b37e 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -436,18 +436,18 @@ class Gnumeric extends BaseReader { if (!$this->readDataOnly && isset($sheet->PrintInformation)) { $printInformation = $sheet->PrintInformation[0]; - $scale = $printInformation->Scale->attributes()['percentage']; + $scale = (string) $printInformation->Scale->attributes()['percentage']; $pageOrder = (string) $printInformation->order; $orientation = (string) $printInformation->orientation; - $horizontalCentered = (bool) $printInformation->hcenter; - $verticalCentered = (bool) $printInformation->vcenter; + $horizontalCentered = (string) $printInformation->hcenter->attributes()['value']; + $verticalCentered = (string) $printInformation->vcenter->attributes()['value']; $this->spreadsheet->getActiveSheet()->getPageSetup() ->setPageOrder($pageOrder === 'r_then_d' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) ->setScale((int) $scale) ->setOrientation($orientation ?? PageSetup::ORIENTATION_DEFAULT) - ->setHorizontalCentered($horizontalCentered) - ->setVerticalCentered($verticalCentered); + ->setHorizontalCentered((bool) $horizontalCentered) + ->setVerticalCentered((bool) $verticalCentered); } } From 8629337101fef7bf51fb7e062080de510438a94b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 5 Jul 2020 16:22:35 +0200 Subject: [PATCH 076/153] Retrieving print/page setup for the Xml Reader --- src/PhpSpreadsheet/Reader/Gnumeric.php | 106 ++++++++----- .../Reader/Ods/PageSettings.php | 18 ++- src/PhpSpreadsheet/Reader/Xml.php | 102 ++++++++++++ src/PhpSpreadsheet/Worksheet/PageMargins.php | 30 ++++ src/PhpSpreadsheet/Worksheet/PageSetup.php | 6 +- .../Reader/Gnumeric/PageSetupTest.php | 147 +++++++++++++++++ .../Reader/Ods/PageSetupTest.php | 149 ++++++++++++++++++ .../Reader/Xls/PageSetupTest.php | 149 ++++++++++++++++++ .../Reader/Xlsx/PageSetupTest.php | 149 ++++++++++++++++++ .../Reader/Xml/PageSetupTest.php | 148 +++++++++++++++++ .../Worksheet/PageMarginsTest.php | 89 +++++++++++ 11 files changed, 1045 insertions(+), 48 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/PageSetupTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/PageSetupTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetupTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/PageMarginsTest.php diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 8375b37e..32105477 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -17,6 +17,7 @@ use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; +use PhpOffice\PhpSpreadsheet\Worksheet\PageMargins; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; @@ -24,6 +25,8 @@ use XMLReader; class Gnumeric extends BaseReader { + private const UOM_CONVERSION_POINTS_TO_CENTIMETERS = 0.03527777778; + /** * Shared Expressions. * @@ -402,6 +405,72 @@ class Gnumeric extends BaseReader } } + private function printInformation(SimpleXMLElement $sheet): void + { + if (!$this->readDataOnly && isset($sheet->PrintInformation)) { + $printInformation = $sheet->PrintInformation[0]; + $scale = (string) $printInformation->Scale->attributes()['percentage']; + $pageOrder = (string) $printInformation->order; + $orientation = (string) $printInformation->orientation; + $horizontalCentered = (string) $printInformation->hcenter->attributes()['value']; + $verticalCentered = (string) $printInformation->vcenter->attributes()['value']; + + $this->spreadsheet->getActiveSheet()->getPageSetup() + ->setPageOrder($pageOrder === 'r_then_d' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) + ->setScale((int) $scale) + ->setOrientation($orientation ?? PageSetup::ORIENTATION_DEFAULT) + ->setHorizontalCentered((bool) $horizontalCentered) + ->setVerticalCentered((bool) $verticalCentered); + } + } + + private function sheetMargins(SimpleXMLElement $sheet): void + { + if (!$this->readDataOnly && isset($sheet->PrintInformation, $sheet->PrintInformation->Margins)) { + $marginSet = [ + // Default Settings + 'top' => 0.75, + 'header' => 0.3, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.75, + 'footer' => 0.3, + ]; + + foreach ($sheet->PrintInformation->Margins->children($this->gnm, true) as $key => $margin) { + $marginAttributes = $margin->attributes(); + $marginSize = ($marginAttributes['Points']) ?? 72; // Default is 72pt + // Convert value in points to inches + $marginSize = PageMargins::fromPoints((float) $marginSize); + $marginSet[$key] = $marginSize; + } + + foreach ($marginSet as $key => $marginSize) { + // Gnumeric is quirky in the way it displays the header/footer values: + // header is actually the sum of top and header; footer is actually the sum of bottom and footer + // then top is actually the header value, and bottom is actually the footer value + switch ($key) { + case 'left': + case 'right': + $this->sheetMargin($key, $marginSize); + break; + case 'top': + $this->sheetMargin($key, $marginSet['header'] ?? 0); + break; + case 'bottom': + $this->sheetMargin($key, $marginSet['footer'] ?? 0); + break; + case 'header': + $this->sheetMargin($key, ($marginSet['top'] ?? 0) - $marginSize); + break; + case 'footer': + $this->sheetMargin($key, ($marginSet['bottom'] ?? 0) - $marginSize); + break; + } + } + } + } + private function sheetMargin(string $key, float $marginSize): void { switch ($key) { @@ -432,43 +501,6 @@ class Gnumeric extends BaseReader } } - private function printInformation(SimpleXMLElement $sheet): void - { - if (!$this->readDataOnly && isset($sheet->PrintInformation)) { - $printInformation = $sheet->PrintInformation[0]; - $scale = (string) $printInformation->Scale->attributes()['percentage']; - $pageOrder = (string) $printInformation->order; - $orientation = (string) $printInformation->orientation; - $horizontalCentered = (string) $printInformation->hcenter->attributes()['value']; - $verticalCentered = (string) $printInformation->vcenter->attributes()['value']; - - $this->spreadsheet->getActiveSheet()->getPageSetup() - ->setPageOrder($pageOrder === 'r_then_d' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) - ->setScale((int) $scale) - ->setOrientation($orientation ?? PageSetup::ORIENTATION_DEFAULT) - ->setHorizontalCentered((bool) $horizontalCentered) - ->setVerticalCentered((bool) $verticalCentered); - } - } - - private function sheetMargins(SimpleXMLElement $sheet): void - { - if (!$this->readDataOnly && isset($sheet->PrintInformation, $sheet->PrintInformation->Margins)) { - foreach ($sheet->PrintInformation->Margins->children($this->gnm, true) as $key => $margin) { - $marginAttributes = $margin->attributes(); - $marginSize = $marginAttributes['Points'] ?? 72 / 100; // Default - switch ($marginAttributes['PrefUnit']) { - case 'inch': - case 'mm': - $marginSize = ($marginAttributes['Points']) / 100; - - break; - } - $this->sheetMargin($key, (float) $marginSize); - } - } - } - private function processComments(SimpleXMLElement $sheet): void { if ((!$this->readDataOnly) && (isset($sheet->Objects))) { diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php index 05095752..77341aab 100644 --- a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -47,6 +47,7 @@ class PageSettings $styleScale = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'scale-to'); $stylePrintOrder = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-page-order'); $centered = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'table-centering'); + $marginLeft = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-left'); $marginRight = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-right'); $marginTop = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-top'); @@ -59,17 +60,18 @@ class PageSettings $marginFooter = $footerProperties->getAttributeNS($this->stylesFo, 'min-height'); $this->pageLayoutStyles[$styleName] = (object) [ - 'orientation' => $styleOrientation, - 'scale' => $styleScale, + 'orientation' => $styleOrientation ?: PageSetup::ORIENTATION_DEFAULT, + 'scale' => $styleScale ?: 100, 'printOrder' => $stylePrintOrder, 'horizontalCentered' => $centered === 'horizontal' || $centered === 'both', 'verticalCentered' => $centered === 'vertical' || $centered === 'both', - 'marginLeft' => round((float) $marginLeft ?? 0.7, 5), - 'marginRight' => round((float) $marginRight ?? 0.7, 5), - 'marginTop' => round((float) $marginTop ?? 0.7, 5), - 'marginBottom' => round((float) $marginBottom ?? 0.7, 5), - 'marginHeader' => round((float) $marginHeader ?? 0.0, 5), - 'marginFooter' => round((float) $marginFooter ?? 0.0, 5), + // margin size is already stored in inches, so no UOM conversion is required + 'marginLeft' => (float) $marginLeft ?? 0.7, + 'marginRight' => (float) $marginRight ?? 0.7, + 'marginTop' => (float) $marginTop ?? 0.3, + 'marginBottom' => (float) $marginBottom ?? 0.3, + 'marginHeader' => (float) $marginHeader ?? 0.45, + 'marginFooter' => (float) $marginFooter ?? 0.45, ]; } } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index f9ad5475..9ea0a82d 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -15,6 +15,7 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Font; +use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use SimpleXMLElement; /** @@ -626,6 +627,27 @@ class Xml extends BaseReader ++$rowID; } + + $xml_x = $worksheet->children($namespaces['x']); + if (isset($xml_x->WorksheetOptions)) { + $printSettings = $this->pageSetup($xml_x, $namespaces, $this->getPrintDefaults()); + $printSettings = $this->printSetup($xml_x, $namespaces, $printSettings); + + $spreadsheet->getActiveSheet()->getPageSetup() + ->setPaperSize($printSettings->paperSize) + ->setOrientation($printSettings->orientation) + ->setScale($printSettings->scale) + ->setVerticalCentered($printSettings->verticalCentered) + ->setHorizontalCentered($printSettings->horizontalCentered) + ->setPageOrder($printSettings->printOrder); + $spreadsheet->getActiveSheet()->getPageMargins() + ->setTop($printSettings->topMargin) + ->setHeader($printSettings->headerMargin) + ->setLeft($printSettings->leftMargin) + ->setRight($printSettings->rightMargin) + ->setBottom($printSettings->bottomMargin) + ->setFooter($printSettings->footerMargin); + } } ++$worksheetID; } @@ -855,4 +877,84 @@ class Xml extends BaseReader } } } + + private function getPrintDefaults() + { + return (object) [ + 'paperSize' => 9, + 'orientation' => PageSetup::ORIENTATION_DEFAULT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'printOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + 'topMargin' => 0.75, + 'headerMargin' => 0.3, + 'leftMargin' => 0.7, + 'rightMargin' => 0.7, + 'bottomMargin' => 0.75, + 'footerMargin' => 0.3, + ]; + } + + private function pageSetup(SimpleXMLElement $xmlX, array $namespaces, \stdClass $printDefaults): \stdClass + { + if (isset($xmlX->WorksheetOptions->PageSetup)) { + foreach ($xmlX->WorksheetOptions->PageSetup as $pageSetupData) { + foreach ($pageSetupData as $pageSetupKey => $pageSetupValue) { + $pageSetupAttributes = $pageSetupValue->attributes($namespaces['x']); + switch ($pageSetupKey) { + case 'Layout': + $printDefaults->orientation = (string) strtolower($pageSetupAttributes->Orientation) ?: PageSetup::ORIENTATION_PORTRAIT; + $printDefaults->horizontalCentered = (bool)$pageSetupAttributes->CenterHorizontal ?: false; + $printDefaults->verticalCentered = (bool)$pageSetupAttributes->CenterVertical ?: false; + + break; + case 'Header': + $printDefaults->headerMargin = (float)$pageSetupAttributes->Margin ?: 1.0; + + break; + case 'Footer': + $printDefaults->footerMargin = (float)$pageSetupAttributes->Margin ?: 1.0; + + break; + case 'PageMargins': + $printDefaults->leftMargin = (float) $pageSetupAttributes->Left ?: 1.0; + $printDefaults->rightMargin = (float) $pageSetupAttributes->Right ?: 1.0; + $printDefaults->topMargin = (float) $pageSetupAttributes->Top ?: 1.0; + $printDefaults->bottomMargin = (float) $pageSetupAttributes->Bottom ?: 1.0; + + break; + } + } + } + } + + return $printDefaults; + } + + private function printSetup(SimpleXMLElement $xmlX, array $namespaces, \stdClass $printDefaults): \stdClass + { + if (isset($xmlX->WorksheetOptions->Print)) { + foreach ($xmlX->WorksheetOptions->Print as $printData) { + foreach ($printData as $printKey => $printValue) { + switch ($printKey) { + case 'LeftToRight': + $printDefaults->printOrder = PageSetup::PAGEORDER_OVER_THEN_DOWN; + + break; + case 'PaperSizeIndex': + $printDefaults->paperSize = (int) $printValue ?: 9; + + break; + case 'Scale': + $printDefaults->scale = (int) $printValue ?: 100; + + break; + } + } + } + } + + return $printDefaults; + } } diff --git a/src/PhpSpreadsheet/Worksheet/PageMargins.php b/src/PhpSpreadsheet/Worksheet/PageMargins.php index 9ebfb648..d59c6afd 100644 --- a/src/PhpSpreadsheet/Worksheet/PageMargins.php +++ b/src/PhpSpreadsheet/Worksheet/PageMargins.php @@ -211,4 +211,34 @@ class PageMargins } } } + + public static function fromCentimeters($value): float + { + return $value / 2.54; + } + + public static function toCentimeters($value): float + { + return $value * 2.54; + } + + public static function fromMillimeters($value): float + { + return $value / 25.4; + } + + public static function toMillimeters($value): float + { + return $value * 25.4; + } + + public static function fromPoints($value): float + { + return $value / 72; + } + + public static function toPoints($value): float + { + return $value * 72; + } } diff --git a/src/PhpSpreadsheet/Worksheet/PageSetup.php b/src/PhpSpreadsheet/Worksheet/PageSetup.php index 290b8349..d8d5098f 100644 --- a/src/PhpSpreadsheet/Worksheet/PageSetup.php +++ b/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -249,7 +249,7 @@ class PageSetup */ private $firstPageNumber; - private $pageOrder; + private $pageOrder = self::PAGEORDER_DOWN_THEN_OVER; /** * Create a new PageSetup. @@ -823,7 +823,7 @@ class PageSetup return $this->setFirstPageNumber(null); } - public function getPageOrder(): ?string + public function getPageOrder(): string { return $this->pageOrder; } @@ -831,7 +831,7 @@ class PageSetup public function setPageOrder(?string $pageOrder): self { if ($pageOrder === null || $pageOrder === self::PAGEORDER_DOWN_THEN_OVER || $pageOrder === self::PAGEORDER_OVER_THEN_DOWN) { - $this->pageOrder = $pageOrder; + $this->pageOrder = $pageOrder ?? self::PAGEORDER_DOWN_THEN_OVER; } return $this; diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php new file mode 100644 index 00000000..057dbf69 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php @@ -0,0 +1,147 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup() + { + $assertions = $this->pageSetupAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageSetup()->$testMethodName(); + $this->assertSame( + $expectedResult, + $actualResult, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + public function testPageMargins() + { + $assertions = $this->pageMarginAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageMargins()->$testMethodName(); + $this->assertEqualsWithDelta( + $expectedResult, + $actualResult, + self::MARGIN_PRECISION, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test} margin" + ); + } + } + } + + public function pageSetupAssertions() + { + return [ + 'Sheet1' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 75, + 'horizontalCentered' => true, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet2' => [ + 'orientation' => PageSetup::ORIENTATION_LANDSCAPE, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_OVER_THEN_DOWN, + ], + 'Sheet3' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 90, + 'horizontalCentered' => true, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet4' => [ + // Default Settings + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + ]; + } + + public function pageMarginAssertions() + { + return [ + 'Sheet1' => [ + // Here the values are in inches + 'top' => 0.315, + 'header' => 0.630, + 'left' => 0.512, + 'right' => 0.512, + 'bottom' => 0.315, + 'footer' => 0.433, + ], + 'Sheet2' => [ + // Here the values are in inches + 'top' => 0.315, + 'header' => 0.433, + 'left' => 0.709, + 'right' => 0.709, + 'bottom' => 0.315, + 'footer' => 0.433, + ], + 'Sheet3' => [ + // Here the values are in inches + 'top' => 0.512, + 'header' => 0.433, + 'left' => 0.709, + 'right' => 0.709, + 'bottom' => 0.512, + 'footer' => 0.433, + ], + 'Sheet4' => [ + // Default Settings (in inches) + 'top' => 0.3, + 'header' => 0.45, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.3, + 'footer' => 0.45, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/PageSetupTest.php new file mode 100644 index 00000000..74eaae8f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/PageSetupTest.php @@ -0,0 +1,149 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup() + { + $assertions = $this->pageSetupAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageSetup()->$testMethodName(); + $this->assertSame( + $expectedResult, + $actualResult, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + public function testPageMargins() + { + $assertions = $this->pageMarginAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageMargins()->$testMethodName(); + $this->assertEqualsWithDelta( + $expectedResult, + $actualResult, + self::MARGIN_PRECISION, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test} margin" + ); + } + } + } + + public function pageSetupAssertions() + { + return [ + 'Sheet1' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 75, + 'horizontalCentered' => true, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet2' => [ + 'orientation' => PageSetup::ORIENTATION_LANDSCAPE, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_OVER_THEN_DOWN, + ], + 'Sheet3' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 90, + 'horizontalCentered' => true, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet4' => [ + // Default Settings + 'orientation' => PageSetup::ORIENTATION_DEFAULT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + ]; + } + + public function pageMarginAssertions() + { + return [ + 'Sheet1' => [ + // Here the values are in cm + 'top' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'header' => 1.6 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 1.1 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet2' => [ + // Here the values are in cm + 'top' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'header' => 1.1 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 1.1 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet3' => [ + // Here the values are in cm + 'top' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'header' => 1.1 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 1.1 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet4' => [ + // Default Settings (already in inches) + 'top' => 0.3, + 'header' => 0.45, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.3, + 'footer' => 0.45, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/PageSetupTest.php new file mode 100644 index 00000000..2cfbbb3f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/PageSetupTest.php @@ -0,0 +1,149 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup() + { + $assertions = $this->pageSetupAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageSetup()->$testMethodName(); + $this->assertSame( + $expectedResult, + $actualResult, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + public function testPageMargins() + { + $assertions = $this->pageMarginAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageMargins()->$testMethodName(); + $this->assertEqualsWithDelta( + $expectedResult, + $actualResult, + self::MARGIN_PRECISION, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test} margin" + ); + } + } + } + + public function pageSetupAssertions() + { + return [ + 'Sheet1' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 75, + 'horizontalCentered' => true, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet2' => [ + 'orientation' => PageSetup::ORIENTATION_LANDSCAPE, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_OVER_THEN_DOWN, + ], + 'Sheet3' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 90, + 'horizontalCentered' => true, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet4' => [ + // Default Settings + 'orientation' => PageSetup::ORIENTATION_DEFAULT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + ]; + } + + public function pageMarginAssertions() + { + return [ + 'Sheet1' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'header' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 0.8 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet2' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'header' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 0.8 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet3' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'header' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 1.3 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet4' => [ + // Default Settings (already in inches for comparison) + 'top' => 0.75, + 'header' => 0.3, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.75, + 'footer' => 0.3, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetupTest.php new file mode 100644 index 00000000..a5b287fe --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetupTest.php @@ -0,0 +1,149 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup() + { + $assertions = $this->pageSetupAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageSetup()->$testMethodName(); + $this->assertSame( + $expectedResult, + $actualResult, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + public function testPageMargins() + { + $assertions = $this->pageMarginAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageMargins()->$testMethodName(); + $this->assertEqualsWithDelta( + $expectedResult, + $actualResult, + self::MARGIN_PRECISION, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test} margin" + ); + } + } + } + + public function pageSetupAssertions() + { + return [ + 'Sheet1' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 75, + 'horizontalCentered' => true, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet2' => [ + 'orientation' => PageSetup::ORIENTATION_LANDSCAPE, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_OVER_THEN_DOWN, + ], + 'Sheet3' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 90, + 'horizontalCentered' => true, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet4' => [ + // Default Settings + 'orientation' => PageSetup::ORIENTATION_DEFAULT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + ]; + } + + public function pageMarginAssertions() + { + return [ + 'Sheet1' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'header' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 0.8 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet2' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'header' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 0.8 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet3' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'header' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 1.3 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet4' => [ + // Default Settings (already in inches for comparison) + 'top' => 0.75, + 'header' => 0.3, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.75, + 'footer' => 0.3, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php new file mode 100644 index 00000000..8b934be6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php @@ -0,0 +1,148 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup() + { + $assertions = $this->pageSetupAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageSetup()->$testMethodName(); + $this->assertSame( + $expectedResult, + $actualResult, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + public function testPageMargins() + { + $assertions = $this->pageMarginAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getPageMargins()->$testMethodName(); + $this->assertEqualsWithDelta( + $expectedResult, + $actualResult, + self::MARGIN_PRECISION, + "Failed assertion for Worksheet '{$worksheet->getTitle()}' {$test} margin" + ); + } + } + } + + public function pageSetupAssertions() + { + return [ + 'Sheet1' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 75, + 'horizontalCentered' => true, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet2' => [ + 'orientation' => PageSetup::ORIENTATION_LANDSCAPE, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_OVER_THEN_DOWN, + ], + 'Sheet3' => [ + 'orientation' => PageSetup::ORIENTATION_PORTRAIT, + 'scale' => 90, + 'horizontalCentered' => true, + 'verticalCentered' => true, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + 'Sheet4' => [ + // Default Settings + 'orientation' => PageSetup::ORIENTATION_DEFAULT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'pageOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + ], + ]; + } + + public function pageMarginAssertions() + { + return [ + 'Sheet1' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'header' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 0.8 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet2' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'header' => 0.8 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 1.9 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 0.8 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet3' => [ + // Here the values are in cm, so we convert to inches for comparison with internal uom + 'top' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'header' => 1.3 / self::MARGIN_UNIT_CONVERSION, + 'left' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'right' => 1.8 / self::MARGIN_UNIT_CONVERSION, + 'bottom' => 2.4 / self::MARGIN_UNIT_CONVERSION, + 'footer' => 1.3 / self::MARGIN_UNIT_CONVERSION, + ], + 'Sheet4' => [ + // Default Settings (already in inches for comparison) + 'top' => 0.75, + 'header' => 0.3, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.75, + 'footer' => 0.3, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/PageMarginsTest.php b/tests/PhpSpreadsheetTests/Worksheet/PageMarginsTest.php new file mode 100644 index 00000000..b9713707 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/PageMarginsTest.php @@ -0,0 +1,89 @@ + Date: Sun, 5 Jul 2020 16:28:46 +0200 Subject: [PATCH 077/153] Forgot to check in the test files for the unit tests --- tests/data/Reader/Gnumeric/PageSetup.gnumeric | Bin 0 -> 2347 bytes tests/data/Reader/Ods/PageSetup.ods | Bin 0 -> 4814 bytes tests/data/Reader/XLS/PageSetup.xls | Bin 0 -> 34816 bytes tests/data/Reader/XLSX/PageSetup.xlsx | Bin 0 -> 15225 bytes tests/data/Reader/Xml/PageSetup.xml | 250 ++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 tests/data/Reader/Gnumeric/PageSetup.gnumeric create mode 100644 tests/data/Reader/Ods/PageSetup.ods create mode 100644 tests/data/Reader/XLS/PageSetup.xls create mode 100644 tests/data/Reader/XLSX/PageSetup.xlsx create mode 100644 tests/data/Reader/Xml/PageSetup.xml diff --git a/tests/data/Reader/Gnumeric/PageSetup.gnumeric b/tests/data/Reader/Gnumeric/PageSetup.gnumeric new file mode 100644 index 0000000000000000000000000000000000000000..53359154866fec0215c55f3144d34ea5444f4014 GIT binary patch literal 2347 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLS/PageSetup.xls b/tests/data/Reader/XLS/PageSetup.xls new file mode 100644 index 0000000000000000000000000000000000000000..559d05326f7887f1c31f07f70c82e30a8f858e62 GIT binary patch literal 34816 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/Xml/PageSetup.xml b/tests/data/Reader/Xml/PageSetup.xml new file mode 100644 index 00000000..8f996a93 --- /dev/null +++ b/tests/data/Reader/Xml/PageSetup.xml @@ -0,0 +1,250 @@ + + + + + Mark Baker + Mark Baker + 2020-07-04T11:51:41Z + 2020-06-29T17:37:00Z + 2020-07-04T11:52:32Z + 16.00 + + + + + + 13170 + 21600 + 2145 + 2145 + False + False + + + + + + + + 1 + 2 + 3 + + + 4 + 5 + 6 + + + 7 + 8 + 9 + + + 30 + 6 + +
+ + + +
+