diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 87d933e2..754a463b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,6 +124,37 @@ jobs: - name: Code style with PHP_CodeSniffer run: ./vendor/bin/phpcs -q --report=checkstyle | cs2pr + versions: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + extensions: ctype, dom, gd, iconv, fileinfo, libxml, mbstring, simplexml, xml, xmlreader, xmlwriter, zip, zlib + coverage: none + tools: cs2pr + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Code Version Compatibility check with PHP_CodeSniffer + run: ./vendor/bin/phpcs -q --report-width=200 --report=summary,full src/ --standard=PHPCompatibility --runtime-set testVersion 7.2- + phpstan: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcace6f..aac3a32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Implemented basic AutoFiltering for Ods Reader and Writer [PR #2053](https://github.com/PHPOffice/PhpSpreadsheet/pull/2053) +- Implemented basic AutoFiltering for Gnumeric Reader [PR #2055](https://github.com/PHPOffice/PhpSpreadsheet/pull/2055) - Improved support for Row and Column ranges in formulae [Issue #1755](https://github.com/PHPOffice/PhpSpreadsheet/issues/1755) [PR #2028](https://github.com/PHPOffice/PhpSpreadsheet/pull/2028) +- Implemented URLENCODE() Web Function - Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions. - Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908) - Support for notContainsText Conditional Style in xlsx [Issue #984](https://github.com/PHPOffice/PhpSpreadsheet/issues/984) @@ -27,7 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Nothing. ### Fixed - +- Correctly handle absolute A1 references when converting to R1C1 format [PR #2060](https://github.com/PHPOffice/PhpSpreadsheet/pull/2060) +- Correct default fill style for conditional without a pattern defined [Issue #2035](https://github.com/PHPOffice/PhpSpreadsheet/issues/2035) [PR #2050](https://github.com/PHPOffice/PhpSpreadsheet/pull/2050) - Fixed issue where array key check for existince before accessing arrays in Xlsx.php. [PR #1970](https://github.com/PHPOffice/PhpSpreadsheet/pull/1970) - Fixed issue with quoted strings in number format mask rendered with toFormattedString() [Issue 1972#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) [PR #1978](https://github.com/PHPOffice/PhpSpreadsheet/pull/1978) - Fixed issue with percentage formats in number format mask rendered with toFormattedString() [Issue 1929#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #1928](https://github.com/PHPOffice/PhpSpreadsheet/pull/1928) diff --git a/composer.json b/composer.json index 6cd65021..314f21cd 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,6 @@ }, "require": { "php": "^7.2 || ^8.0", - "ext-simplexml": "*", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -61,6 +60,7 @@ "ext-iconv": "*", "ext-libxml": "*", "ext-mbstring": "*", + "ext-simplexml": "*", "ext-xml": "*", "ext-xmlreader": "*", "ext-xmlwriter": "*", @@ -75,6 +75,7 @@ "psr/simple-cache": "^1.0" }, "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0", "friendsofphp/php-cs-fixer": "^2.18", "jpgraph/jpgraph": "^4.0", diff --git a/composer.lock b/composer.lock index 3670f857..4921cc8f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3be2673a6367d296c616bf9c34b77953", + "content-hash": "9158fcde13425499acaf0da201637737", "packages": [ { "name": "ezyang/htmlpurifier", @@ -802,6 +802,77 @@ ], "time": "2021-03-25T17:01:18+00:00" }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "c960cf4629fab7155caca18c038ca7257b7595e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/c960cf4629fab7155caca18c038ca7257b7595e3", + "reference": "c960cf4629fab7155caca18c038ca7257b7595e3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "enlightn/security-checker": "^1.2", + "phpcompatibility/php-compatibility": "^9.0" + }, + "default-branch": true, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2021-03-14T13:49:41+00:00" + }, { "name": "doctrine/annotations", "version": "1.12.1", @@ -5065,12 +5136,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "dealerdirect/phpcodesniffer-composer-installer": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^7.2 || ^8.0", - "ext-simplexml": "*", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -5078,6 +5150,7 @@ "ext-iconv": "*", "ext-libxml": "*", "ext-mbstring": "*", + "ext-simplexml": "*", "ext-xml": "*", "ext-xmlreader": "*", "ext-xmlwriter": "*", diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 716a3787..c836a99a 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -1220,13 +1220,13 @@ Merged Cells - - - - + ✔ ✔ - + ✔ + ✔ + N/A + N/A @@ -1313,13 +1313,13 @@ ● ● - - + ● + ● ● ● - + ● diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 89355400..037ee997 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -975,11 +975,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php - - - message: "#^Parameter \\#3 \\$rowNum of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Matrix\\:\\:extractRowValue\\(\\) expects int, float\\|int\\<0, max\\>\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Matrix\\:\\:extractRowValue\\(\\) has no return typehint specified\\.$#" count: 1 @@ -2545,71 +2540,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:\\$referenceHelper has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Parameter \\#1 \\$fp of function fread expects resource, resource\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Parameter \\#1 \\$fp of function fclose expects resource, resource\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:\\$mappings has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Offset 'No' does not exist on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Offset 'Unit' does not exist on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Offset 'DefaultSizePts' does not exist on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseBorderAttributes\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseBorderAttributes\\(\\) has parameter \\$borderAttributes with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseRichText\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseRichText\\(\\) has parameter \\$is with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseGnumericColour\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:parseGnumericColour\\(\\) has parameter \\$gnmColour with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Gnumeric.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Html\\:\\:\\$rowspan has no typehint specified\\.$#" count: 1 @@ -2810,21 +2740,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Ods.php - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 7 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:convertToExcelAddressValue\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#3 \\$formula of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:translateSeparator\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$officeNs has no typehint specified\\.$#" count: 1 diff --git a/samples/Basic/16_Csv.php b/samples/Basic/16_Csv.php index 15bbf0d4..137f6469 100644 --- a/samples/Basic/16_Csv.php +++ b/samples/Basic/16_Csv.php @@ -38,7 +38,8 @@ $helper->write($spreadsheetFromCSV, __FILE__, ['Xlsx']); $filenameCSV = $helper->getFilename(__FILE__, 'csv'); /** @var \PhpOffice\PhpSpreadsheet\Writer\Csv $writerCSV */ $writerCSV = new CsvWriter($spreadsheetFromCSV); -$writerCSV->setExcelCompatibility(true); +//$writerCSV->setExcelCompatibility(true); +$writerCSV->setUseBom(true); // because of non-ASCII chars $callStartTime = microtime(true); $writerCSV->save($filenameCSV); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php b/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php index d0324964..823d70c6 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php @@ -11,7 +11,7 @@ class Hyperlink * HYPERLINK. * * Excel Function: - * =HYPERLINK(linkURL,displayName) + * =HYPERLINK(linkURL, [displayName]) * * @param mixed $linkURL Expect string. Value to check, is also the value returned when no error * @param mixed $displayName Expect string. Value to return when testValue is an error condition diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php new file mode 100644 index 00000000..b0739eb3 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php @@ -0,0 +1,39 @@ +getMessage(); } if (!is_array($matrix) || ($rowNum > count($matrix))) { @@ -69,12 +75,12 @@ class Matrix return Functions::REF(); } - if ($columnNum == 0) { + if ($columnNum === 0) { return self::extractRowValue($matrix, $rowKeys, $rowNum); } $columnNum = $columnKeys[--$columnNum]; - if ($rowNum == 0) { + if ($rowNum === 0) { return array_map( function ($value) { return [$value]; @@ -89,7 +95,7 @@ class Matrix private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum) { - if ($rowNum == 0) { + if ($rowNum === 0) { return $matrix; } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php index ab3a9a07..8a3223b1 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php @@ -51,16 +51,20 @@ class Sum { $returnValue = 0; // Loop through the arguments - foreach (Functions::flattenArray($args) as $arg) { + $aArgs = Functions::flattenArrayIndexed($args); + foreach ($aArgs as $k => $arg) { // Is it a numeric value? if (is_numeric($arg) || empty($arg)) { if (is_string($arg)) { $arg = (int) $arg; } $returnValue += $arg; + } elseif (is_bool($arg)) { + $returnValue += (int) $arg; } elseif (Functions::isError($arg)) { return $arg; - } else { + // ignore non-numerics from cell, but fail as literals (except null) + } elseif ($arg !== null && !Functions::isCellValue($k)) { return Functions::VALUE(); } } diff --git a/src/PhpSpreadsheet/Calculation/locale/fr/functions b/src/PhpSpreadsheet/Calculation/locale/fr/functions index 7f40d5fd..b28b4a70 100644 --- a/src/PhpSpreadsheet/Calculation/locale/fr/functions +++ b/src/PhpSpreadsheet/Calculation/locale/fr/functions @@ -47,6 +47,7 @@ DVARP = BDVARP ## Calcule la variance pour l’ensemble d’une population d DATE = DATE ## Renvoie le numéro de série d’une date précise. DATEVALUE = DATEVAL ## Convertit une date représentée sous forme de texte en numéro de série. DAY = JOUR ## Convertit un numéro de série en jour du mois. +DAYS = JOURS ## Calcule le nombre de jours qui séparent deux dates. DAYS360 = JOURS360 ## Calcule le nombre de jours qui séparent deux dates sur la base d’une année de 360 jours. EDATE = MOIS.DECALER ## Renvoie le numéro séquentiel de la date qui représente une date spécifiée (l’argument date_départ), corrigée en plus ou en moins du nombre de mois indiqué. EOMONTH = FIN.MOIS ## Renvoie le numéro séquentiel de la date du dernier jour du mois précédant ou suivant la date_départ du nombre de mois indiqué. diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index b0e34e25..632c046f 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -102,14 +102,23 @@ class AddressHelper ?int $currentRowNumber = null, ?int $currentColumnNumber = null ): string { - $validityCheck = preg_match('/^\$?([A-Z]{1,3})\$?(\d{1,7})$/i', $address, $cellReference); + $validityCheck = preg_match(Coordinate::A1_COORDINATE_REGEX, $address, $cellReference); if ($validityCheck === 0) { throw new Exception('Invalid A1-format Cell Reference'); } - $columnId = Coordinate::columnIndexFromString($cellReference[1]); - $rowId = (int) $cellReference[2]; + $columnId = Coordinate::columnIndexFromString($cellReference['col_ref']); + if ($cellReference['absolute_col'] === '$') { + // Column must be absolute address + $currentColumnNumber = null; + } + + $rowId = (int) $cellReference['row_ref']; + if ($cellReference['absolute_row'] === '$') { + // Row must be absolute address + $currentRowNumber = null; + } if ($currentRowNumber !== null) { if ($rowId === $currentRowNumber) { diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 0b3917f2..58d2573e 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -13,6 +13,8 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; */ abstract class Coordinate { + public const A1_COORDINATE_REGEX = '/^(?\$?)(?[A-Z]{1,3})(?\$?)(?\d{1,7})$/i'; + /** * Default range variable constant. * @@ -29,8 +31,8 @@ abstract class Coordinate */ public static function coordinateFromString($pCoordinateString) { - if (preg_match('/^([$]?[A-Z]{1,3})([$]?\\d{1,7})$/', $pCoordinateString, $matches)) { - return [$matches[1], $matches[2]]; + if (preg_match(self::A1_COORDINATE_REGEX, $pCoordinateString, $matches)) { + return [$matches['absolute_col'] . $matches['col_ref'], $matches['absolute_row'] . $matches['row_ref']]; } elseif (self::coordinateIsRange($pCoordinateString)) { throw new Exception('Cell coordinate string can not be a range of cells'); } elseif ($pCoordinateString == '') { diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index d3cdf1b0..85bae6f8 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -7,25 +7,32 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\PageSetup; use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Properties; +use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Styles; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Settings; -use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PhpOffice\PhpSpreadsheet\Style\Alignment; -use PhpOffice\PhpSpreadsheet\Style\Border; -use PhpOffice\PhpSpreadsheet\Style\Borders; -use PhpOffice\PhpSpreadsheet\Style\Fill; -use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; use XMLReader; class Gnumeric extends BaseReader { - private const UOM_CONVERSION_POINTS_TO_CENTIMETERS = 0.03527777778; + const NAMESPACE_GNM = 'http://www.gnumeric.org/v10.dtd'; // gmr in old sheets + + const NAMESPACE_XSI = 'http://www.w3.org/2001/XMLSchema-instance'; + + const NAMESPACE_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'; + + const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink'; + + const NAMESPACE_DC = 'http://purl.org/dc/elements/1.1/'; + + const NAMESPACE_META = 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'; + + const NAMESPACE_OOO = 'http://openoffice.org/2004/office'; /** * Shared Expressions. @@ -41,15 +48,22 @@ class Gnumeric extends BaseReader */ private $spreadsheet; + /** @var ReferenceHelper */ private $referenceHelper; - /** - * Namespace shared across all functions. - * It is 'gnm', except for really old sheets which use 'gmr'. - * - * @var string - */ - private $gnm = 'gnm'; + /** @var array */ + public static $mappings = [ + 'dataType' => [ + '10' => DataType::TYPE_NULL, + '20' => DataType::TYPE_BOOL, + '30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel + '40' => DataType::TYPE_NUMERIC, // Float + '50' => DataType::TYPE_ERROR, + '60' => DataType::TYPE_STRING, + //'70': // Cell Range + //'80': // Array + ], + ]; /** * Create a new Gnumeric. @@ -77,16 +91,20 @@ class Gnumeric extends BaseReader if (function_exists('gzread')) { // Read signature data (first 3 bytes) $fh = fopen($pFilename, 'rb'); - $data = fread($fh, 2); - fclose($fh); + if ($fh !== false) { + $data = fread($fh, 2); + fclose($fh); + } } - return $data == chr(0x1F) . chr(0x8B); + return $data === chr(0x1F) . chr(0x8B); } - private static function matchXml(string $name, string $field): bool + private static function matchXml(XMLReader $xml, string $expectedLocalName): bool { - return 1 === preg_match("/^(gnm|gmr):$field$/", $name); + return $xml->namespaceURI === self::NAMESPACE_GNM + && $xml->localName === $expectedLocalName + && $xml->nodeType === XMLReader::ELEMENT; } /** @@ -106,10 +124,10 @@ class Gnumeric extends BaseReader $worksheetNames = []; while ($xml->read()) { - if (self::matchXml($xml->name, 'SheetName') && $xml->nodeType == XMLReader::ELEMENT) { + if (self::matchXml($xml, 'SheetName')) { $xml->read(); // Move onto the value node $worksheetNames[] = (string) $xml->value; - } elseif (self::matchXml($xml->name, 'Sheets')) { + } elseif (self::matchXml($xml, 'Sheets')) { // break out of the loop once we've got our sheet names rather than parse the entire file break; } @@ -135,7 +153,7 @@ class Gnumeric extends BaseReader $worksheetInfo = []; while ($xml->read()) { - if (self::matchXml($xml->name, 'Sheet') && $xml->nodeType == XMLReader::ELEMENT) { + if (self::matchXml($xml, 'Sheet')) { $tmpInfo = [ 'worksheetName' => '', 'lastColumnLetter' => 'A', @@ -145,20 +163,18 @@ class Gnumeric extends BaseReader ]; while ($xml->read()) { - if ($xml->nodeType == XMLReader::ELEMENT) { - if (self::matchXml($xml->name, 'Name')) { - $xml->read(); // Move onto the value node - $tmpInfo['worksheetName'] = (string) $xml->value; - } elseif (self::matchXml($xml->name, 'MaxCol')) { - $xml->read(); // Move onto the value node - $tmpInfo['lastColumnIndex'] = (int) $xml->value; - $tmpInfo['totalColumns'] = (int) $xml->value + 1; - } elseif (self::matchXml($xml->name, 'MaxRow')) { - $xml->read(); // Move onto the value node - $tmpInfo['totalRows'] = (int) $xml->value + 1; + if (self::matchXml($xml, 'Name')) { + $xml->read(); // Move onto the value node + $tmpInfo['worksheetName'] = (string) $xml->value; + } elseif (self::matchXml($xml, 'MaxCol')) { + $xml->read(); // Move onto the value node + $tmpInfo['lastColumnIndex'] = (int) $xml->value; + $tmpInfo['totalColumns'] = (int) $xml->value + 1; + } elseif (self::matchXml($xml, 'MaxRow')) { + $xml->read(); // Move onto the value node + $tmpInfo['totalRows'] = (int) $xml->value + 1; - break; - } + break; } } $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); @@ -188,94 +204,34 @@ 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; + return array_merge(self::$mappings, Styles::$mappings); } private function processComments(SimpleXMLElement $sheet): void { if ((!$this->readDataOnly) && (isset($sheet->Objects))) { - foreach ($sheet->Objects->children($this->gnm, true) as $key => $comment) { + foreach ($sheet->Objects->children(self::NAMESPACE_GNM) as $key => $comment) { $commentAttributes = $comment->attributes(); // Only comment objects are handled at the moment if ($commentAttributes->Text) { - $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound)->setAuthor((string) $commentAttributes->Author)->setText($this->parseRichText((string) $commentAttributes->Text)); + $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound) + ->setAuthor((string) $commentAttributes->Author) + ->setText($this->parseRichText((string) $commentAttributes->Text)); } } } } + /** + * @param mixed $value + */ + private static function testSimpleXml($value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement(''); + } + /** * Loads Spreadsheet from file. * @@ -304,12 +260,10 @@ class Gnumeric extends BaseReader $gFileData = $this->gzfileGetContents($pFilename); $xml2 = simplexml_load_string($this->securityScanner->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); - $xml = ($xml2 !== false) ? $xml2 : new SimpleXMLElement(''); - $namespacesMeta = $xml->getNamespaces(true); - $this->gnm = array_key_exists('gmr', $namespacesMeta) ? 'gmr' : 'gnm'; + $xml = self::testSimpleXml($xml2); - $gnmXML = $xml->children($namespacesMeta[$this->gnm]); - (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML, $namespacesMeta); + $gnmXML = $xml->children(self::NAMESPACE_GNM); + (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML); $worksheetID = 0; foreach ($gnmXML->Sheets->Sheet as $sheet) { @@ -329,7 +283,7 @@ class Gnumeric extends BaseReader $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); if (!$this->readDataOnly) { - (new PageSetup($this->spreadsheet, $this->gnm)) + (new PageSetup($this->spreadsheet)) ->printInformation($sheet) ->sheetMargins($sheet); } @@ -382,90 +336,22 @@ class Gnumeric extends BaseReader if (array_key_exists($vtype, self::$mappings['dataType'])) { $type = self::$mappings['dataType'][$vtype]; } - if ($vtype == '20') { // Boolean + if ($vtype === '20') { // Boolean $cell = $cell == 'TRUE'; } } $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); } - $this->processComments($sheet); - - foreach ($sheet->Styles->StyleRegion as $styleRegion) { - $styleAttributes = $styleRegion->attributes(); - if ( - ($styleAttributes['startRow'] <= $maxRow) && - ($styleAttributes['startCol'] <= $maxCol) - ) { - $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); - $startRow = $styleAttributes['startRow'] + 1; - - $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; - $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); - - $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); - $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; - - $styleAttributes = $styleRegion->Style->attributes(); - - $styleArray = []; - // We still set the number format mask for date/time values, even if readDataOnly is true - $formatCode = (string) $styleAttributes['Format']; - if (Date::isDateTimeFormatCode($formatCode)) { - $styleArray['numberFormat']['formatCode'] = $formatCode; - } - if (!$this->readDataOnly) { - // If readDataOnly is false, we set all formatting information - $styleArray['numberFormat']['formatCode'] = $formatCode; - - self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); - self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); - $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; - $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); - $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; - $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; - - $this->addColors($styleArray, $styleAttributes); - - $fontAttributes = $styleRegion->Style->Font->attributes(); - $styleArray['font']['name'] = (string) $styleRegion->Style->Font; - $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); - $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; - $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; - $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; - self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); - - switch ($fontAttributes['Script']) { - case '1': - $styleArray['font']['superscript'] = true; - - break; - case '-1': - $styleArray['font']['subscript'] = true; - - break; - } - - if (isset($styleRegion->Style->StyleBorder)) { - $srssb = $styleRegion->Style->StyleBorder; - $this->addBorderStyle($srssb, $styleArray, 'top'); - $this->addBorderStyle($srssb, $styleArray, 'bottom'); - $this->addBorderStyle($srssb, $styleArray, 'left'); - $this->addBorderStyle($srssb, $styleArray, 'right'); - $this->addBorderDiagonal($srssb, $styleArray); - } - if (isset($styleRegion->Style->HyperLink)) { - // TO DO - $hyperlink = $styleRegion->Style->HyperLink->attributes(); - } - } - $this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); - } + if ($sheet->Styles !== null) { + (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol); } + $this->processComments($sheet); $this->processColumnWidths($sheet, $maxCol); $this->processRowHeights($sheet, $maxRow); $this->processMergedCells($sheet); + $this->processAutofilter($sheet); ++$worksheetID; } @@ -476,32 +362,10 @@ class Gnumeric extends BaseReader return $this->spreadsheet; } - private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void - { - if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; - } elseif (isset($srssb->Diagonal)) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; - } elseif (isset($srssb->{'Rev-Diagonal'})) { - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); - $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; - } - } - - private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void - { - $ucDirection = ucfirst($direction); - if (isset($srssb->$ucDirection)) { - $styleArray['borders'][$direction] = self::parseBorderAttributes($srssb->$ucDirection->attributes()); - } - } - - private function processMergedCells(SimpleXMLElement $sheet): void + private function processMergedCells(?SimpleXMLElement $sheet): void { // Handle Merged Cells in this worksheet - if (isset($sheet->MergedRegions)) { + if ($sheet !== null && isset($sheet->MergedRegions)) { foreach ($sheet->MergedRegions->Merge as $mergeCells) { if (strpos($mergeCells, ':') !== false) { $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); @@ -510,92 +374,144 @@ class Gnumeric extends BaseReader } } - private function processColumnLoop(int $c, int $maxCol, SimpleXMLElement $columnOverride, float $defaultWidth): int + private function processAutofilter(?SimpleXMLElement $sheet): void { - $columnAttributes = $columnOverride->attributes(); + if ($sheet !== null && isset($sheet->Filters)) { + foreach ($sheet->Filters->Filter as $autofilter) { + if ($autofilter !== null) { + $attributes = $autofilter->attributes(); + if (isset($attributes['Area'])) { + $this->spreadsheet->getActiveSheet()->setAutoFilter((string) $attributes['Area']); + } + } + } + } + } + + private function setColumnWidth(int $whichColumn, float $defaultWidth): void + { + $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + if ($columnDimension !== null) { + $columnDimension->setWidth($defaultWidth); + } + } + + private function setColumnInvisible(int $whichColumn): void + { + $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + if ($columnDimension !== null) { + $columnDimension->setVisible(false); + } + } + + private function processColumnLoop(int $whichColumn, int $maxCol, SimpleXMLElement $columnOverride, float $defaultWidth): int + { + $columnAttributes = self::testSimpleXml($columnOverride->attributes()); $column = $columnAttributes['No']; $columnWidth = ((float) $columnAttributes['Unit']) / 5.4; $hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1'); $columnCount = (int) ($columnAttributes['Count'] ?? 1); - while ($c < $column) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); - ++$c; + while ($whichColumn < $column) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; } - while (($c < ($column + $columnCount)) && ($c <= $maxCol)) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($columnWidth); + while (($whichColumn < ($column + $columnCount)) && ($whichColumn <= $maxCol)) { + $this->setColumnWidth($whichColumn, $columnWidth); if ($hidden) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setVisible(false); + $this->setColumnInvisible($whichColumn); } - ++$c; + ++$whichColumn; } - return $c; + return $whichColumn; } - private function processColumnWidths(SimpleXMLElement $sheet, int $maxCol): void + private function processColumnWidths(?SimpleXMLElement $sheet, int $maxCol): void { - if ((!$this->readDataOnly) && (isset($sheet->Cols))) { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Cols))) { // Column Widths + $defaultWidth = 0; $columnAttributes = $sheet->Cols->attributes(); - $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; - $c = 0; - foreach ($sheet->Cols->ColInfo as $columnOverride) { - $c = $this->processColumnLoop($c, $maxCol, $columnOverride, $defaultWidth); + if ($columnAttributes !== null) { + $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; } - while ($c <= $maxCol) { - $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); - ++$c; + $whichColumn = 0; + foreach ($sheet->Cols->ColInfo as $columnOverride) { + $whichColumn = $this->processColumnLoop($whichColumn, $maxCol, $columnOverride, $defaultWidth); + } + while ($whichColumn <= $maxCol) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; } } } - private function processRowLoop(int $r, int $maxRow, SimpleXMLElement $rowOverride, float $defaultHeight): int + private function setRowHeight(int $whichRow, float $defaultHeight): void { - $rowAttributes = $rowOverride->attributes(); + $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow); + if ($rowDimension !== null) { + $rowDimension->setRowHeight($defaultHeight); + } + } + + private function setRowInvisible(int $whichRow): void + { + $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow); + if ($rowDimension !== null) { + $rowDimension->setVisible(false); + } + } + + private function processRowLoop(int $whichRow, int $maxRow, SimpleXMLElement $rowOverride, float $defaultHeight): int + { + $rowAttributes = self::testSimpleXml($rowOverride->attributes()); $row = $rowAttributes['No']; $rowHeight = (float) $rowAttributes['Unit']; $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1'); $rowCount = (int) ($rowAttributes['Count'] ?? 1); - while ($r < $row) { - ++$r; - $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); + while ($whichRow < $row) { + ++$whichRow; + $this->setRowHeight($whichRow, $defaultHeight); } - while (($r < ($row + $rowCount)) && ($r < $maxRow)) { - ++$r; - $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($rowHeight); + while (($whichRow < ($row + $rowCount)) && ($whichRow < $maxRow)) { + ++$whichRow; + $this->setRowHeight($whichRow, $rowHeight); if ($hidden) { - $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setVisible(false); + $this->setRowInvisible($whichRow); } } - return $r; + return $whichRow; } - private function processRowHeights(SimpleXMLElement $sheet, int $maxRow): void + private function processRowHeights(?SimpleXMLElement $sheet, int $maxRow): void { - if ((!$this->readDataOnly) && (isset($sheet->Rows))) { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Rows))) { // Row Heights + $defaultHeight = 0; $rowAttributes = $sheet->Rows->attributes(); - $defaultHeight = (float) $rowAttributes['DefaultSizePts']; - $r = 0; + if ($rowAttributes !== null) { + $defaultHeight = (float) $rowAttributes['DefaultSizePts']; + } + $whichRow = 0; foreach ($sheet->Rows->RowInfo as $rowOverride) { - $r = $this->processRowLoop($r, $maxRow, $rowOverride, $defaultHeight); + $whichRow = $this->processRowLoop($whichRow, $maxRow, $rowOverride, $defaultHeight); } // never executed, I can't figure out any circumstances // under which it would be executed, and, even if // such exist, I'm not convinced this is needed. - //while ($r < $maxRow) { - // ++$r; - // $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); + //while ($whichRow < $maxRow) { + // ++$whichRow; + // $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow)->setRowHeight($defaultHeight); //} } } - private function processDefinedNames(SimpleXMLElement $gnmXML): void + private function processDefinedNames(?SimpleXMLElement $gnmXML): void { // Loop through definedNames (global named ranges) - if (isset($gnmXML->Names)) { + if ($gnmXML !== null && isset($gnmXML->Names)) { foreach ($gnmXML->Names->Name as $definedName) { $name = (string) $definedName->name; $value = (string) $definedName->value; @@ -614,77 +530,11 @@ class Gnumeric extends BaseReader } } - private function calcRotation(SimpleXMLElement $styleAttributes): int - { - $rotation = (int) $styleAttributes->Rotation; - if ($rotation >= 270 && $rotation <= 360) { - $rotation -= 360; - } - $rotation = (abs($rotation) > 90) ? 0 : $rotation; - - return $rotation; - } - - private static function addStyle(array &$styleArray, string $key, string $value): void - { - if (array_key_exists($value, self::$mappings[$key])) { - $styleArray[$key] = self::$mappings[$key][$value]; - } - } - - private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void - { - if (array_key_exists($value, self::$mappings[$key])) { - $styleArray[$key1][$key] = self::$mappings[$key][$value]; - } - } - - private static function parseBorderAttributes($borderAttributes) - { - $styleArray = []; - if (isset($borderAttributes['Color'])) { - $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); - } - - self::addStyle($styleArray, 'borderStyle', $borderAttributes['Style']); - - return $styleArray; - } - - private function parseRichText($is) + private function parseRichText(string $is): RichText { $value = new RichText(); $value->createText($is); return $value; } - - private static function parseGnumericColour($gnmColour) - { - [$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour); - $gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2); - $gnmG = substr(str_pad($gnmG, 4, '0', STR_PAD_RIGHT), 0, 2); - $gnmB = substr(str_pad($gnmB, 4, '0', STR_PAD_RIGHT), 0, 2); - - return $gnmR . $gnmG . $gnmB; - } - - private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void - { - $RGB = self::parseGnumericColour($styleAttributes['Fore']); - $styleArray['font']['color']['rgb'] = $RGB; - $RGB = self::parseGnumericColour($styleAttributes['Back']); - $shade = (string) $styleAttributes['Shade']; - if (($RGB != '000000') || ($shade != '0')) { - $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); - if ($shade == '1') { - $styleArray['fill']['startColor']['rgb'] = $RGB; - $styleArray['fill']['endColor']['rgb'] = $RGB2; - } else { - $styleArray['fill']['endColor']['rgb'] = $RGB; - $styleArray['fill']['startColor']['rgb'] = $RGB2; - } - self::addStyle2($styleArray, 'fill', 'fillType', $shade); - } - } } diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php b/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php index 0fe73005..accc2716 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric; +use PhpOffice\PhpSpreadsheet\Reader\Gnumeric; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\PageMargins; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup as WorksheetPageSetup; @@ -14,15 +15,9 @@ class PageSetup */ private $spreadsheet; - /** - * @var string - */ - private $gnm; - - public function __construct(Spreadsheet $spreadsheet, string $gnm) + public function __construct(Spreadsheet $spreadsheet) { $this->spreadsheet = $spreadsheet; - $this->gnm = $gnm; } public function printInformation(SimpleXMLElement $sheet): self @@ -68,7 +63,7 @@ class PageSetup private function buildMarginSet(SimpleXMLElement $sheet, array $marginSet): array { - foreach ($sheet->PrintInformation->Margins->children($this->gnm, true) as $key => $margin) { + foreach ($sheet->PrintInformation->Margins->children(Gnumeric::NAMESPACE_GNM) as $key => $margin) { $marginAttributes = $margin->attributes(); $marginSize = ($marginAttributes['Points']) ?? 72; // Default is 72pt // Convert value in points to inches diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php b/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php index 16d9c2e0..c466a859 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric; +use PhpOffice\PhpSpreadsheet\Reader\Gnumeric; use PhpOffice\PhpSpreadsheet\Spreadsheet; use SimpleXMLElement; @@ -91,74 +92,72 @@ class Properties } } - private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta, array $namespacesMeta): void + private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta): void { $docProps = $this->spreadsheet->getProperties(); foreach ($officePropertyMeta as $propertyName => $propertyValue) { - if ($propertyValue === null) { - continue; - } + if ($propertyValue !== null) { + $attributes = $propertyValue->attributes(Gnumeric::NAMESPACE_META); + $propertyValue = trim((string) $propertyValue); + switch ($propertyName) { + case 'keyword': + $docProps->setKeywords($propertyValue); - $attributes = $propertyValue->attributes($namespacesMeta['meta']); - $propertyValue = trim((string) $propertyValue); - switch ($propertyName) { - case 'keyword': - $docProps->setKeywords($propertyValue); + break; + case 'initial-creator': + $docProps->setCreator($propertyValue); + $docProps->setLastModifiedBy($propertyValue); - break; - case 'initial-creator': - $docProps->setCreator($propertyValue); - $docProps->setLastModifiedBy($propertyValue); + break; + case 'creation-date': + $creationDate = strtotime($propertyValue); + $creationDate = $creationDate === false ? time() : $creationDate; + $docProps->setCreated($creationDate); + $docProps->setModified($creationDate); - break; - case 'creation-date': - $creationDate = strtotime($propertyValue); - $creationDate = $creationDate === false ? time() : $creationDate; - $docProps->setCreated($creationDate); - $docProps->setModified($creationDate); + break; + case 'user-defined': + [, $attrName] = explode(':', $attributes['name']); + $this->userDefinedProperties($attrName, $propertyValue); - break; - case 'user-defined': - [, $attrName] = explode(':', $attributes['name']); - switch ($attrName) { - case 'publisher': - $docProps->setCompany($propertyValue); - - break; - case 'category': - $docProps->setCategory($propertyValue); - - break; - case 'manager': - $docProps->setManager($propertyValue); - - break; - } - - break; + break; + } } } } - public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML, array $namespacesMeta): void + private function userDefinedProperties(string $attrName, string $propertyValue): void { - if (isset($namespacesMeta['office'])) { - $officeXML = $xml->children($namespacesMeta['office']); + $docProps = $this->spreadsheet->getProperties(); + switch ($attrName) { + case 'publisher': + $docProps->setCompany($propertyValue); + + break; + case 'category': + $docProps->setCategory($propertyValue); + + break; + case 'manager': + $docProps->setManager($propertyValue); + + break; + } + } + + public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML): void + { + $officeXML = $xml->children(Gnumeric::NAMESPACE_OFFICE); + if (!empty($officeXML)) { $officeDocXML = $officeXML->{'document-meta'}; $officeDocMetaXML = $officeDocXML->meta; foreach ($officeDocMetaXML as $officePropertyData) { - $officePropertyDC = []; - if (isset($namespacesMeta['dc'])) { - $officePropertyDC = $officePropertyData->children($namespacesMeta['dc']); - } + $officePropertyDC = $officePropertyData->children(Gnumeric::NAMESPACE_DC); $this->docPropertiesDC($officePropertyDC); - $officePropertyMeta = []; - if (isset($namespacesMeta['meta'])) { - $officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']); - } - $this->docPropertiesMeta($officePropertyMeta, $namespacesMeta); + $officePropertyMeta = $officePropertyData->children(Gnumeric::NAMESPACE_META); + $this->docPropertiesMeta($officePropertyMeta); } } elseif (isset($gnmXML->Summary)) { $this->docPropertiesOld($gnmXML); diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php new file mode 100644 index 00000000..9998448e --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -0,0 +1,282 @@ + [ + '0' => Border::BORDER_NONE, + '1' => Border::BORDER_THIN, + '2' => Border::BORDER_MEDIUM, + '3' => Border::BORDER_SLANTDASHDOT, + '4' => Border::BORDER_DASHED, + '5' => Border::BORDER_THICK, + '6' => Border::BORDER_DOUBLE, + '7' => Border::BORDER_DOTTED, + '8' => Border::BORDER_MEDIUMDASHED, + '9' => Border::BORDER_DASHDOT, + '10' => Border::BORDER_MEDIUMDASHDOT, + '11' => Border::BORDER_DASHDOTDOT, + '12' => Border::BORDER_MEDIUMDASHDOTDOT, + '13' => Border::BORDER_MEDIUMDASHDOTDOT, + ], + 'fillType' => [ + '1' => Fill::FILL_SOLID, + '2' => Fill::FILL_PATTERN_DARKGRAY, + '3' => Fill::FILL_PATTERN_MEDIUMGRAY, + '4' => Fill::FILL_PATTERN_LIGHTGRAY, + '5' => Fill::FILL_PATTERN_GRAY125, + '6' => Fill::FILL_PATTERN_GRAY0625, + '7' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe + '8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe + '9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe + '10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe + '11' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch + '12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch + '13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL, + '14' => Fill::FILL_PATTERN_LIGHTVERTICAL, + '15' => Fill::FILL_PATTERN_LIGHTUP, + '16' => Fill::FILL_PATTERN_LIGHTDOWN, + '17' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch + '18' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch + ], + 'horizontal' => [ + '1' => Alignment::HORIZONTAL_GENERAL, + '2' => Alignment::HORIZONTAL_LEFT, + '4' => Alignment::HORIZONTAL_RIGHT, + '8' => Alignment::HORIZONTAL_CENTER, + '16' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + '32' => Alignment::HORIZONTAL_JUSTIFY, + '64' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + ], + 'underline' => [ + '1' => Font::UNDERLINE_SINGLE, + '2' => Font::UNDERLINE_DOUBLE, + '3' => Font::UNDERLINE_SINGLEACCOUNTING, + '4' => Font::UNDERLINE_DOUBLEACCOUNTING, + ], + 'vertical' => [ + '1' => Alignment::VERTICAL_TOP, + '2' => Alignment::VERTICAL_BOTTOM, + '4' => Alignment::VERTICAL_CENTER, + '8' => Alignment::VERTICAL_JUSTIFY, + ], + ]; + + public function __construct(Spreadsheet $spreadsheet, bool $readDataOnly) + { + $this->spreadsheet = $spreadsheet; + $this->readDataOnly = $readDataOnly; + } + + public function read(SimpleXMLElement $sheet, int $maxRow, int $maxCol): void + { + if ($sheet->Styles->StyleRegion !== null) { + $this->readStyles($sheet->Styles->StyleRegion, $maxRow, $maxCol); + } + } + + private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $maxCol): void + { + foreach ($styleRegion as $style) { + if ($style === null) { + continue; + } + + $styleAttributes = $style->attributes(); + if ($styleAttributes !== null && ($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { + $cellRange = $this->readStyleRange($styleAttributes, $maxCol, $maxRow); + + $styleAttributes = $style->Style->attributes(); + + $styleArray = []; + // We still set the number format mask for date/time values, even if readDataOnly is true + // so that we can identify whether a float is a float or a date value + $formatCode = (string) $styleAttributes['Format']; + if (Date::isDateTimeFormatCode($formatCode)) { + $styleArray['numberFormat']['formatCode'] = $formatCode; + } + if ($this->readDataOnly === false && $styleAttributes !== null) { + // If readDataOnly is false, we set all formatting information + $styleArray['numberFormat']['formatCode'] = $formatCode; + $styleArray = $this->readStyle($styleArray, $styleAttributes, $style); + } + $this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); + } + } + } + + private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void + { + if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; + } elseif (isset($srssb->Diagonal)) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; + } elseif (isset($srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; + } + } + + private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void + { + $ucDirection = ucfirst($direction); + if (isset($srssb->$ucDirection)) { + $styleArray['borders'][$direction] = self::parseBorderAttributes($srssb->$ucDirection->attributes()); + } + } + + private function calcRotation(SimpleXMLElement $styleAttributes): int + { + $rotation = (int) $styleAttributes->Rotation; + if ($rotation >= 270 && $rotation <= 360) { + $rotation -= 360; + } + $rotation = (abs($rotation) > 90) ? 0 : $rotation; + + return $rotation; + } + + private static function addStyle(array &$styleArray, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key] = self::$mappings[$key][$value]; + } + } + + private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key1][$key] = self::$mappings[$key][$value]; + } + } + + private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array + { + $styleArray = []; + if ($borderAttributes !== null) { + if (isset($borderAttributes['Color'])) { + $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); + } + + self::addStyle($styleArray, 'borderStyle', $borderAttributes['Style']); + } + + return $styleArray; + } + + private static function parseGnumericColour(string $gnmColour): string + { + [$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour); + $gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2); + $gnmG = substr(str_pad($gnmG, 4, '0', STR_PAD_RIGHT), 0, 2); + $gnmB = substr(str_pad($gnmB, 4, '0', STR_PAD_RIGHT), 0, 2); + + return $gnmR . $gnmG . $gnmB; + } + + private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void + { + $RGB = self::parseGnumericColour($styleAttributes['Fore']); + $styleArray['font']['color']['rgb'] = $RGB; + $RGB = self::parseGnumericColour($styleAttributes['Back']); + $shade = (string) $styleAttributes['Shade']; + if (($RGB !== '000000') || ($shade !== '0')) { + $RGB2 = self::parseGnumericColour($styleAttributes['PatternColor']); + if ($shade === '1') { + $styleArray['fill']['startColor']['rgb'] = $RGB; + $styleArray['fill']['endColor']['rgb'] = $RGB2; + } else { + $styleArray['fill']['endColor']['rgb'] = $RGB; + $styleArray['fill']['startColor']['rgb'] = $RGB2; + } + self::addStyle2($styleArray, 'fill', 'fillType', $shade); + } + } + + private function readStyleRange(SimpleXMLElement $styleAttributes, int $maxCol, int $maxRow): string + { + $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); + $startRow = $styleAttributes['startRow'] + 1; + + $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; + $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); + + $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); + $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; + + return $cellRange; + } + + private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array + { + self::addStyle2($styleArray, 'alignment', 'horizontal', $styleAttributes['HAlign']); + self::addStyle2($styleArray, 'alignment', 'vertical', $styleAttributes['VAlign']); + $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; + $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); + $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; + $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; + + $this->addColors($styleArray, $styleAttributes); + + $fontAttributes = $style->Style->Font->attributes(); + if ($fontAttributes !== null) { + $styleArray['font']['name'] = (string) $style->Style->Font; + $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); + $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; + $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; + $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; + self::addStyle2($styleArray, 'font', 'underline', $fontAttributes['Underline']); + + switch ($fontAttributes['Script']) { + case '1': + $styleArray['font']['superscript'] = true; + + break; + case '-1': + $styleArray['font']['subscript'] = true; + + break; + } + } + + if (isset($style->Style->StyleBorder)) { + $srssb = $style->Style->StyleBorder; + $this->addBorderStyle($srssb, $styleArray, 'top'); + $this->addBorderStyle($srssb, $styleArray, 'bottom'); + $this->addBorderStyle($srssb, $styleArray, 'left'); + $this->addBorderStyle($srssb, $styleArray, 'right'); + $this->addBorderDiagonal($srssb, $styleArray); + } + if (isset($style->Style->HyperLink)) { + // TO DO + $hyperlink = $style->Style->HyperLink->attributes(); + } + + return $styleArray; + } +} diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 1a4d7ca3..26151cdc 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -11,8 +11,9 @@ use DOMNode; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; -use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; +use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter; +use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames; use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings; use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; @@ -22,7 +23,6 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; use XMLReader; use ZipArchive; @@ -304,8 +304,10 @@ class Ods extends BaseReader $pageSettings->readStyleCrossReferences($dom); - // Content + $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); + $definedNameReader = new DefinedNames($spreadsheet, $tableNs); + // Content $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body') ->item(0) ->getElementsByTagNameNS($officeNs, 'spreadsheet'); @@ -642,8 +644,8 @@ class Ods extends BaseReader ++$worksheetID; } - $this->readDefinedRanges($spreadsheet, $workbookData, $tableNs); - $this->readDefinedExpressions($spreadsheet, $workbookData, $tableNs); + $autoFilterReader->read($workbookData); + $definedNameReader->read($workbookData); } $spreadsheet->setActiveSheetIndex(0); @@ -771,26 +773,6 @@ class Ods extends BaseReader return $value; } - private function convertToExcelAddressValue(string $openOfficeAddress): string - { - $excelAddress = $openOfficeAddress; - - // Cell range 3-d reference - // As we don't support 3-d ranges, we're just going to take a quick and dirty approach - // and assume that the second worksheet reference is the same as the first - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); - // Cell range reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress); - // Cell reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress); - // Cell range reference - $excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress); - // Simple cell reference - $excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress); - - return $excelAddress; - } - private function convertToExcelFormulaValue(string $openOfficeFormula): string { $temp = explode('"', $openOfficeFormula); @@ -801,11 +783,13 @@ class Ods extends BaseReader // Cell range reference in another sheet $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); // Cell reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value); + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); // Cell range reference - $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value); + $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); // Simple cell reference - $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value); + $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + // Convert references to defined names/formulae + $value = str_replace('$$', '', $value ?? ''); $value = Calculation::translateSeparator(';', ',', $value, $inBraces); } @@ -816,53 +800,4 @@ class Ods extends BaseReader return $excelFormula; } - - /** - * Read any Named Ranges that are defined in this spreadsheet. - */ - private function readDefinedRanges(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void - { - $namedRanges = $workbookData->getElementsByTagNameNS($tableNs, 'named-range'); - foreach ($namedRanges as $definedNameElement) { - $definedName = $definedNameElement->getAttributeNS($tableNs, 'name'); - $baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address'); - $range = $definedNameElement->getAttributeNS($tableNs, 'cell-range-address'); - - $baseAddress = $this->convertToExcelAddressValue($baseAddress); - $range = $this->convertToExcelAddressValue($range); - - $this->addDefinedName($spreadsheet, $baseAddress, $definedName, $range); - } - } - - /** - * Read any Named Formulae that are defined in this spreadsheet. - */ - private function readDefinedExpressions(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void - { - $namedExpressions = $workbookData->getElementsByTagNameNS($tableNs, 'named-expression'); - foreach ($namedExpressions as $definedNameElement) { - $definedName = $definedNameElement->getAttributeNS($tableNs, 'name'); - $baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address'); - $expression = $definedNameElement->getAttributeNS($tableNs, 'expression'); - - $baseAddress = $this->convertToExcelAddressValue($baseAddress); - $expression = $this->convertToExcelFormulaValue($expression); - - $this->addDefinedName($spreadsheet, $baseAddress, $definedName, $expression); - } - } - - /** - * Assess scope and store the Defined Name. - */ - private function addDefinedName(Spreadsheet $spreadsheet, string $baseAddress, string $definedName, string $value): void - { - [$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true); - $worksheet = $spreadsheet->getSheetByName($sheetReference); - // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet - if ($worksheet !== null) { - $spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value)); - } - } } diff --git a/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php b/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php new file mode 100644 index 00000000..bdc8b3ff --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php @@ -0,0 +1,45 @@ +readAutoFilters($workbookData); + } + + protected function readAutoFilters(DOMElement $workbookData): void + { + $databases = $workbookData->getElementsByTagNameNS($this->tableNs, 'database-ranges'); + + foreach ($databases as $autofilters) { + foreach ($autofilters->childNodes as $autofilter) { + $autofilterRange = $this->getAttributeValue($autofilter, 'target-range-address'); + if ($autofilterRange !== null) { + $baseAddress = $this->convertToExcelAddressValue($autofilterRange); + $this->spreadsheet->getActiveSheet()->setAutoFilter($baseAddress); + } + } + } + } + + protected function getAttributeValue(?DOMNode $node, string $attributeName): ?string + { + if ($node !== null && $node->attributes !== null) { + $attribute = $node->attributes->getNamedItemNS( + $this->tableNs, + $attributeName + ); + + if ($attribute !== null) { + return $attribute->nodeValue; + } + } + + return null; + } +} diff --git a/src/PhpSpreadsheet/Reader/Ods/BaseReader.php b/src/PhpSpreadsheet/Reader/Ods/BaseReader.php new file mode 100644 index 00000000..17e2d4d5 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Ods/BaseReader.php @@ -0,0 +1,77 @@ +spreadsheet = $spreadsheet; + $this->tableNs = $tableNs; + } + + abstract public function read(DOMElement $workbookData): void; + + protected function convertToExcelAddressValue(string $openOfficeAddress): string + { + $excelAddress = $openOfficeAddress; + + // Cell range 3-d reference + // As we don't support 3-d ranges, we're just going to take a quick and dirty approach + // and assume that the second worksheet reference is the same as the first + $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); + // Cell range reference in another sheet + $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress ?? ''); + // Cell reference in another sheet + $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress ?? ''); + // Cell range reference + $excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress ?? ''); + // Simple cell reference + $excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress ?? ''); + + return $excelAddress ?? ''; + } + + protected function convertToExcelFormulaValue(string $openOfficeFormula): string + { + $temp = explode('"', $openOfficeFormula); + $tKey = false; + foreach ($temp as &$value) { + // @var string $value + // Only replace in alternate array entries (i.e. non-quoted blocks) + if ($tKey = !$tKey) { + // Cell range reference in another sheet + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); + // Cell reference in another sheet + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); + // Cell range reference + $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); + // Simple cell reference + $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + // Convert references to defined names/formulae + $value = str_replace('$$', '', $value ?? ''); + + $value = Calculation::translateSeparator(';', ',', $value, $inBraces); + } + } + + // Then rebuild the formula string + $excelFormula = implode('"', $temp); + + return $excelFormula; + } +} diff --git a/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php new file mode 100644 index 00000000..6810a3c7 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php @@ -0,0 +1,66 @@ +readDefinedRanges($workbookData); + $this->readDefinedExpressions($workbookData); + } + + /** + * Read any Named Ranges that are defined in this spreadsheet. + */ + protected function readDefinedRanges(DOMElement $workbookData): void + { + $namedRanges = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-range'); + foreach ($namedRanges as $definedNameElement) { + $definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name'); + $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); + $range = $definedNameElement->getAttributeNS($this->tableNs, 'cell-range-address'); + + $baseAddress = $this->convertToExcelAddressValue($baseAddress); + $range = $this->convertToExcelAddressValue($range); + + $this->addDefinedName($baseAddress, $definedName, $range); + } + } + + /** + * Read any Named Formulae that are defined in this spreadsheet. + */ + protected function readDefinedExpressions(DOMElement $workbookData): void + { + $namedExpressions = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-expression'); + foreach ($namedExpressions as $definedNameElement) { + $definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name'); + $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); + $expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression'); + + $baseAddress = $this->convertToExcelAddressValue($baseAddress); + $expression = substr($expression, strpos($expression, ':=') + 1); + $expression = $this->convertToExcelFormulaValue($expression); + + $this->addDefinedName($baseAddress, $definedName, $expression); + } + } + + /** + * Assess scope and store the Defined Name. + */ + private function addDefinedName(string $baseAddress, string $definedName, string $value): void + { + [$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true); + $worksheet = $this->spreadsheet->getSheetByName($sheetReference); + // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet + if ($worksheet !== null) { + $this->spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value)); + } + } +} diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 2b0c7016..80c32065 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -42,9 +42,12 @@ class Styles extends BaseParserClass public static function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void { - $fontStyle->setName((string) $fontStyleXml->name['val']); - $fontStyle->setSize((float) $fontStyleXml->sz['val']); - + if (isset($fontStyleXml->name, $fontStyleXml->name['val'])) { + $fontStyle->setName((string) $fontStyleXml->name['val']); + } + if (isset($fontStyleXml->sz, $fontStyleXml->sz['val'])) { + $fontStyle->setSize((float) $fontStyleXml->sz['val']); + } if (isset($fontStyleXml->b)) { $fontStyle->setBold(!isset($fontStyleXml->b['val']) || self::boolean((string) $fontStyleXml->b['val'])); } @@ -68,8 +71,7 @@ class Styles extends BaseParserClass $verticalAlign = strtolower((string) $fontStyleXml->vertAlign['val']); if ($verticalAlign === 'superscript') { $fontStyle->setSuperscript(true); - } - if ($verticalAlign === 'subscript') { + } elseif ($verticalAlign === 'subscript') { $fontStyle->setSubscript(true); } } @@ -103,17 +105,21 @@ class Styles extends BaseParserClass self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color) ); } elseif ($fillStyleXml->patternFill) { - $patternType = (string) $fillStyleXml->patternFill['patternType'] != '' - ? (string) $fillStyleXml->patternFill['patternType'] - : Fill::FILL_NONE; - - $fillStyle->setFillType($patternType); + $defaultFillStyle = Fill::FILL_NONE; if ($fillStyleXml->patternFill->fgColor) { $fillStyle->getStartColor()->setARGB(self::readColor($fillStyleXml->patternFill->fgColor, true)); + $defaultFillStyle = Fill::FILL_SOLID; } if ($fillStyleXml->patternFill->bgColor) { $fillStyle->getEndColor()->setARGB(self::readColor($fillStyleXml->patternFill->bgColor, true)); + $defaultFillStyle = Fill::FILL_SOLID; } + + $patternType = (string) $fillStyleXml->patternFill['patternType'] != '' + ? (string) $fillStyleXml->patternFill['patternType'] + : $defaultFillStyle; + + $fillStyle->setFillType($patternType); } } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 282cd528..4ef4efe7 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -284,7 +284,7 @@ class Xml extends BaseReader $worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); if ( - (isset($this->loadSheetsOnly)) && (isset($worksheet_ss['Name'])) && + isset($this->loadSheetsOnly, $worksheet_ss['Name']) && (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly)) ) { continue; diff --git a/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php b/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php new file mode 100644 index 00000000..cf0450f1 --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php @@ -0,0 +1,63 @@ +objWriter = $objWriter; + $this->spreadsheet = $spreadsheet; + } + + public function write(): void + { + $wrapperWritten = false; + $sheetCount = $this->spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $this->spreadsheet->getSheet($i); + $autofilter = $worksheet->getAutoFilter(); + if ($autofilter !== null && !empty($autofilter->getRange())) { + if ($wrapperWritten === false) { + $this->objWriter->startElement('table:database-ranges'); + $wrapperWritten = true; + } + $this->objWriter->startElement('table:database-range'); + $this->objWriter->writeAttribute('table:orientation', 'column'); + $this->objWriter->writeAttribute('table:display-filter-buttons', 'true'); + $this->objWriter->writeAttribute( + 'table:target-range-address', + $this->formatRange($worksheet, $autofilter) + ); + $this->objWriter->endElement(); + } + } + + if ($wrapperWritten === true) { + $this->objWriter->endElement(); + } + } + + protected function formatRange(Worksheet $worksheet, Autofilter $autofilter): string + { + $title = $worksheet->getTitle(); + $range = $autofilter->getRange(); + + return "'{$title}'.{$range}"; + } +} diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index e4bd1793..a589e549 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -101,6 +101,7 @@ class Content extends WriterPart $this->writeSheets($objWriter); + (new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write(); // Defined names (ranges and formulae) (new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write(); diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index a1b477bf..4bece774 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -219,7 +219,8 @@ class Xls extends BaseWriter $arrRootData[] = $OLE_DocumentSummaryInformation; } - $root = new Root(time(), time(), $arrRootData); + $time = $this->spreadsheet->getProperties()->getModified(); + $root = new Root($time, $time, $arrRootData); // save the OLE file $this->openFileHandle($pFilename); $root->save($this->fileHandle); diff --git a/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php b/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php index e3a6b206..caf85c04 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php +++ b/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php @@ -75,6 +75,16 @@ class ColorMap return self::$colorMap["#{$colorRgb}"]; } +// TODO Try and map RGB value to nearest colour within the define pallette +// $red = Color::getRed($colorRgb, false); +// $green = Color::getGreen($colorRgb, false); +// $blue = Color::getBlue($colorRgb, false); + +// $paletteSpace = 3; +// $newColor = ($red * $paletteSpace / 256) * ($paletteSpace * $paletteSpace) + +// ($green * $paletteSpace / 256) * $paletteSpace + +// ($blue * $paletteSpace / 256); + return $defaultIndex; } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 84844d3d..894ce03a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -390,7 +390,13 @@ class Worksheet extends BIFFwriter // Row dimensions foreach ($phpSheet->getRowDimensions() as $rowDimension) { $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs - $this->writeRow($rowDimension->getRowIndex() - 1, (int) $rowDimension->getRowHeight(), $xfIndex, $rowDimension->getVisible(), $rowDimension->getOutlineLevel()); + $this->writeRow( + $rowDimension->getRowIndex() - 1, + (int) $rowDimension->getRowHeight(), + $xfIndex, + !$rowDimension->getVisible(), + $rowDimension->getOutlineLevel() + ); } // Write Cells @@ -1181,7 +1187,7 @@ class Worksheet extends BIFFwriter // collapsed. The zero height flag, 0x20, is used to collapse a row. $grbit |= $level; - if ($hidden) { + if ($hidden === true) { $grbit |= 0x0030; } if ($height !== null) { @@ -2160,7 +2166,9 @@ class Worksheet extends BIFFwriter */ public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1): void { - $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage ? $this->processBitmapGd($bitmap) : $this->processBitmap($bitmap)); + $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage + ? $this->processBitmapGd($bitmap) + : $this->processBitmap($bitmap)); [$width, $height, $size, $data] = $bitmap_array; // Scale the frame of the image. diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index eefae529..56b890bb 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -318,10 +318,10 @@ class Chart extends WriterPart if ($chartType === DataSeries::TYPE_BUBBLECHART) { $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $yAxis); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); } - $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); } $objWriter->endElement(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 2c8e6f58..38dead4f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1236,6 +1236,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('t', 'str'); } elseif (is_bool($calculatedValue)) { $objWriter->writeAttribute('t', 'b'); + $calculatedValue = (int) $calculatedValue; } // array values are not yet supported //$attributes = $pCell->getFormulaAttributes(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HyperlinkTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HyperlinkTest.php new file mode 100644 index 00000000..e71992ed --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HyperlinkTest.php @@ -0,0 +1,51 @@ +getMockBuilder(Cell::class) + ->onlyMethods(['getHyperlink']) + ->disableOriginalConstructor() + ->getMock(); + $cell->method('getHyperlink') + ->willReturn($hyperlink); + + $result = LookupRef::HYPERLINK($linkUrl, $description, $cell); + if (!is_array($expectedResult)) { + self::assertSame($expectedResult, $result); + } else { + self::assertSame($expectedResult[1], $result); + self::assertSame($expectedResult[0], $hyperlink->getUrl()); + self::assertSame($expectedResult[1], $hyperlink->getTooltip()); + } + } + + public function providerHYPERLINK(): array + { + return require 'tests/data/Calculation/LookupRef/HYPERLINK.php'; + } + + public function testHYPERLINKwithoutCell(): void + { + $result = LookupRef::HYPERLINK('https://phpspreadsheet.readthedocs.io/en/latest/', 'Read the Docs'); + self::assertSame(Functions::REF(), $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php index a9ea7f29..b85f0c90 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -26,4 +26,22 @@ class SumTest extends AllSetupTeardown { return require 'tests/data/Calculation/MathTrig/SUM.php'; } + + /** + * @dataProvider providerSUMLiterals + * + * @param mixed $expectedResult + */ + public function testSUMLiterals($expectedResult, string $args): void + { + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue("=SUM($args)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerSUMLiterals(): array + { + return require 'tests/data/Calculation/MathTrig/SUMLITERALS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/AutoFilterTest.php new file mode 100644 index 00000000..18dde473 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/AutoFilterTest.php @@ -0,0 +1,31 @@ +spreadsheet = $reader->load($filename); + } + + public function testAutoFilterRange(): void + { + $worksheet = $this->spreadsheet->getActiveSheet(); + + $autoFilterRange = $worksheet->getAutoFilter()->getRange(); + + self::assertSame('A1:D57', $autoFilterRange); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php index e24178e5..9544fc3a 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php @@ -115,7 +115,8 @@ class GnumericLoadTest extends TestCase self::assertEquals(Font::UNDERLINE_DOUBLE, $sheet->getCell('A24')->getStyle()->getFont()->getUnderline()); self::assertTrue($sheet->getCell('B23')->getStyle()->getFont()->getSubScript()); self::assertTrue($sheet->getCell('B24')->getStyle()->getFont()->getSuperScript()); - self::assertFalse($sheet->getRowDimension(30)->getVisible()); + $rowDimension = $sheet->getRowDimension(30); + self::assertFalse($rowDimension->getVisible()); } public function testLoadFilter(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/AutoFilterTest.php new file mode 100644 index 00000000..47c7ee6a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/AutoFilterTest.php @@ -0,0 +1,31 @@ +spreadsheet = $reader->load($filename); + } + + public function testAutoFilterRange(): void + { + $worksheet = $this->spreadsheet->getActiveSheet(); + + $autoFilterRange = $worksheet->getAutoFilter()->getRange(); + + self::assertSame('A1:C9', $autoFilterRange); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php new file mode 100644 index 00000000..760421ce --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php @@ -0,0 +1,35 @@ +spreadsheet = $reader->load($filename); + } + + public function testDefinedNames(): void + { + $worksheet = $this->spreadsheet->getActiveSheet(); + + $firstDefinedNameValue = $worksheet->getCell('First')->getValue(); + $secondDefinedNameValue = $worksheet->getCell('Second')->getValue(); + $calculatedFormulaValue = $worksheet->getCell('B2')->getCalculatedValue(); + + self::assertSame(3, $firstDefinedNameValue); + self::assertSame(4, $secondDefinedNameValue); + self::assertSame(12, $calculatedFormulaValue); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php index 88d666b2..dc61b953 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFillTest.php @@ -1,6 +1,6 @@ getCell('J16')->getStyle()->getFill()->getFillType()); self::assertSame('solid', $sheet->getCell('C2')->getStyle()->getFill()->getFillType()); } + + public function testDefaultConditionalFill(): void + { + // default fill pattern for a conditional style where the filltype is not defined + $filename = 'tests/data/Reader/XLSX/pr2050cf-fill.xlsx'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + + $style = $spreadsheet->getActiveSheet()->getConditionalStyles('A1')[0]->getStyle(); + self::assertSame('solid', $style->getFill()->getFillType()); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFontTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFontTest.php new file mode 100644 index 00000000..a4e94804 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DefaultFontTest.php @@ -0,0 +1,22 @@ +load($filename); + + $style = $spreadsheet->getActiveSheet()->getConditionalStyles('A1')[0]->getStyle(); + self::assertSame('9C0006', $style->getFont()->getColor()->getRGB()); + self::assertNull($style->getFont()->getName()); + self::assertNull($style->getFont()->getSize()); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Csv/CsvExcelCompatibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Csv/CsvExcelCompatibilityTest.php new file mode 100644 index 00000000..9b7d16aa --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Csv/CsvExcelCompatibilityTest.php @@ -0,0 +1,49 @@ +getActiveSheet(); + $sheet->setCellValue('A1', '1'); + $sheet->setCellValue('B1', '2'); + $sheet->setCellValue('C1', '3'); + $sheet->setCellValue('A2', '4'); + $sheet->setCellValue('B2', '5'); + $sheet->setCellValue('C2', '6'); + $writer = new CsvWriter($spreadsheet); + $writer->setExcelCompatibility(true); + self::assertSame('', $writer->getOutputEncoding()); + $filename = File::temporaryFilename(); + $writer->save($filename); + $reader = new CsvReader(); + $spreadsheet2 = $reader->load($filename); + $contents = file_get_contents($filename); + unlink($filename); + self::assertEquals(1, $spreadsheet2->getActiveSheet()->getCell('A1')->getValue()); + self::assertEquals(6, $spreadsheet2->getActiveSheet()->getCell('C2')->getValue()); + self::assertStringContainsString(CsvReader::UTF8_BOM, $contents); + self::assertStringContainsString("\r\n", $contents); + self::assertStringContainsString('sep=;', $contents); + self::assertStringContainsString('"1";"2";"3"', $contents); + self::assertStringContainsString('"4";"5";"6"', $contents); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php new file mode 100644 index 00000000..4368ef76 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php @@ -0,0 +1,33 @@ +getActiveSheet(); + + $dataSet = [ + ['Year', 'Quarter', 'Sales'], + [2020, 'Q1', 100], + [2020, 'Q2', 120], + [2020, 'Q3', 140], + [2020, 'Q4', 160], + [2021, 'Q1', 180], + [2021, 'Q2', 75], + [2021, 'Q3', 0], + [2021, 'Q4', 0], + ]; + $worksheet->fromArray($dataSet, null, 'A1'); + $worksheet->getAutoFilter()->setRange('A1:C9'); + + $reloaded = $this->writeAndReload($spreadsheet, 'Ods'); + + self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange()); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/DefinedNamesTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/DefinedNamesTest.php new file mode 100644 index 00000000..1b2e30b2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/DefinedNamesTest.php @@ -0,0 +1,33 @@ +getActiveSheet(); + + $dataSet = [ + [7, 'x', 5], + ['=', '=FORMULA'], + ]; + $worksheet->fromArray($dataSet, null, 'A1'); + + $spreadsheet->addDefinedName(new NamedRange('FIRST', $worksheet, '$A$1')); + $spreadsheet->addDefinedName(new NamedRange('SECOND', $worksheet, '$C$1')); + $spreadsheet->addDefinedName(new NamedFormula('FORMULA', $worksheet, '=FIRST*SECOND')); + + $reloaded = $this->writeAndReload($spreadsheet, 'Ods'); + + self::assertSame(7, $reloaded->getActiveSheet()->getCell('FIRST')->getValue()); + self::assertSame(5, $reloaded->getActiveSheet()->getCell('SECOND')->getValue()); + self::assertSame(35, $reloaded->getActiveSheet()->getCell('B2')->getCalculatedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php new file mode 100644 index 00000000..7de39328 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php @@ -0,0 +1,99 @@ +getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + $worksheet->setCellValue("A{$row}", $row); + $worksheet->getRowDimension($row)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getRowDimension($row)->getVisible()); + } + } + + public function dataProviderRowVisibility(): array + { + return [ + [ + [1 => true, 2 => false, 3 => false, 4 => true, 5 => true, 6 => false], + ], + ]; + } + + /** + * @dataProvider dataProviderColumnVisibility + */ + public function testColumnVisibility(array $visibleColumns): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + $worksheet->setCellValue("{$column}1", $column); + $worksheet->getColumnDimension($column)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getColumnDimension($column)->getVisible()); + } + } + + public function dataProviderColumnVisibility(): array + { + return [ + [ + ['A' => true, 'B' => false, 'C' => false, 'D' => true, 'E' => true, 'F' => false], + ], + ]; + } + + /** + * @dataProvider dataProviderSheetVisibility + */ + public function testSheetVisibility(array $visibleSheets): void + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + foreach ($visibleSheets as $sheetName => $visibility) { + $worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet, $sheetName)); + $worksheet->setCellValue('A1', $sheetName); + $worksheet->setSheetState($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + foreach ($visibleSheets as $sheetName => $visibility) { + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + self::assertSame($visibility, $reloadedWorksheet->getSheetState()); + } + } + + public function dataProviderSheetVisibility(): array + { + return [ + [ + [ + 'Worksheet 1' => Worksheet::SHEETSTATE_HIDDEN, + 'Worksheet 2' => Worksheet::SHEETSTATE_VERYHIDDEN, + 'Worksheet 3' => Worksheet::SHEETSTATE_VISIBLE, + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2082Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2082Test.php new file mode 100644 index 00000000..1f72a382 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2082Test.php @@ -0,0 +1,40 @@ +getActiveSheet(); + $worksheet->fromArray(['A', 'B', 'C', 'D']); + $worksheet->getCell('A2')->setValue('=A1<>"A"'); + $worksheet->getCell('A3')->setValue('=A1="A"'); + $worksheet->getCell('B2')->setValue('=LEFT(B1, 0)'); + $worksheet->getCell('B3')->setValue('=B2=""'); + + $writer = new Writer($spreadsheet); + $writer->save($outputFilename); + $zipfile = "zip://$outputFilename#xl/worksheets/sheet1.xml"; + $contents = file_get_contents($zipfile); + unlink($outputFilename); + if ($contents === false) { + self::fail('Unable to open file'); + } else { + self::assertStringContainsString('A1<>"A"0', $contents); + self::assertStringContainsString('A1="A"1', $contents); + self::assertStringContainsString('LEFT(B1, 0)', $contents); + self::assertStringContainsString('B2=""1', $contents); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php new file mode 100644 index 00000000..7e1ca967 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php @@ -0,0 +1,99 @@ +getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + $worksheet->setCellValue("A{$row}", $row); + $worksheet->getRowDimension($row)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleRows as $row => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getRowDimension($row)->getVisible()); + } + } + + public function dataProviderRowVisibility(): array + { + return [ + [ + [1 => false, 2 => false, 3 => true, 4 => false, 5 => true, 6 => false], + ], + ]; + } + + /** + * @dataProvider dataProviderColumnVisibility + */ + public function testColumnVisibility(array $visibleColumns): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + $worksheet->setCellValue("{$column}1", $column); + $worksheet->getColumnDimension($column)->setVisible($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + foreach ($visibleColumns as $column => $visibility) { + self::assertSame($visibility, $reloadedWorksheet->getColumnDimension($column)->getVisible()); + } + } + + public function dataProviderColumnVisibility(): array + { + return [ + [ + ['A' => false, 'B' => false, 'C' => true, 'D' => false, 'E' => true, 'F' => false], + ], + ]; + } + + /** + * @dataProvider dataProviderSheetVisibility + */ + public function testSheetVisibility(array $visibleSheets): void + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + foreach ($visibleSheets as $sheetName => $visibility) { + $worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet, $sheetName)); + $worksheet->setCellValue('A1', $sheetName); + $worksheet->setSheetState($visibility); + } + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + foreach ($visibleSheets as $sheetName => $visibility) { + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + self::assertSame($visibility, $reloadedWorksheet->getSheetState()); + } + } + + public function dataProviderSheetVisibility(): array + { + return [ + [ + [ + 'Worksheet 1' => Worksheet::SHEETSTATE_HIDDEN, + 'Worksheet 2' => Worksheet::SHEETSTATE_VERYHIDDEN, + 'Worksheet 3' => Worksheet::SHEETSTATE_VISIBLE, + ], + ], + ]; + } +} diff --git a/tests/data/Calculation/LookupRef/CHOOSE.php b/tests/data/Calculation/LookupRef/CHOOSE.php index 06371c79..96c29780 100644 --- a/tests/data/Calculation/LookupRef/CHOOSE.php +++ b/tests/data/Calculation/LookupRef/CHOOSE.php @@ -25,4 +25,12 @@ return [ '#VALUE!', 0, 'red', 'blue', 'green', 'brown', ], + [ + '#VALUE!', + 'NaN', 'red', 'blue', 'green', 'brown', + ], + [ + ['blue', 'purple'], + 3, ['red', 'orange'], ['yellow', 'green'], ['blue', 'purple'], + ], ]; diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index d2a8a446..61cb7e06 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -328,4 +328,28 @@ return [ 2, false, ], + [ + 0.61, + 'Ed', + [ + [null, 'Ann', 'Cara', 'Colin', 'Ed', 'Frank'], + ['Math', 0.58, 0.90, 0.67, 0.76, 0.80], + ['French', 0.61, 0.71, 0.59, 0.59, 0.76], + ['Physics', 0.75, 0.45, 0.39, 0.52, 0.69], + ['Bioogy', 0.39, 0.55, 0.77, 0.61, 0.45], + ], + 5, + false, + ], + [ + 'Normal Weight', + 23.5, + [ + [null, 'Min', 0.0, 18.5, 25.0, 30.0], + ['BMI', 'Max', 18.4, 24.9, 29.9, null], + [null, 'Body Type', 'Underweight', 'Normal Weight', 'Overweight', 'Obese'], + ], + 3, + true, + ], ]; diff --git a/tests/data/Calculation/LookupRef/HYPERLINK.php b/tests/data/Calculation/LookupRef/HYPERLINK.php new file mode 100644 index 00000000..9a5e4c2e --- /dev/null +++ b/tests/data/Calculation/LookupRef/HYPERLINK.php @@ -0,0 +1,26 @@ + ['R' => 1]], ], - [ + 'Negative Row' => [ '#VALUE!', // Expected // Input [ @@ -15,7 +15,7 @@ return [ ], -1, ], - [ + 'Row > matrix rows' => [ '#REF!', // Expected // Input [ @@ -24,7 +24,25 @@ return [ ], 10, ], - [ + 'Row is not a number' => [ + '#VALUE!', // Expected + // Input + [ + 20 => ['R' => 1], + 21 => ['R' => 2], + ], + 'NaN', + ], + 'Row is Error' => [ + '#N/A', // Expected + // Input + [ + 20 => ['R' => 1], + 21 => ['R' => 2], + ], + '#N/A', + ], + 'Return row 2' => [ [21 => ['R' => 2]], // Expected // Input [ @@ -33,7 +51,7 @@ return [ ], 2, ], - [ + 'Return row 2 from larger matrix' => [ [21 => ['R' => 2, 'S' => 4]], // Expected // Input [ @@ -43,17 +61,17 @@ return [ 2, 0, ], - [ + 'Negative Column' => [ '#VALUE!', // Expected // Input [ '20' => ['R' => 1, 'S' => 3], '21' => ['R' => 2, 'S' => 4], ], - 2, + 0, -1, ], - [ + 'Column > matrix columns' => [ '#REF!', // Expected // Input [ @@ -63,15 +81,25 @@ return [ 2, 10, ], - [ - '#REF!', // Expected + 'Column is not a number' => [ + '#VALUE!', // Expected // Input [ - '20' => ['R' => 1, 'S' => 3], - '21' => ['R' => 2, 'S' => 4], + 20 => ['R' => 1], + 21 => ['R' => 2], ], - 10, - 2, + 1, + 'NaN', + ], + 'Column is Error' => [ + '#N/A', // Expected + // Input + [ + 20 => ['R' => 1], + 21 => ['R' => 2], + ], + 1, + '#N/A', ], [ 4, // Expected @@ -115,6 +143,15 @@ return [ 2, 1, ], + [ + [1 => ['Bananas', 'Pears']], + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 0, + ], [ 3, [ diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index a5d6f615..0a059048 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -349,4 +349,35 @@ return [ ], 2.0, ], + [ + 3.50, + 'Cornflakes', + [ + ['Item Description', 'Price'], + ['Tinned Tomatoes', 0.90], + ['Tinned Tuna', 1.50], + ['Cornflakes', 3.50], + ['Shortcake Biscuits', 1.00], + ['Toothpaste', 4.10], + ['Tinned Baked Beans', 0.99], + ['White Sliced Bread', 0.80], + ], + 2, + false, + ], + [ + 'E', + 0.52, + [ + ['Lower', 'Upper', 'Grade'], + [0.00, 0.44, 'F'], + [0.45, 0.54, 'E'], + [0.55, 0.64, 'D'], + [0.65, 0.74, 'C'], + [0.75, 0.84, 'B'], + [0.85, 1.00, 'A'], + ], + 3, + true, + ], ]; diff --git a/tests/data/Calculation/MathTrig/SUM.php b/tests/data/Calculation/MathTrig/SUM.php index a8219076..0c54613e 100644 --- a/tests/data/Calculation/MathTrig/SUM.php +++ b/tests/data/Calculation/MathTrig/SUM.php @@ -4,5 +4,9 @@ return [ [50, 5, 15, 30], [52, 5, 15, 30, 2], [53.1, 5.7, 15, 30, 2.4], - ['#VALUE!', 5.7, 'X', 30, 2.4], // error here conflicts with SUMIF + [52.1, 5.7, '14', 30, 2.4], + [38.1, 5.7, 'X', 30, 2.4], // error if entered in formula, but not in cell + [38.1, 5.7, null, 30, 2.4], + [38.1, 5.7, false, 30, 2.4], + [39.1, 5.7, true, 30, 2.4], ]; diff --git a/tests/data/Calculation/MathTrig/SUMLITERALS.php b/tests/data/Calculation/MathTrig/SUMLITERALS.php new file mode 100644 index 00000000..fd184ebd --- /dev/null +++ b/tests/data/Calculation/MathTrig/SUMLITERALS.php @@ -0,0 +1,12 @@ +