diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c01a7bdd7..c7d02f4734 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,7 @@ jobs: - '8.0' - '8.1' - '8.2' + - '8.3' include: - php-version: 'nightly' diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 3eafb6ca19..ba381a28f7 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -5,6 +5,7 @@ samples src tests + infra samples/Header.php */tests/Core/*/*Test\.(inc|css|js)$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 240454ef75..76f9746d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,12 +24,18 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Support for Conditional Formatting Color Scale. [PR #3738](https://github.com/PHPOffice/PhpSpreadsheet/pull/3738) - Support Additional Tags in Helper/Html. [Issue #3751](https://github.com/PHPOffice/PhpSpreadsheet/issues/3751) [PR #3752](https://github.com/PHPOffice/PhpSpreadsheet/pull/3752) - Writer ODS : Write Border Style for cells [Issue #3690](https://github.com/PHPOffice/PhpSpreadsheet/issues/3690) [PR #3693](https://github.com/PHPOffice/PhpSpreadsheet/pull/3693) +- Sheet Background Images [Issue #1649](https://github.com/PHPOffice/PhpSpreadsheet/issues/1649) [PR #3795](https://github.com/PHPOffice/PhpSpreadsheet/pull/3795) +- Check if Coordinate is Inside Range [PR #3779](https://github.com/PHPOffice/PhpSpreadsheet/pull/3779) +- Flipping Images [Issue #731](https://github.com/PHPOffice/PhpSpreadsheet/issues/731) [PR #3801](https://github.com/PHPOffice/PhpSpreadsheet/pull/3801) +- Chart Dynamic Title and Font Properties [Issue #3797](https://github.com/PHPOffice/PhpSpreadsheet/issues/3797) [PR #3800](https://github.com/PHPOffice/PhpSpreadsheet/pull/3800) ### Changed - Drop support for PHP 7.4, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support [PR #3713](https://github.com/PHPOffice/PhpSpreadsheet/pull/3713) - RLM Added to NumberFormatter Currency. This happens depending on release of ICU which Php is using (it does not yet happen with any official release). PhpSpreadsheet will continue to use the value returned by Php, but a method is added to keep the result unchanged from release to release. [Issue #3571](https://github.com/PHPOffice/PhpSpreadsheet/issues/3571) [PR #3640](https://github.com/PHPOffice/PhpSpreadsheet/pull/3640) - `toFormattedString` will now always return a string. This was introduced with 1.28.0, but was not properly documented at the time. This can affect the results of `toArray`, `namedRangeToArray`, and `rangeToArray`. [PR #3304](https://github.com/PHPOffice/PhpSpreadsheet/pull/3304) +- Value of constants FORMAT_CURRENCY_EUR and FORMAT_CURRENCY_USD was changed in 1.28.0, but was not properly documented at the time. [Issue #3577](https://github.com/PHPOffice/PhpSpreadsheet/issues/3577) +- Html Writer will attempt to use Chart coordinates to determine image size. [Issue #3783](https://github.com/PHPOffice/PhpSpreadsheet/issues/3783) [PR #3787](https://github.com/PHPOffice/PhpSpreadsheet/pull/3787) ### Deprecated @@ -67,6 +73,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Break Some Circular References. [PR #3716](https://github.com/PHPOffice/PhpSpreadsheet/pull/3716) [PR #3707](https://github.com/PHPOffice/PhpSpreadsheet/pull/3707) - Missing Font Index in Some Xls. [PR #3734](https://github.com/PHPOffice/PhpSpreadsheet/pull/3734) - Load Tables even with READ_DATA_ONLY. [PR #3726](https://github.com/PHPOffice/PhpSpreadsheet/pull/3726) +- Theme File Missing but Referenced in Spreadsheet. [Issue #3770](https://github.com/PHPOffice/PhpSpreadsheet/issues/3770) [PR #3772](https://github.com/PHPOffice/PhpSpreadsheet/pull/3772) +- Slk Shared Formulas. [Issue #2267](https://github.com/PHPOffice/PhpSpreadsheet/issues/2267) [PR #3776](https://github.com/PHPOffice/PhpSpreadsheet/pull/3776) +- Html omitting some charts. [Issue #3767](https://github.com/PHPOffice/PhpSpreadsheet/issues/3767) [PR #3771](https://github.com/PHPOffice/PhpSpreadsheet/pull/3771) +- Case Insensitive Comparison for Sheet Names [PR #3791](https://github.com/PHPOffice/PhpSpreadsheet/pull/3791) +- Performance improvement for Xlsx Reader. [Issue #3683](https://github.com/PHPOffice/PhpSpreadsheet/issues/3683) [PR #3810](https://github.com/PHPOffice/PhpSpreadsheet/pull/3810) +- Prevent loop in Shared/File. [Issue #3807](https://github.com/PHPOffice/PhpSpreadsheet/issues/3807) [PR #3809](https://github.com/PHPOffice/PhpSpreadsheet/pull/3809) ## 1.29.0 - 2023-06-15 @@ -146,10 +158,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Improved support for locale settings and currency codes when matching formatted strings to numerics in the Calculation Engine [PR #3373](https://github.com/PHPOffice/PhpSpreadsheet/pull/3373) and [PR #3374](https://github.com/PHPOffice/PhpSpreadsheet/pull/3374) - Improved support for locale settings and matching in the Advanced Value Binder [PR #3376](https://github.com/PHPOffice/PhpSpreadsheet/pull/3376) - `toFormattedString` will now always return a string. This can affect the results of `toArray`, `namedRangeToArray`, and `rangeToArray`. [PR #3304](https://github.com/PHPOffice/PhpSpreadsheet/pull/3304) +- Value of constants FORMAT_CURRENCY_EUR and FORMAT_CURRENCY_USD is changed. [Issue #3577](https://github.com/PHPOffice/PhpSpreadsheet/issues/3577) [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377) ### Deprecated -- Rationalisation of Pre-defined Currency Format Masks +- Rationalisation of Pre-defined Currency Format Masks [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377) ### Removed diff --git a/composer.lock b/composer.lock index 562eaac4ea..1560eafc97 100644 --- a/composer.lock +++ b/composer.lock @@ -1136,16 +1136,16 @@ "packages-dev": [ { "name": "composer/pcre", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", "shasum": "" }, "require": { @@ -1187,7 +1187,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.0" + "source": "https://github.com/composer/pcre/tree/3.1.1" }, "funding": [ { @@ -1203,7 +1203,7 @@ "type": "tidelift" } ], - "time": "2022-11-17T09:50:14+00:00" + "time": "2023-10-11T07:11:09+00:00" }, { "name": "composer/semver", @@ -1358,12 +1358,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "b49fdb59dd34284bd347435a959f2093894d1ac8" + "reference": "eda4086d152f545894f94018cb208f38dc251172" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/b49fdb59dd34284bd347435a959f2093894d1ac8", - "reference": "b49fdb59dd34284bd347435a959f2093894d1ac8", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/eda4086d152f545894f94018cb208f38dc251172", + "reference": "eda4086d152f545894f94018cb208f38dc251172", "shasum": "" }, "require": { @@ -1428,7 +1428,7 @@ "issues": "https://github.com/PHPCSStandards/composer-installer/issues", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-09-22T07:48:18+00:00" + "time": "2023-11-26T02:51:26+00:00" }, { "name": "doctrine/instantiator", @@ -1564,55 +1564,50 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.34.0", + "version": "v3.40.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7c7a4ad2ed8fe50df3e25528218b13d383608f23" + "reference": "27d2b3265b5d550ec411b4319967ae7cfddfb2e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7c7a4ad2ed8fe50df3e25528218b13d383608f23", - "reference": "7c7a4ad2ed8fe50df3e25528218b13d383608f23", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/27d2b3265b5d550ec411b4319967ae7cfddfb2e0", + "reference": "27d2b3265b5d550ec411b4319967ae7cfddfb2e0", "shasum": "" }, "require": { - "composer/semver": "^3.3", + "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", "ext-json": "*", "ext-tokenizer": "*", "php": "^7.4 || ^8.0", "sebastian/diff": "^4.0 || ^5.0", - "symfony/console": "^5.4 || ^6.0", - "symfony/event-dispatcher": "^5.4 || ^6.0", - "symfony/filesystem": "^5.4 || ^6.0", - "symfony/finder": "^5.4 || ^6.0", - "symfony/options-resolver": "^5.4 || ^6.0", - "symfony/polyfill-mbstring": "^1.27", - "symfony/polyfill-php80": "^1.27", - "symfony/polyfill-php81": "^1.27", - "symfony/process": "^5.4 || ^6.0", - "symfony/stopwatch": "^5.4 || ^6.0" - }, - "conflict": { - "stevebauman/unfinalize": "*" + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3 || ^2.0", "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^2.0", + "keradus/cli-executor": "^2.1", "mikey179/vfsstream": "^1.6.11", - "php-coveralls/php-coveralls": "^2.5.3", + "php-coveralls/php-coveralls": "^2.7", "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy": "^1.16", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", + "phpspec/prophecy": "^1.17", "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "phpunitgoodpractices/polyfill": "^1.6", - "phpunitgoodpractices/traits": "^1.9.2", - "symfony/phpunit-bridge": "^6.2.3", - "symfony/yaml": "^5.4 || ^6.0" + "phpunit/phpunit": "^9.6", + "symfony/phpunit-bridge": "^6.3.8 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1650,7 +1645,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.34.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.40.0" }, "funding": [ { @@ -1658,7 +1653,7 @@ "type": "github" } ], - "time": "2023-09-29T15:34:26+00:00" + "time": "2023-11-26T09:25:53+00:00" }, { "name": "masterminds/html5", @@ -1775,16 +1770,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.2.0", + "version": "v8.2.2", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "170a236a588d177c2aa7447ce490a030ca68e6f4" + "reference": "596a87b876d7793be7be060a8ac13424de120dd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/170a236a588d177c2aa7447ce490a030ca68e6f4", - "reference": "170a236a588d177c2aa7447ce490a030ca68e6f4", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/596a87b876d7793be7be060a8ac13424de120dd5", + "reference": "596a87b876d7793be7be060a8ac13424de120dd5", "shasum": "" }, "require": { @@ -1794,7 +1789,7 @@ "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", "myclabs/deep-copy": "^1.7", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "psr/http-message": "^1.0 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "setasign/fpdi": "^2.1" @@ -1852,7 +1847,7 @@ "type": "custom" } ], - "time": "2023-09-01T11:44:52+00:00" + "time": "2023-11-07T13:52:14+00:00" }, { "name": "mpdf/psr-http-message-shim", @@ -2376,16 +2371,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.36", + "version": "1.10.46", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "ffa3089511121a672e62969404e4fddc753f9b15" + "reference": "90d3d25c5b98b8068916bbf08ce42d5cb6c54e70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ffa3089511121a672e62969404e4fddc753f9b15", - "reference": "ffa3089511121a672e62969404e4fddc753f9b15", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/90d3d25c5b98b8068916bbf08ce42d5cb6c54e70", + "reference": "90d3d25c5b98b8068916bbf08ce42d5cb6c54e70", "shasum": "" }, "require": { @@ -2434,20 +2429,20 @@ "type": "tidelift" } ], - "time": "2023-09-29T14:07:45+00:00" + "time": "2023-11-28T14:57:26+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.14", + "version": "1.3.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "614acc10c522e319639bf38b0698a4a566665f04" + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/614acc10c522e319639bf38b0698a4a566665f04", - "reference": "614acc10c522e319639bf38b0698a4a566665f04", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", "shasum": "" }, "require": { @@ -2484,9 +2479,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.14" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15" }, - "time": "2023-08-25T09:46:39+00:00" + "time": "2023-10-09T18:58:39+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2809,16 +2804,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "9.6.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "43653e6ad7adc22e7b667dd561bf8fcb74c10cf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43653e6ad7adc22e7b667dd561bf8fcb74c10cf0", + "reference": "43653e6ad7adc22e7b667dd561bf8fcb74c10cf0", "shasum": "" }, "require": { @@ -2892,7 +2887,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.14" }, "funding": [ { @@ -2908,7 +2903,7 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2023-12-01T06:10:48+00:00" }, { "name": "psr/container", @@ -5332,16 +5327,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -5370,7 +5365,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -5378,7 +5373,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" } ], "aliases": [], @@ -5408,5 +5403,5 @@ "platform-overrides": { "php": "8.0.99" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/infra/DocumentGenerator.php b/infra/DocumentGenerator.php index e2c3c86cb9..5a86d17784 100644 --- a/infra/DocumentGenerator.php +++ b/infra/DocumentGenerator.php @@ -52,7 +52,7 @@ private static function tableRow(array $lengths, ?array $values = null): string return rtrim($result, ' '); } - private static function getPhpSpreadsheetFunctionText($functionCall): string + private static function getPhpSpreadsheetFunctionText(mixed $functionCall): string { if (is_string($functionCall)) { return $functionCall; diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index 5e570f7f0f..508a6480e3 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -38,32 +38,32 @@ class LocaleGenerator */ protected $translationBaseFolder; - protected $phpSpreadsheetFunctions; + protected array $phpSpreadsheetFunctions; /** * @var Spreadsheet */ protected $translationSpreadsheet; - protected $verbose; + protected bool $verbose; /** * @var Worksheet */ protected $localeTranslations; - protected $localeLanguageMap = []; + protected array $localeLanguageMap = []; - protected $errorCodeMap = []; + protected array $errorCodeMap = []; /** * @var Worksheet */ private $functionNameTranslations; - protected $functionNameLanguageMap = []; + protected array $functionNameLanguageMap = []; - protected $functionNameMap = []; + protected array $functionNameMap = []; public function __construct( string $translationBaseFolder, @@ -98,7 +98,7 @@ public function generateLocales(): void } } - protected function buildConfigFileForLocale($column, $locale): void + protected function buildConfigFileForLocale(string $column, string $locale): void { $language = $this->localeTranslations->getCell($column . self::ENGLISH_LANGUAGE_NAME_ROW)->getValue(); $localeLanguage = $this->localeTranslations->getCell($column . self::LOCALE_LANGUAGE_NAME_ROW)->getValue(); @@ -124,7 +124,8 @@ protected function buildConfigFileForLocale($column, $locale): void fclose($configFile); } - protected function writeConfigArgumentSeparator($configFile, $column): void + /** @param resource $configFile resource to write to */ + protected function writeConfigArgumentSeparator($configFile, string $column): void { $translationCell = $this->localeTranslations->getCell($column . self::ARGUMENT_SEPARATOR_ROW); $localeValue = $translationCell->getValue(); @@ -136,7 +137,8 @@ protected function writeConfigArgumentSeparator($configFile, $column): void } } - protected function writeConfigCurrencySymbol($configFile, $column): void + /** @param resource $configFile resource to write to */ + protected function writeConfigCurrencySymbol($configFile, string $column): void { $translationCell = $this->localeTranslations->getCell($column . self::CURRENCY_SYMBOL_ROW); $localeValue = $translationCell->getValue(); @@ -151,7 +153,7 @@ protected function writeConfigCurrencySymbol($configFile, $column): void } } - protected function buildFunctionsFileForLocale($column, $locale): void + protected function buildFunctionsFileForLocale(string $column, string $locale): void { $language = $this->functionNameTranslations->getCell($column . self::ENGLISH_LANGUAGE_NAME_ROW)->getValue(); $localeLanguage = $this->functionNameTranslations->getCell($column . self::LOCALE_LANGUAGE_NAME_ROW) @@ -176,6 +178,7 @@ protected function buildFunctionsFileForLocale($column, $locale): void fclose($functionFile); } + /** @return resource used by other methods in this class */ protected function openConfigFile(string $locale, string $language, string $localeLanguage) { $this->log("Building locale {$locale} ($language) configuration"); @@ -185,11 +188,15 @@ protected function openConfigFile(string $locale, string $language, string $loca $this->log("Writing locale configuration to {$configFileName}"); $configFile = fopen($configFileName, 'wb'); + if ($configFile === false) { + throw new Exception('Unable to open $configFileName for write'); + } $this->writeFileHeader($configFile, $localeLanguage, $language, 'locale settings'); return $configFile; } + /** @return resource used by other methods in this class */ protected function openFunctionNameFile(string $locale, string $language, string $localeLanguage) { $this->log("Building locale {$locale} ($language) function names"); @@ -199,6 +206,9 @@ protected function openFunctionNameFile(string $locale, string $language, string $this->log("Writing local function names to {$functionFileName}"); $functionFile = fopen($functionFileName, 'wb'); + if ($functionFile === false) { + throw new Exception('Unable to open $functionFileName for write'); + } $this->writeFileHeader($functionFile, $localeLanguage, $language, 'function name translations'); return $functionFile; @@ -218,6 +228,7 @@ protected function getLocaleFolder(string $locale): string return $localeFolder; } + /** @param resource $localeFile file being written to */ protected function writeFileHeader($localeFile, string $localeLanguage, string $language, string $title): void { fwrite($localeFile, str_repeat('#', 60) . self::EOL); @@ -229,6 +240,7 @@ protected function writeFileHeader($localeFile, string $localeLanguage, string $ fwrite($localeFile, str_repeat('#', 60) . self::EOL . self::EOL); } + /** @param resource $localeFile file being written to */ protected function writeFileSectionHeader($localeFile, string $header): void { fwrite($localeFile, self::EOL . '##' . self::EOL); @@ -245,9 +257,6 @@ protected function openTranslationWorkbook(): void protected function getTranslationSheet(string $sheetName): Worksheet { $worksheet = $this->translationSpreadsheet->setActiveSheetIndexByName($sheetName); - if ($worksheet === null) { - throw new Exception("{$sheetName} Worksheet not found"); - } return $worksheet; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c7cbf42ca9..c9762340b2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,31 +1,6 @@ parameters: ignoreErrors: - - - message: "#^Cannot call method getTokenSubType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 4 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - - message: "#^Cannot call method getTokenType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 8 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - message: "#^Binary operation \"/\" between float and array\\|float\\|int\\|string results in an error\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php - - - - message: "#^Offset 2 does not exist on array\\{int, int, int, int\\}\\|array\\{int, int\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Validations.php - - - - message: "#^Offset 3 does not exist on array\\{int, int, int, int\\}\\|array\\{int, int\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Validations.php - - - - message: "#^Parameter \\#2 \\$value of function ini_set expects string, int given\\.$#" - count: 1 - path: tests/bootstrap.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f6ce123adc..154541708c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,6 +8,8 @@ parameters: paths: - src/ - tests/ + - samples/ + - infra/ excludePaths: - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -21,3 +23,4 @@ parameters: ignoreErrors: # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' + - '~^Variable \$helper might not be defined\.$~' diff --git a/samples/Autofilter/10_Autofilter_selection_2.php b/samples/Autofilter/10_Autofilter_selection_2.php index 4d5d891cff..6b82bcdf52 100644 --- a/samples/Autofilter/10_Autofilter_selection_2.php +++ b/samples/Autofilter/10_Autofilter_selection_2.php @@ -126,7 +126,7 @@ $autoFilter->getColumn('D') ->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER) ->createRule() - ->setRule(Rule::AUTOFILTER_COLUMN_RULE_EQUAL, null, Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE) + ->setRule(Rule::AUTOFILTER_COLUMN_RULE_EQUAL, '', Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE) ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); $helper->log('Add filter on the Date (Column D) to display year to date'); diff --git a/samples/Basic/02_Types.php b/samples/Basic/02_Types.php index 4eaaa8b15f..d28af579d2 100644 --- a/samples/Basic/02_Types.php +++ b/samples/Basic/02_Types.php @@ -109,9 +109,9 @@ $richText->createText('你好 '); $payable = $richText->createTextRun('你 好 吗?'); -$payable->getFont()->setBold(true); -$payable->getFont()->setItalic(true); -$payable->getFont()->setColor(new Color(Color::COLOR_DARKGREEN)); +$payable->getFontOrThrow()->setBold(true); +$payable->getFontOrThrow()->setItalic(true); +$payable->getFontOrThrow()->setColor(new Color(Color::COLOR_DARKGREEN)); $richText->createText(', unless specified otherwise on the invoice.'); @@ -123,7 +123,7 @@ $richText2->createText("black text\n"); $red = $richText2->createTextRun('red text'); -$red->getFont()->setColor(new Color(Color::COLOR_RED)); +$red->getFontOrThrow()->setColor(new Color(Color::COLOR_RED)); $spreadsheet->getActiveSheet() ->getCell('C14') diff --git a/samples/Basic/19_Namedrange.php b/samples/Basic/19_Namedrange.php index 6170fa28e7..5b1a252708 100644 --- a/samples/Basic/19_Namedrange.php +++ b/samples/Basic/19_Namedrange.php @@ -36,6 +36,9 @@ // Rename named ranges $helper->log('Rename named ranges'); +if ($spreadsheet->getNamedRange('PersonName') === null) { + throw new Exception('named range not found'); +} $spreadsheet->getNamedRange('PersonName')->setName('PersonFN'); // Rename worksheet diff --git a/samples/Basic/25_In_memory_image.php b/samples/Basic/25_In_memory_image.php index c80a4981ec..ebb73c2ed5 100644 --- a/samples/Basic/25_In_memory_image.php +++ b/samples/Basic/25_In_memory_image.php @@ -28,12 +28,15 @@ // Generate an image $helper->log('Generate an image'); MemoryDrawing::checkGd(); -$gdImage = @imagecreatetruecolor(120, 20); +$gdImage = imagecreatetruecolor(120, 20); if (!$gdImage) { - exit('Cannot Initialize new GD image stream'); + throw new Exception('Cannot Initialize new GD image stream'); } $textColor = imagecolorallocate($gdImage, 255, 255, 255); +if ($textColor === false) { + throw new Exception('imagecolorallocate failed'); +} imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); // Add a drawing to the worksheet diff --git a/samples/Basic/39_Dropdown.php b/samples/Basic/39_Dropdown.php index 0e8ff52b6e..232ed0eda9 100644 --- a/samples/Basic/39_Dropdown.php +++ b/samples/Basic/39_Dropdown.php @@ -20,7 +20,8 @@ ->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.') ->setKeywords('Office PhpSpreadsheet php') ->setCategory('Test result file'); -function transpose($value) + +function transpose(string $value): array { return [$value]; } @@ -30,12 +31,12 @@ function transpose($value) $column = 'F'; // Set data for dropdowns -$continents = glob(__DIR__ . '/data/continents/*'); +$continents = glob(__DIR__ . '/data/continents/*') ?: []; foreach ($continents as $key => $filename) { $continent = pathinfo($filename, PATHINFO_FILENAME); $helper->log("Loading $continent"); $continent = str_replace(' ', '_', $continent); - $countries = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $countries = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $countryCount = count($countries); // Transpose $countries from a row to a column array diff --git a/samples/Calculations/Engineering/Convert-Online.php b/samples/Calculations/Engineering/Convert-Online.php index e20e4c79b6..3d26a3f6ba 100644 --- a/samples/Calculations/Engineering/Convert-Online.php +++ b/samples/Calculations/Engineering/Convert-Online.php @@ -77,6 +77,7 @@ $quantity = $_POST['quantity']; $fromUnit = $_POST['fromUnit']; $toUnit = $_POST['toUnit']; + /** @var float|string */ $result = ConvertUOM::CONVERT($quantity, $fromUnit, $toUnit); echo "{$quantity} {$units[$_POST['category']][$fromUnit]} is {$result} {$units[$_POST['category']][$toUnit]}", PHP_EOL; diff --git a/samples/Chart/32_Chart_read_write.php b/samples/Chart/32_Chart_read_write.php index a1f2f54681..900c9567c2 100644 --- a/samples/Chart/32_Chart_read_write.php +++ b/samples/Chart/32_Chart_read_write.php @@ -13,7 +13,7 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -40,23 +40,23 @@ } else { natsort($chartNames); foreach ($chartNames as $i => $chartName) { - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } $helper->log(' ' . $chartName . ' - ' . $caption); $indentation = str_repeat(' ', strlen($chartName) + 3); - $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); if ($groupCount == 1) { - $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); $helper->log($indentation . ' ' . $chartType); $helper->renderChart($chart, __FILE__); } else { $chartTypes = []; for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); } $chartTypes = array_unique($chartTypes); if (count($chartTypes) == 1) { diff --git a/samples/Chart/32_Chart_read_write_HTML.php b/samples/Chart/32_Chart_read_write_HTML.php index 3cb377873c..0dc5484ce5 100644 --- a/samples/Chart/32_Chart_read_write_HTML.php +++ b/samples/Chart/32_Chart_read_write_HTML.php @@ -18,7 +18,7 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -46,22 +46,22 @@ } else { natsort($chartNames); foreach ($chartNames as $i => $chartName) { - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } $helper->log(' ' . $chartName . ' - ' . $caption); $helper->log(str_repeat(' ', strlen($chartName) + 3)); - $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); if ($groupCount == 1) { - $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); $helper->log(' ' . $chartType); } else { $chartTypes = []; for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); } $chartTypes = array_unique($chartTypes); if (count($chartTypes) == 1) { diff --git a/samples/Chart/32_Chart_read_write_PDF.php b/samples/Chart/32_Chart_read_write_PDF.php index e3d702a9df..0ac41f71f6 100644 --- a/samples/Chart/32_Chart_read_write_PDF.php +++ b/samples/Chart/32_Chart_read_write_PDF.php @@ -20,7 +20,7 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -48,22 +48,22 @@ } else { natsort($chartNames); foreach ($chartNames as $i => $chartName) { - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } $helper->log(' ' . $chartName . ' - ' . $caption); $helper->log(str_repeat(' ', strlen($chartName) + 3)); - $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); if ($groupCount == 1) { - $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); $helper->log(' ' . $chartType); } else { $chartTypes = []; for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); } $chartTypes = array_unique($chartTypes); if (count($chartTypes) == 1) { diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php index e5296d1de5..c177de79a3 100644 --- a/samples/Chart/33_Chart_create_line_dateaxis.php +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -195,7 +195,7 @@ $chartSheet = $spreadsheet->getSheet(1); $chartSheet->setTitle('Scatter+Line Chart'); -$chartSheet = $spreadsheet->getSheetByName('Scatter+Line Chart'); +$chartSheet = $spreadsheet->getSheetByNameOrThrow('Scatter+Line Chart'); // Add the chart to the worksheet $chartSheet->addChart($chart); @@ -340,11 +340,14 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array { - $dataSheet = $wrkbk->getSheetByName('Data'); + $dataSheet = $wrkbk->getSheetByNameOrThrow('Data'); // start the xaxis at the beginning of the quarter of the first date $startDateStr = $dataSheet->getCell('B2')->getValue(); // yyyy-mm-dd date string $startDate = DateTime::createFromFormat('Y-m-d', $startDateStr); // php date obj + if ($startDate === false) { + throw new Exception("invalid start date $startDateStr on spreadsheet"); + } // get date of first day of the quarter of the start date $startMonth = (int) $startDate->format('n'); // suppress leading zero @@ -357,12 +360,19 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array // end the xaxis at the end of the quarter of the last date $lastDateStr = $dataSheet->getCell([2, $nrows + 1])->getValue(); $lastDate = DateTime::createFromFormat('Y-m-d', $lastDateStr); + if ($lastDate === false) { + throw new Exception("invalid last date $lastDateStr on spreadsheet"); + } $lastMonth = (int) $lastDate->format('n'); $lastYr = (int) $lastDate->format('Y'); $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); $qtrEndMonth = 3 + (($qtr - 1) * 3); - $lastDOM = cal_days_in_month(CAL_GREGORIAN, $qtrEndMonth, $lastYr); $qtrEndMonth = sprintf('%02d', $qtrEndMonth); + $lastDOMDate = DateTime::createFromFormat('Y-m-d', "$lastYr-$qtrEndMonth-01"); + if ($lastDOMDate === false) { + throw new Exception("invalid last dom date $lastYr-$qtrEndMonth-01 on spreadsheet"); + } + $lastDOM = $lastDOMDate->format('t'); $qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM"; $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr); diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index f58aadaf73..7467c82069 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -169,7 +169,7 @@ $chartSheet = $spreadsheet->getSheet(1); $chartSheet->setTitle('Scatter Chart'); -$chartSheet = $spreadsheet->getSheetByName('Scatter Chart'); +$chartSheet = $spreadsheet->getSheetByNameOrThrow('Scatter Chart'); // Add the chart to the worksheet $chartSheet->addChart($chart); diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index 4c403d993b..57eb691ef8 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -19,11 +19,13 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNames) ?: []; } if (count($inputFileNames) === 1) { + /** @var string[] */ $unresolvedErrors = []; } else { + /** @var string[] */ $unresolvedErrors = [ // The following spreadsheet was created by 3rd party software, // and doesn't include the data that usually accompanies a chart. @@ -66,9 +68,9 @@ natsort($chartNames); foreach ($chartNames as $j => $chartName) { $i = $renderedCharts + $j; - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } diff --git a/samples/Chart/35_Chart_render33.php b/samples/Chart/35_Chart_render33.php index 97091fceec..f9c7c11ad6 100644 --- a/samples/Chart/35_Chart_render33.php +++ b/samples/Chart/35_Chart_render33.php @@ -10,7 +10,7 @@ Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; -$inputFileNames = $helper->getTemporaryFolder() . '/33_Chart_create_*.xlsx'; +$inputFileNamesString = $helper->getTemporaryFolder() . '/33_Chart_create_*.xlsx'; if ((isset($argc)) && ($argc > 1)) { $inputFileNames = []; @@ -18,11 +18,13 @@ $inputFileNames[] = __DIR__ . '/../templates/' . $argv[$i]; } } else { - $inputFileNames = glob($inputFileNames); + $inputFileNames = glob($inputFileNamesString) ?: []; } if (count($inputFileNames) === 1) { + /** @var string[] */ $unresolvedErrors = []; } else { + /** @var string[] */ $unresolvedErrors = [ //'33_Chart_create_bar_stacked.xlsx', // fixed with mitoteam/jpgraph 10.3 ]; @@ -62,9 +64,9 @@ natsort($chartNames); foreach ($chartNames as $j => $chartName) { $i = $renderedCharts + $j; - $chart = $worksheet->getChartByName($chartName); + $chart = $worksheet->getChartByNameOrThrow($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } diff --git a/samples/Chart/37_Chart_dynamic_title.php b/samples/Chart/37_Chart_dynamic_title.php new file mode 100644 index 0000000000..e0df17cd04 --- /dev/null +++ b/samples/Chart/37_Chart_dynamic_title.php @@ -0,0 +1,83 @@ +log('File ' . $inputFileNameShort . ' does not exist'); + + continue; + } + $reader = IOFactory::createReader($inputFileType); + $reader->setIncludeCharts(true); + $callStartTime = microtime(true); + $spreadsheet = $reader->load($inputFileName); + $helper->logRead($inputFileType, $inputFileName, $callStartTime); + + $helper->log('Iterate worksheets looking at the charts'); + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $sheetName = $worksheet->getTitle(); + $worksheet->getCell('A1')->setValue('Changed Title'); + $helper->log('Worksheet: ' . $sheetName); + + $chartNames = $worksheet->getChartNames(); + if (empty($chartNames)) { + $helper->log(' There are no charts in this worksheet'); + } else { + natsort($chartNames); + foreach ($chartNames as $i => $chartName) { + $chart = $worksheet->getChartByNameOrThrow($chartName); + if ($chart->getTitle() !== null) { + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; + } else { + $caption = 'Untitled'; + } + $helper->log(' ' . $chartName . ' - ' . $caption); + $indentation = str_repeat(' ', strlen($chartName) + 3); + $groupCount = $chart->getPlotAreaOrThrow()->getPlotGroupCount(); + if ($groupCount == 1) { + $chartType = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex(0)->getPlotType(); + $helper->log($indentation . ' ' . $chartType); + $helper->renderChart($chart, __FILE__, $spreadsheet); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $chart->getPlotAreaOrThrow()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = 'Multiple Plot ' . array_pop($chartTypes); + $helper->log($indentation . ' ' . $chartType); + $helper->renderChart($chart, __FILE__); + } elseif (count($chartTypes) == 0) { + $helper->log($indentation . ' *** Type not yet implemented'); + } else { + $helper->log($indentation . ' Combination Chart'); + $helper->renderChart($chart, __FILE__); + } + } + } + } + } + + $callStartTime = microtime(true); + $helper->write($spreadsheet, $inputFileName, ['Xlsx'], true); + + Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); + $callStartTime = microtime(true); + $helper->write($spreadsheet, $inputFileName, ['Html'], true); + + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); +} diff --git a/samples/DefinedNames/CrossWorksheetNamedFormula.php b/samples/DefinedNames/CrossWorksheetNamedFormula.php index 5ce7651628..6c2faec2b9 100644 --- a/samples/DefinedNames/CrossWorksheetNamedFormula.php +++ b/samples/DefinedNames/CrossWorksheetNamedFormula.php @@ -40,7 +40,7 @@ $worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet)); setYearlyData($worksheet, '2020', [], 'GROWTH'); -function setYearlyData(Worksheet $worksheet, string $year, $yearlyData, ?string $title = null): void +function setYearlyData(Worksheet $worksheet, string $year, array $yearlyData, ?string $title = null): void { // Set up some basic data $worksheetTitle = $title ?: $year; diff --git a/samples/DefinedNames/ScopedNamedRange.php b/samples/DefinedNames/ScopedNamedRange.php index aa71454df0..88bc62777e 100644 --- a/samples/DefinedNames/ScopedNamedRange.php +++ b/samples/DefinedNames/ScopedNamedRange.php @@ -60,12 +60,18 @@ ->setCellValue("B{$row}", '=SUM(COLUMN_DATA_VALUES)') ->setCellValue("C{$row}", '=SUM(COLUMN_DATA_VALUES)'); +$range = $spreadsheet->getNamedRange('CHARGE_RATE'); +if ($range === null || $range->getWorksheet() === null) { + throw new Exception('expected named range not found'); +} +$chargeRateCellValue = $spreadsheet + ->getSheetByNameOrThrow($range->getWorksheet()->getTitle()) + ->getCell($range->getCellsInRange()[0])->getValue(); + $helper->log(sprintf( 'Worked %.2f hours at a rate of %s - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), - $chargeRateCellValue = $spreadsheet - ->getSheetByName($spreadsheet->getNamedRange('CHARGE_RATE')->getWorksheet()->getTitle()) - ->getCell($spreadsheet->getNamedRange('CHARGE_RATE')->getCellsInRange()[0])->getValue(), + $chargeRateCellValue, $worksheet->getCell("C{$row}")->getCalculatedValue() )); diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index d7a1cb51e3..38ba4d9c45 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -21,7 +21,7 @@ function replaceBody(string $html): string EOF; - return preg_replace($bodystring, $bodyrepl, $html); + return preg_replace($bodystring, $bodyrepl, $html) ?? ''; } require __DIR__ . '/../Header.php'; diff --git a/samples/Pdf/21c_Pdf.php b/samples/Pdf/21c_Pdf.php index f556dcb2ff..6b6e6ab05f 100644 --- a/samples/Pdf/21c_Pdf.php +++ b/samples/Pdf/21c_Pdf.php @@ -16,7 +16,7 @@ function addHeadersFootersMpdf2000(string $html): string odd-footer-name: html_myFooter2; EOF; - $html = preg_replace('/@page page0 {/', $pagerepl, $html); + $html = preg_replace('/@page page0 {/', $pagerepl, $html) ?? ''; $bodystring = '//'; $simulatedBodyStart = Mpdf::SIMULATED_BODY_START; $bodyrepl = << [ - 'R' => $fontFile, - ], - ]; + 'R' => $fontFile, + ], + ]; return new \Mpdf\Mpdf($config); } diff --git a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php index 66efc3e0fb..0a79dadd9d 100644 --- a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php +++ b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php @@ -4,9 +4,15 @@ require __DIR__ . '/../Header.php'; +/** @return string[] */ +function getDesiredSheetNames(): array +{ + return ['Data Sheet #1', 'Data Sheet #3']; +} + $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$sheetnames = ['Data Sheet #1', 'Data Sheet #3']; +$sheetnames = getDesiredSheetNames(); $helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); diff --git a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php index 437fe6cffa..a8148f33c5 100644 --- a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php +++ b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php @@ -13,13 +13,13 @@ class MyReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; - private $columns = []; + private array $columns = []; - public function __construct($startRow, $endRow, $columns) + public function __construct(int $startRow, int $endRow, array $columns) { $this->startRow = $startRow; $this->endRow = $endRow; diff --git a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php index ec01132618..64c5b5665f 100644 --- a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php +++ b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php @@ -13,17 +13,14 @@ /** Define a Read Filter class implementing IReadFilter */ class ChunkReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; /** * We expect a list of the rows that we want to read to be passed into the constructor. - * - * @param mixed $startRow - * @param mixed $chunkSize */ - public function __construct($startRow, $chunkSize) + public function __construct(int $startRow, int $chunkSize) { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; diff --git a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php index e8c4a65903..5b2a4e48f7 100644 --- a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php +++ b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php @@ -13,17 +13,14 @@ /** Define a Read Filter class implementing IReadFilter */ class ChunkReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; /** * Set the list of rows that we want to read. - * - * @param mixed $startRow - * @param mixed $chunkSize */ - public function setRows($startRow, $chunkSize): void + public function setRows(int $startRow, int $chunkSize): void { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; diff --git a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php index 82544f60a7..3d6b2cb06a 100644 --- a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php +++ b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php @@ -13,17 +13,14 @@ /** Define a Read Filter class implementing IReadFilter */ class ChunkReadFilter implements IReadFilter { - private $startRow = 0; + private int $startRow = 0; - private $endRow = 0; + private int $endRow = 0; /** * Set the list of rows that we want to read. - * - * @param mixed $startRow - * @param mixed $chunkSize */ - public function setRows($startRow, $chunkSize): void + public function setRows(int $startRow, int $chunkSize): void { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; @@ -51,8 +48,8 @@ public function readCell($columnAddress, $row, $worksheetName = '') // Tell the Reader that we want to use the Read Filter that we've Instantiated // and that we want to store it in contiguous rows/columns -$reader->setReadFilter($chunkFilter) - ->setContiguous(true); +$reader->setReadFilter($chunkFilter); +$reader->setContiguous(true); // Instantiate a new PhpSpreadsheet object manually $spreadsheet = new Spreadsheet(); diff --git a/samples/Reader/20_Reader_worksheet_hyperlink_image.php b/samples/Reader/20_Reader_worksheet_hyperlink_image.php index a6869f9717..76ab21da73 100644 --- a/samples/Reader/20_Reader_worksheet_hyperlink_image.php +++ b/samples/Reader/20_Reader_worksheet_hyperlink_image.php @@ -16,9 +16,12 @@ $gdImage = @imagecreatetruecolor(120, 20); if ($gdImage === false) { - throw new \Exception('imagecreatetruecolor failed'); + throw new Exception('imagecreatetruecolor failed'); } $textColor = imagecolorallocate($gdImage, 255, 255, 255); +if ($textColor === false) { + throw new Exception('imagecolorallocate failed'); +} imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); $baseUrl = 'https://phpspreadsheet.readthedocs.io'; @@ -54,7 +57,7 @@ $helper->log('reloaded Spreadsheet'); foreach ($reloadedSpreadsheet->getActiveSheet()->getDrawingCollection() as $pDrawing) { - $helper->log('Read link: ' . $pDrawing->getHyperlink()->getUrl()); + $helper->log('Read link: ' . ($pDrawing->getHyperlink()?->getUrl() ?? 'none')); } $helper->log('end'); diff --git a/samples/Wizards/NumberFormat/Accounting.php b/samples/Wizards/NumberFormat/Accounting.php index c09c7ca6cc..20737e502b 100644 --- a/samples/Wizards/NumberFormat/Accounting.php +++ b/samples/Wizards/NumberFormat/Accounting.php @@ -83,11 +83,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Accounting($_POST['currency'], $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $wizard = new Wizard\Accounting($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Currency.php b/samples/Wizards/NumberFormat/Currency.php index 87da383607..313db31949 100644 --- a/samples/Wizards/NumberFormat/Currency.php +++ b/samples/Wizards/NumberFormat/Currency.php @@ -83,11 +83,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Currency($_POST['currency'], $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $wizard = new Wizard\Currency($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Number.php b/samples/Wizards/NumberFormat/Number.php index ab2b2b3f3f..274f7904a1 100644 --- a/samples/Wizards/NumberFormat/Number.php +++ b/samples/Wizards/NumberFormat/Number.php @@ -49,11 +49,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Number($_POST['decimals'], isset($_POST['thousands'])); + $wizard = new Wizard\Number((int) $_POST['decimals'], isset($_POST['thousands'])); $mask = $wizard->format(); $example = NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Percentage.php b/samples/Wizards/NumberFormat/Percentage.php index c1838827ba..71b5c69113 100644 --- a/samples/Wizards/NumberFormat/Percentage.php +++ b/samples/Wizards/NumberFormat/Percentage.php @@ -43,11 +43,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Percentage($_POST['decimals']); + $wizard = new Wizard\Percentage((int) $_POST['decimals']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/Wizards/NumberFormat/Scientific.php b/samples/Wizards/NumberFormat/Scientific.php index 40d390a5e2..61e635d782 100644 --- a/samples/Wizards/NumberFormat/Scientific.php +++ b/samples/Wizards/NumberFormat/Scientific.php @@ -43,11 +43,11 @@ if (isset($_POST['submit'])) { if (!is_numeric($_POST['number'])) { $helper->log('The Sample Number Value must be numeric'); - } elseif (!is_numeric($_POST['decimals']) || str_contains($_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { + } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); } else { try { - $wizard = new Wizard\Scientific($_POST['decimals']); + $wizard = new Wizard\Scientific((int) $_POST['decimals']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); diff --git a/samples/templates/36writeMultiple1.xlsx b/samples/templates/36writeMultiple1.xlsx new file mode 100644 index 0000000000..ddbd5124a2 Binary files /dev/null and b/samples/templates/36writeMultiple1.xlsx differ diff --git a/samples/templates/37dynamictitle.xlsx b/samples/templates/37dynamictitle.xlsx new file mode 100644 index 0000000000..6a5215e861 Binary files /dev/null and b/samples/templates/37dynamictitle.xlsx differ diff --git a/samples/templates/sampleSpreadsheet.php b/samples/templates/sampleSpreadsheet.php index 92e88262d3..998f211055 100644 --- a/samples/templates/sampleSpreadsheet.php +++ b/samples/templates/sampleSpreadsheet.php @@ -74,19 +74,19 @@ $spreadsheet->getActiveSheet()->getComment('E11')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('Total amount on the current invoice, excluding VAT.'); $spreadsheet->getActiveSheet()->getComment('E12')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('Total amount of VAT on the current invoice.'); $spreadsheet->getActiveSheet()->getComment('E13')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('Total amount on the current invoice, including VAT.'); $spreadsheet->getActiveSheet()->getComment('E13')->setWidth('100pt'); @@ -100,9 +100,9 @@ $richText->createText('This invoice is '); $payable = $richText->createTextRun('payable within thirty days after the end of the month'); -$payable->getFont()->setBold(true); -$payable->getFont()->setItalic(true); -$payable->getFont()->setColor(new Color(Color::COLOR_DARKGREEN)); +$payable->getFontOrThrow()->setBold(true); +$payable->getFontOrThrow()->setItalic(true); +$payable->getFontOrThrow()->setColor(new Color(Color::COLOR_DARKGREEN)); $richText->createText(', unless specified otherwise on the invoice.'); diff --git a/samples/templates/sampleSpreadsheet2.php b/samples/templates/sampleSpreadsheet2.php index 5a7beda705..76b5e04547 100644 --- a/samples/templates/sampleSpreadsheet2.php +++ b/samples/templates/sampleSpreadsheet2.php @@ -74,19 +74,19 @@ $spreadsheet->getActiveSheet()->getComment('E11')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E11')->getText()->createTextRun('Total amount on the current invoice, excluding VAT.'); $spreadsheet->getActiveSheet()->getComment('E12')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E12')->getText()->createTextRun('Total amount of VAT on the current invoice.'); $spreadsheet->getActiveSheet()->getComment('E13')->setAuthor('PhpSpreadsheet'); $commentRichText = $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('PhpSpreadsheet:'); -$commentRichText->getFont()->setBold(true); +$commentRichText->getFontOrThrow()->setBold(true); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun("\r\n"); $spreadsheet->getActiveSheet()->getComment('E13')->getText()->createTextRun('Total amount on the current invoice, including VAT.'); $spreadsheet->getActiveSheet()->getComment('E13')->setWidth('100pt'); @@ -100,9 +100,9 @@ $richText->createText('This invoice is '); $payable = $richText->createTextRun('payable within thirty days after the end of the month'); -$payable->getFont()->setBold(true); -$payable->getFont()->setItalic(true); -$payable->getFont()->setColor(new Color(Color::COLOR_DARKGREEN)); +$payable->getFontOrThrow()->setBold(true); +$payable->getFontOrThrow()->setItalic(true); +$payable->getFontOrThrow()->setColor(new Color(Color::COLOR_DARKGREEN)); $richText->createText(', unless specified otherwise on the invoice.'); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index ad13a500de..960db939c2 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4409,7 +4409,7 @@ private function internalParseFormula($formula, ?Cell $cell = null): bool|array [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); if ($rangeWS2 !== '') { $rangeWS2 .= '!'; - } else { // @phpstan-ignore-line + } else { $rangeWS2 = $rangeWS1; } @@ -4715,7 +4715,7 @@ private function processTokenStack(mixed $tokens, $cellID = null, ?Cell $cell = if (($operand2Data = $stack->pop()) === null) { return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } - if (($operand1Data = $stack->pop()) === null) { + if (($operand1Data = $stack->pop()) === null) { // @phpstan-ignore-line return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } @@ -5072,7 +5072,7 @@ private function processTokenStack(mixed $tokens, $cellID = null, ?Cell $cell = krsort($args); krsort($emptyArguments); - if ($argCount > 0) { + if ($argCount > 0 && is_array($functionCall)) { $args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments); } @@ -5571,7 +5571,7 @@ public function getImplementedFunctionNames(): array private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array { - $reflector = new ReflectionMethod(implode('::', $functionCall)); + $reflector = new ReflectionMethod($functionCall[0], $functionCall[1]); $methodArguments = $reflector->getParameters(); if (count($methodArguments) > 0) { diff --git a/src/PhpSpreadsheet/Calculation/FormulaParser.php b/src/PhpSpreadsheet/Calculation/FormulaParser.php index cffce179c6..0be1ca8ac9 100644 --- a/src/PhpSpreadsheet/Calculation/FormulaParser.php +++ b/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -530,12 +530,12 @@ private function parseToTokens(): void if ($i == 0) { $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX); } elseif ( - (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) ) { $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); } else { @@ -551,12 +551,12 @@ private function parseToTokens(): void if ($i == 0) { continue; } elseif ( - (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) - && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) - || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) ) { $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); } else { diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 5b8ba19834..8ebc115a88 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -3,9 +3,10 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Collection\Cells; -use PhpOffice\PhpSpreadsheet\Exception; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -76,13 +77,13 @@ class Cell implements Stringable /** * Update the cell into the cell collection. * - * @return $this + * @throws SpreadsheetException */ public function updateInCollection(): self { $parent = $this->parent; if ($parent === null) { - throw new Exception('Cannot update when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot update when cell is not bound to a worksheet'); } $parent->update($this); @@ -101,6 +102,8 @@ public function attach(Cells $parent): void /** * Create a new Cell. + * + * @throws SpreadsheetException */ public function __construct(mixed $value, ?string $dataType, Worksheet $worksheet) { @@ -117,19 +120,21 @@ public function __construct(mixed $value, ?string $dataType, Worksheet $workshee } $this->dataType = $dataType; } elseif (self::getValueBinder()->bindValue($this, $value) === false) { - throw new Exception('Value could not be bound to cell.'); + throw new SpreadsheetException('Value could not be bound to cell.'); } $this->ignoredErrors = new IgnoredErrors(); } /** * Get cell coordinate column. + * + * @throws SpreadsheetException */ public function getColumn(): string { $parent = $this->parent; if ($parent === null) { - throw new Exception('Cannot get column when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get column when cell is not bound to a worksheet'); } return $parent->getCurrentColumn(); @@ -137,12 +142,14 @@ public function getColumn(): string /** * Get cell coordinate row. + * + * @throws SpreadsheetException */ public function getRow(): int { $parent = $this->parent; if ($parent === null) { - throw new Exception('Cannot get row when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get row when cell is not bound to a worksheet'); } return $parent->getCurrentRow(); @@ -151,9 +158,9 @@ public function getRow(): int /** * Get cell coordinate. * - * @return string + * @throws SpreadsheetException */ - public function getCoordinate() + public function getCoordinate(): string { $parent = $this->parent; if ($parent !== null) { @@ -162,7 +169,7 @@ public function getCoordinate() $coordinate = null; } if ($coordinate === null) { - throw new Exception('Coordinate no longer exists'); + throw new SpreadsheetException('Coordinate no longer exists'); } return $coordinate; @@ -216,15 +223,13 @@ protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self * @param mixed $value Value * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder * - * @throws Exception - * - * @return $this + * @throws SpreadsheetException */ public function setValue(mixed $value, ?IValueBinder $binder = null): self { $binder ??= self::getValueBinder(); if (!$binder->bindValue($this, $value)) { - throw new Exception('Value could not be bound to cell.'); + throw new SpreadsheetException('Value could not be bound to cell.'); } return $this; @@ -241,9 +246,9 @@ public function setValue(mixed $value, ?IValueBinder $binder = null): self * If you do mismatch value and datatype, then the value you enter may be changed to match the datatype * that you specify. * - * @return Cell + * @throws SpreadsheetException */ - public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING) + public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self { $oldValue = $this->value; @@ -265,7 +270,7 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE break; case DataType::TYPE_NUMERIC: if (is_string($value) && !is_numeric($value)) { - throw new Exception('Invalid numeric value for datatype Numeric'); + throw new SpreadsheetException('Invalid numeric value for datatype Numeric'); } $this->value = 0 + $value; @@ -288,7 +293,7 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE break; default: - throw new Exception('Invalid datatype: ' . $dataType); + throw new SpreadsheetException('Invalid datatype: ' . $dataType); } // set the datatype @@ -313,11 +318,12 @@ public static function getCalculateDateTimeType(): int return self::$calculateDateTimeType; } + /** @throws CalculationException*/ public static function setCalculateDateTimeType(int $calculateDateTimeType): void { self::$calculateDateTimeType = match ($calculateDateTimeType) { self::CALCULATE_DATE_TIME_ASIS, self::CALCULATE_DATE_TIME_FLOAT, self::CALCULATE_TIME_FLOAT => $calculateDateTimeType, - default => throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception("Invalid value $calculateDateTimeType for calculated date time type"), + default => throw new CalculationException("Invalid value $calculateDateTimeType for calculated date time type"), }; } @@ -348,9 +354,9 @@ private function convertDateTimeInt(mixed $result) * * @param bool $resetLog Whether the calculation engine logger should be reset or not * - * @return mixed + * @throws CalculationException */ - public function getCalculatedValue(bool $resetLog = true) + public function getCalculatedValue(bool $resetLog = true): mixed { if ($this->dataType === DataType::TYPE_FORMULA) { try { @@ -368,14 +374,14 @@ public function getCalculatedValue(bool $resetLog = true) $result = array_shift($result); } } - } catch (Exception $ex) { + } catch (SpreadsheetException $ex) { if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) { return $this->calculatedValue; // Fallback for calculations referencing external files. } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) { return ExcelError::NAME(); } - throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception( + throw new CalculationException( $this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(), $ex->getCode(), $ex @@ -453,11 +459,13 @@ public function isFormula(): bool /** * Does this cell contain Data validation rules? + * + * @throws SpreadsheetException */ public function hasDataValidation(): bool { if (!isset($this->parent)) { - throw new Exception('Cannot check for data validation when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot check for data validation when cell is not bound to a worksheet'); } return $this->getWorksheet()->dataValidationExists($this->getCoordinate()); @@ -465,11 +473,13 @@ public function hasDataValidation(): bool /** * Get Data validation rules. + * + * @throws SpreadsheetException */ public function getDataValidation(): DataValidation { if (!isset($this->parent)) { - throw new Exception('Cannot get data validation for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get data validation for cell that is not bound to a worksheet'); } return $this->getWorksheet()->getDataValidation($this->getCoordinate()); @@ -477,11 +487,13 @@ public function getDataValidation(): DataValidation /** * Set Data validation rules. + * + * @throws SpreadsheetException */ public function setDataValidation(?DataValidation $dataValidation = null): self { if (!isset($this->parent)) { - throw new Exception('Cannot set data validation for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot set data validation for cell that is not bound to a worksheet'); } $this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation); @@ -501,11 +513,13 @@ public function hasValidValue(): bool /** * Does this cell contain a Hyperlink? + * + * @throws SpreadsheetException */ public function hasHyperlink(): bool { if (!isset($this->parent)) { - throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet'); + throw new SpreadsheetException('Cannot check for hyperlink when cell is not bound to a worksheet'); } return $this->getWorksheet()->hyperlinkExists($this->getCoordinate()); @@ -513,11 +527,13 @@ public function hasHyperlink(): bool /** * Get Hyperlink. + * + * @throws SpreadsheetException */ public function getHyperlink(): Hyperlink { if (!isset($this->parent)) { - throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet'); } return $this->getWorksheet()->getHyperlink($this->getCoordinate()); @@ -525,11 +541,13 @@ public function getHyperlink(): Hyperlink /** * Set Hyperlink. + * + * @throws SpreadsheetException */ public function setHyperlink(?Hyperlink $hyperlink = null): self { if (!isset($this->parent)) { - throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet'); + throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet'); } $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink); @@ -549,6 +567,8 @@ public function getParent() /** * Get parent worksheet. + * + * @throws SpreadsheetException */ public function getWorksheet(): Worksheet { @@ -560,7 +580,7 @@ public function getWorksheet(): Worksheet } if ($worksheet === null) { - throw new Exception('Worksheet no longer exists'); + throw new SpreadsheetException('Worksheet no longer exists'); } return $worksheet; diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 564d280a2e..81ad64d4f9 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -14,6 +14,7 @@ abstract class Coordinate { public const A1_COORDINATE_REGEX = '/^(?\$?[A-Z]{1,3})(?\$?\d{1,7})$/i'; + public const FULL_REFERENCE_REGEX = '/^(?:(?[^!]*)!)?(?(?[$]?[A-Z]{1,3}[$]?\d{1,7})(?:\:(?[$]?[A-Z]{1,3}[$]?\d{1,7}))?)$/i'; /** * Default range variable constant. @@ -258,6 +259,91 @@ public static function getRangeBoundaries(string $range) ]; } + /** + * Check if cell or range reference is valid and return an array with type of reference (cell or range), worksheet (if it was given) + * and the coordinate or the first coordinate and second coordinate if it is a range. + * + * @param string $reference Coordinate or Range (e.g. A1:A1, B2, B:C, 2:3) + * + * @return array reference data + */ + private static function validateReferenceAndGetData($reference): array + { + $data = []; + preg_match(self::FULL_REFERENCE_REGEX, $reference, $matches); + if (count($matches) === 0) { + return ['type' => 'invalid']; + } + + if (isset($matches['secondCoordinate'])) { + $data['type'] = 'range'; + $data['firstCoordinate'] = str_replace('$', '', $matches['firstCoordinate']); + $data['secondCoordinate'] = str_replace('$', '', $matches['secondCoordinate']); + } else { + $data['type'] = 'coordinate'; + $data['coordinate'] = str_replace('$', '', $matches['firstCoordinate']); + } + + $worksheet = $matches['worksheet']; + if ($worksheet !== '') { + if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") { + $worksheet = substr($worksheet, 1, -1); + } + $data['worksheet'] = strtolower($worksheet); + } + $data['localReference'] = str_replace('$', '', $matches['localReference']); + + return $data; + } + + /** + * Check if coordinate is inside a range. + * + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) + * @param string $coordinate Cell coordinate (e.g. A1) + * + * @return bool true if coordinate is inside range + */ + public static function coordinateIsInsideRange(string $range, string $coordinate): bool + { + $rangeData = self::validateReferenceAndGetData($range); + if ($rangeData['type'] === 'invalid') { + throw new Exception('First argument needs to be a range'); + } + + $coordinateData = self::validateReferenceAndGetData($coordinate); + if ($coordinateData['type'] === 'invalid') { + throw new Exception('Second argument needs to be a single coordinate'); + } + + if (isset($coordinateData['worksheet']) && !isset($rangeData['worksheet'])) { + return false; + } + if (!isset($coordinateData['worksheet']) && isset($rangeData['worksheet'])) { + return false; + } + + if (isset($coordinateData['worksheet'], $rangeData['worksheet'])) { + if ($coordinateData['worksheet'] !== $rangeData['worksheet']) { + return false; + } + } + + $boundaries = self::rangeBoundaries($rangeData['localReference']); + $coordinates = self::indexesFromString($coordinateData['localReference']); + + $columnIsInside = $boundaries[0][0] <= $coordinates[0] && $coordinates[0] <= $boundaries[1][0]; + if (!$columnIsInside) { + return false; + } + $rowIsInside = $boundaries[0][1] <= $coordinates[1] && $coordinates[1] <= $boundaries[1][1]; + if (!$rowIsInside) { + return false; + } + + return true; + } + /** * Column index from string. * diff --git a/src/PhpSpreadsheet/Cell/DataValidation.php b/src/PhpSpreadsheet/Cell/DataValidation.php index 7f1b363c5d..22b285b307 100644 --- a/src/PhpSpreadsheet/Cell/DataValidation.php +++ b/src/PhpSpreadsheet/Cell/DataValidation.php @@ -140,13 +140,13 @@ public function getFormula1() /** * Set Formula 1. * - * @param string $formula + * @param float|int|string $formula usually string, but can be number (test for equal) * * @return $this */ public function setFormula1($formula): static { - $this->formula1 = $formula; + $this->formula1 = (string) $formula; return $this; } @@ -164,13 +164,13 @@ public function getFormula2() /** * Set Formula 2. * - * @param string $formula + * @param float|int|string $formula usually string, but can be number (test for equal) * * @return $this */ public function setFormula2($formula): static { - $this->formula2 = $formula; + $this->formula2 = (string) $formula; return $this; } diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 7de219184a..3f9249cedd 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -124,10 +124,10 @@ public function getAxisIsNumericFormat(): bool return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric']; } - public function setAxisOption(string $key, ?string $value): void + public function setAxisOption(string $key, null|float|int|string $value): void { if ($value !== null && $value !== '') { - $this->axisOptions[$key] = $value; + $this->axisOptions[$key] = (string) $value; } } @@ -141,11 +141,11 @@ public function setAxisOptionsProperties( ?string $axisOrientation = null, ?string $majorTmt = null, ?string $minorTmt = null, - ?string $minimum = null, - ?string $maximum = null, - ?string $majorUnit = null, - ?string $minorUnit = null, - ?string $textRotation = null, + null|float|int|string $minimum = null, + null|float|int|string $maximum = null, + null|float|int|string $majorUnit = null, + null|float|int|string $minorUnit = null, + null|float|int|string $textRotation = null, ?string $hidden = null, ?string $baseTimeUnit = null, ?string $majorTimeUnit = null, diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 55c535e187..3dda80bd58 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -133,6 +133,16 @@ class Chart private ChartColor $fillColor; + /** + * Rendered width in pixels. + */ + private ?float $renderedWidth = null; + + /** + * Rendered height in pixels. + */ + private ?float $renderedHeight = null; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -276,6 +286,16 @@ public function getPlotArea(): ?PlotArea return $this->plotArea; } + public function getPlotAreaOrThrow(): PlotArea + { + $plotArea = $this->getPlotArea(); + if ($plotArea !== null) { + return $plotArea; + } + + throw new Exception('Chart has no PlotArea'); + } + /** * Set Plot Area. */ @@ -793,4 +813,28 @@ public function getFillColor(): ChartColor { return $this->fillColor; } + + public function setRenderedWidth(?float $width): self + { + $this->renderedWidth = $width; + + return $this; + } + + public function getRenderedWidth(): ?float + { + return $this->renderedWidth; + } + + public function setRenderedHeight(?float $height): self + { + $this->renderedHeight = $height; + + return $this; + } + + public function getRenderedHeight(): ?float + { + return $this->renderedHeight; + } } diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 8bceae071d..6475793457 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -124,7 +124,7 @@ class DataSeries * @param DataSeriesValues[] $plotCategory * @param DataSeriesValues[] $plotValues * @param null|string $plotDirection - * @param bool $smoothLine + * @param null|bool $smoothLine null treated as false * @param null|string $plotStyle */ public function __construct($plotType = null, $plotGrouping = null, array $plotOrder = [], array $plotLabel = [], array $plotCategory = [], array $plotValues = [], $plotDirection = null, $smoothLine = false, $plotStyle = null) @@ -144,7 +144,7 @@ public function __construct($plotType = null, $plotGrouping = null, array $plotO } $this->plotCategory = $plotCategory; - $this->smoothLine = $smoothLine; + $this->smoothLine = (bool) $smoothLine; $this->plotStyle = $plotStyle; if ($plotDirection === null) { diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index a3f5ca0cf0..72bbf81036 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -98,7 +98,7 @@ class DataSeriesValues extends Properties * @param mixed $dataValues * @param null|mixed $marker * @param null|ChartColor|ChartColor[]|string|string[] $fillColor - * @param string $pointSize + * @param int|string $pointSize point size */ public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3') { diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 849f561949..740702d12d 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -795,9 +795,9 @@ public function getLineColorProperty($propertyName) * @param string $capType * @param string $joinType * @param string $headArrowType - * @param string $headArrowSize + * @param null|int|string $headArrowSize index into ARROW_SIZES array * @param string $endArrowType - * @param string $endArrowSize + * @param null|int|string $endArrowSize index into ARROW_SIZES array * @param string $headArrowWidth * @param string $headArrowLength * @param string $endArrowWidth @@ -824,7 +824,7 @@ public function setLineStyleProperties($lineWidth = null, $compoundType = '', $d if ($headArrowType !== '') { $this->lineStyleProperties['arrow']['head']['type'] = $headArrowType; } - if (array_key_exists($headArrowSize, self::ARROW_SIZES)) { + if (isset(self::ARROW_SIZES[$headArrowSize])) { $this->lineStyleProperties['arrow']['head']['size'] = $headArrowSize; $this->lineStyleProperties['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; $this->lineStyleProperties['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; @@ -832,7 +832,7 @@ public function setLineStyleProperties($lineWidth = null, $compoundType = '', $d if ($endArrowType !== '') { $this->lineStyleProperties['arrow']['end']['type'] = $endArrowType; } - if (array_key_exists($endArrowSize, self::ARROW_SIZES)) { + if (isset(self::ARROW_SIZES[$endArrowSize])) { $this->lineStyleProperties['arrow']['end']['size'] = $endArrowSize; $this->lineStyleProperties['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; $this->lineStyleProperties['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php index 40ce338d93..27cf3b165d 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -26,9 +26,9 @@ */ abstract class JpGraphRendererBase implements IRenderer { - private static $width = 640; + private const DEFAULT_WIDTH = 640.0; - private static $height = 480; + private const DEFAULT_HEIGHT = 480.0; private static $colourSet = [ 'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1', @@ -70,6 +70,16 @@ public function __construct(Chart $chart) ]; } + private function getGraphWidth(): float + { + return $this->chart->getRenderedWidth() ?? self::DEFAULT_WIDTH; + } + + private function getGraphHeight(): float + { + return $this->chart->getRenderedHeight() ?? self::DEFAULT_HEIGHT; + } + /** * This method should be overriden in descendants to do real JpGraph library initialization. */ @@ -221,7 +231,7 @@ private function renderLegend(): void private function renderCartesianPlotArea(string $type = 'textlin'): void { - $this->graph = new Graph(self::$width, self::$height); + $this->graph = new Graph($this->getGraphWidth(), $this->getGraphHeight()); $this->graph->SetScale($type); $this->renderTitle(); @@ -258,14 +268,14 @@ private function renderCartesianPlotArea(string $type = 'textlin'): void private function renderPiePlotArea(): void { - $this->graph = new PieGraph(self::$width, self::$height); + $this->graph = new PieGraph($this->getGraphWidth(), $this->getGraphHeight()); $this->renderTitle(); } private function renderRadarPlotArea(): void { - $this->graph = new RadarGraph(self::$width, self::$height); + $this->graph = new RadarGraph($this->getGraphWidth(), $this->getGraphHeight()); $this->graph->SetScale('lin'); $this->renderTitle(); @@ -460,7 +470,6 @@ private function renderPlotScatter(int $groupID, bool $bubble): void $dataValuesY[$k] = $k; } } - //var_dump($dataValuesY, $dataValuesX, $bubbleSize); $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); if ($scatterStyle == 'lineMarker') { @@ -468,7 +477,7 @@ private function renderPlotScatter(int $groupID, bool $bubble): void $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); } elseif ($scatterStyle == 'smoothMarker') { $spline = new Spline($dataValuesY, $dataValuesX); - [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); + [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * $this->getGraphWidth() / 20); $lplot = new LinePlot($splineDataX, $splineDataY); $lplot->SetColor(self::$colourSet[self::$plotColour]); diff --git a/src/PhpSpreadsheet/Chart/Title.php b/src/PhpSpreadsheet/Chart/Title.php index d8e6e7497f..378987446e 100644 --- a/src/PhpSpreadsheet/Chart/Title.php +++ b/src/PhpSpreadsheet/Chart/Title.php @@ -3,9 +3,16 @@ namespace PhpOffice\PhpSpreadsheet\Chart; use PhpOffice\PhpSpreadsheet\RichText\RichText; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; class Title { + public const TITLE_CELL_REFERENCE + = '/^(.*)!' // beginning of string, everything up to ! is match[1] + . '[$]([A-Z]{1,3})' // absolute column string match[2] + . '[$](\d{1,7})$/i'; // absolute row string match[3] + /** * Title Caption. * @@ -25,6 +32,10 @@ class Title */ private ?Layout $layout; + private string $cellReference = ''; + + private ?Font $font = null; + /** * Create a new Title. * @@ -48,8 +59,14 @@ public function getCaption() return $this->caption; } - public function getCaptionText(): string + public function getCaptionText(?Spreadsheet $spreadsheet = null): string { + if ($spreadsheet !== null) { + $caption = $this->getCalculatedTitle($spreadsheet); + if ($caption !== null) { + return $caption; + } + } $caption = $this->caption; if (is_string($caption)) { return $caption; @@ -100,13 +117,50 @@ public function getOverlay() * * @param bool $overlay */ - public function setOverlay($overlay): void + public function setOverlay($overlay): static { $this->overlay = $overlay; + + return $this; } public function getLayout(): ?Layout { return $this->layout; } + + public function setCellReference(string $cellReference): self + { + $this->cellReference = $cellReference; + + return $this; + } + + public function getCellReference(): string + { + return $this->cellReference; + } + + public function getCalculatedTitle(?Spreadsheet $spreadsheet): ?string + { + preg_match(self::TITLE_CELL_REFERENCE, $this->cellReference, $matches); + if (count($matches) === 0 || $spreadsheet === null) { + return null; + } + $sheetName = preg_replace("/^'(.*)'$/", '$1', $matches[1]) ?? ''; + + return $spreadsheet->getSheetByName($sheetName)?->getCell($matches[2] . $matches[3])?->getFormattedValue(); + } + + public function getFont(): ?Font + { + return $this->font; + } + + public function setFont(?Font $font): self + { + $this->font = $font; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Exception.php b/src/PhpSpreadsheet/Exception.php index 9c5ab30ee0..349158078a 100644 --- a/src/PhpSpreadsheet/Exception.php +++ b/src/PhpSpreadsheet/Exception.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet; -class Exception extends \Exception +use RuntimeException; + +class Exception extends RuntimeException { } diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 3d53f1e053..cb23cf597c 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -198,7 +198,7 @@ public function log(string $message): void * * @codeCoverageIgnore */ - public function renderChart(Chart $chart, string $fileName): void + public function renderChart(Chart $chart, string $fileName, ?Spreadsheet $spreadsheet = null): void { if ($this->isCli() === true) { return; @@ -206,17 +206,32 @@ public function renderChart(Chart $chart, string $fileName): void Settings::setChartRenderer(MtJpGraphRenderer::class); $fileName = $this->getFilename($fileName, 'png'); + $title = $chart->getTitle(); + $caption = null; + if ($title !== null) { + $calculatedTitle = $title->getCalculatedTitle($spreadsheet); + if ($calculatedTitle !== null) { + $caption = $title->getCaption(); + $title->setCaption($calculatedTitle); + } + } try { $chart->render($fileName); $this->log('Rendered image: ' . $fileName); - $imageData = file_get_contents($fileName); + $imageData = @file_get_contents($fileName); if ($imageData !== false) { echo '
'; + } else { + $this->log('Unable to open chart' . PHP_EOL); } } catch (Throwable $e) { $this->log('Error rendering chart: ' . $e->getMessage() . PHP_EOL); } + if (isset($title, $caption)) { + $title->setCaption($caption); + } + Settings::unsetChartRenderer(); } public function titles(string $category, string $functionName, ?string $description = null): void diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index d5a565a55c..a93bbcf4c7 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -9,7 +9,6 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Csv extends BaseReader @@ -102,6 +101,14 @@ class Csv extends BaseReader /** @var bool */ private $sheetNameIsFileName = false; + private string $getTrue = 'true'; + + private string $getFalse = 'false'; + + private string $thousandsSeparator = ','; + + private string $decimalSeparator = '.'; + /** * Create a new CSV Reader instance. */ @@ -234,13 +241,14 @@ public function listWorksheetInfo(string $filename): array $worksheetInfo[0]['lastColumnIndex'] = 0; $worksheetInfo[0]['totalRows'] = 0; $worksheetInfo[0]['totalColumns'] = 0; + $delimiter = $this->delimiter ?? ''; // Loop through each line of the file in turn - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); while (is_array($rowData)) { ++$worksheetInfo[0]['totalRows']; $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1); - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); } $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); @@ -386,15 +394,24 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo $outRow = 0; // Loop through each line of the file in turn - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $delimiter = $this->delimiter ?? ''; + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); $valueBinder = Cell::getValueBinder(); $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion(); + $this->getTrue = Calculation::getTRUE(); + $this->getFalse = Calculation::getFALSE(); + $this->thousandsSeparator = StringHelper::getThousandsSeparator(); + $this->decimalSeparator = StringHelper::getDecimalSeparator(); while (is_array($rowData)) { $noOutputYet = true; $columnLetter = 'A'; foreach ($rowData as $rowDatum) { - $this->convertBoolean($rowDatum, $preserveBooleanString); - $numberFormatMask = $this->convertFormattedNumber($rowDatum); + if ($preserveBooleanString) { + $rowDatum = $rowDatum ?? ''; + } else { + $this->convertBoolean($rowDatum); + } + $numberFormatMask = $this->castFormattedNumberToNumeric ? $this->convertFormattedNumber($rowDatum) : ''; if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($this->contiguous) { if ($noOutputYet) { @@ -405,15 +422,17 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo $outRow = $currentRow; } // Set basic styling for the value (Note that this could be overloaded by styling in a value binder) - $sheet->getCell($columnLetter . $outRow)->getStyle() - ->getNumberFormat() - ->setFormatCode($numberFormatMask); + if ($numberFormatMask !== '') { + $sheet->getStyle($columnLetter . $outRow) + ->getNumberFormat() + ->setFormatCode($numberFormatMask); + } // Set cell value $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum); } ++$columnLetter; } - $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + $rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); ++$currentRow; } @@ -429,12 +448,12 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo /** * Convert string true/false to boolean, and null to null-string. */ - private function convertBoolean(mixed &$rowDatum, bool $preserveBooleanString): void + private function convertBoolean(mixed &$rowDatum): void { - if (is_string($rowDatum) && !$preserveBooleanString) { - if (strcasecmp(Calculation::getTRUE(), $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { + if (is_string($rowDatum)) { + if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { $rowDatum = true; - } elseif (strcasecmp(Calculation::getFALSE(), $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { + } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { $rowDatum = false; } } else { @@ -447,18 +466,18 @@ private function convertBoolean(mixed &$rowDatum, bool $preserveBooleanString): */ private function convertFormattedNumber(mixed &$rowDatum): string { - $numberFormatMask = NumberFormat::FORMAT_GENERAL; + $numberFormatMask = ''; if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) { $numeric = str_replace( - [StringHelper::getThousandsSeparator(), StringHelper::getDecimalSeparator()], + [$this->thousandsSeparator, $this->decimalSeparator], ['', '.'], $rowDatum ); if (is_numeric($numeric)) { - $decimalPos = strpos($rowDatum, StringHelper::getDecimalSeparator()); + $decimalPos = strpos($rowDatum, $this->decimalSeparator); if ($this->preserveNumericFormatting === true) { - $numberFormatMask = (str_contains($rowDatum, StringHelper::getThousandsSeparator())) + $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator)) ? '#,##0' : '0'; if ($decimalPos !== false) { $decimals = strlen($rowDatum) - $decimalPos - 1; diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php index 3b026fe277..275d0971b9 100644 --- a/src/PhpSpreadsheet/Reader/Slk.php +++ b/src/PhpSpreadsheet/Reader/Slk.php @@ -5,6 +5,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; +use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Border; @@ -269,14 +270,14 @@ private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, strin $hasCalculatedValue = false; $tryNumeric = false; $cellDataFormula = $cellData = ''; + $sharedColumn = $sharedRow = -1; + $sharedFormula = false; foreach ($rowData as $rowDatum) { switch ($rowDatum[0]) { - case 'C': case 'X': $column = substr($rowDatum, 1); break; - case 'R': case 'Y': $row = substr($rowDatum, 1); @@ -298,9 +299,36 @@ private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, strin ->getText() ->createText($comment); + break; + case 'C': + $sharedColumn = (int) substr($rowDatum, 1); + + break; + case 'R': + $sharedRow = (int) substr($rowDatum, 1); + + break; + case 'S': + $sharedFormula = true; + break; } } + if ($sharedFormula === true && $sharedRow >= 0 && $sharedColumn >= 0) { + $thisCoordinate = Coordinate::stringFromColumnIndex((int) $column) . $row; + $sharedCoordinate = Coordinate::stringFromColumnIndex($sharedColumn) . $sharedRow; + $formula = $spreadsheet->getActiveSheet()->getCell($sharedCoordinate)->getValue(); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($formula); + $referenceHelper = ReferenceHelper::getInstance(); + $newFormula = $referenceHelper->updateFormulaReferences($formula, 'A1', (int) $column - $sharedColumn, (int) $row - $sharedRow, '', true, false); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($newFormula); + //$calc = $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->getCalculatedValue(); + //$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($calc); + $cellData = Calculation::unwrapResult($cellData); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($cellData, $tryNumeric); + + return; + } $columnLetter = Coordinate::stringFromColumnIndex((int) $column); $cellData = Calculation::unwrapResult($cellData); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 3c2b4f244a..d9b60146e7 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -427,6 +427,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } switch ($rel['Type']) { case "$xmlNamespaceBase/theme": + if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) { + break; // issue3770 + } $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; $themeOrderAdditional = count($themeOrderArray); @@ -934,10 +937,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $holdSelected = $docSheet->getSelectedCells(); $cAttrS = (int) ($cAttr['s'] ?? 0); // no style index means 0, it seems - $cell->setXfIndex(isset($styles[$cAttrS]) - ? $cAttrS : 0); + $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0; + $cell->setXfIndex($cAttrS); // issue 3495 - if ($cell->getDataType() === DataType::TYPE_FORMULA) { + if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { $cell->getStyle()->setQuotePrefix(false); } $docSheet->setSelectedCells($holdSelected); @@ -970,6 +973,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet if ($this->readDataOnly === false) { $this->readAutoFilter($xmlSheetNS, $docSheet); + $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'); } $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS); @@ -1420,6 +1424,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy'))); if ($xfrm) { $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); + $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); } if ($outerShdw) { $shadow = $objDrawing->getShadow(); @@ -1514,6 +1520,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cx'))); $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cy'))); $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); + $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); } if ($outerShdw) { $shadow = $objDrawing->getShadow(); @@ -2198,6 +2206,27 @@ private function readAutoFilter( } } + private function readBackgroundImage( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet, + string $relsName + ): void { + if ($xmlSheet && $xmlSheet->picture) { + $id = (string) self::getArrayItem(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id'); + $rels = $this->loadZip($relsName); + foreach ($rels->Relationship as $rel) { + $attrs = $rel->attributes() ?? []; + $rid = (string) ($attrs['Id'] ?? ''); + $target = (string) ($attrs['Target'] ?? ''); + if ($rid === $id && substr($target, 0, 2) === '..') { + $target = 'xl' . substr($target, 2); + $content = $this->getFromZipArchive($this->zip, $target); + $docSheet->setBackgroundImage($content); + } + } + } + } + private function readTables( SimpleXMLElement $xmlSheet, Worksheet $docSheet, diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 789d46ee78..32737f01a6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -497,6 +497,8 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title $caption = []; $titleLayout = null; $titleOverlay = false; + $titleFormula = null; + $titleFont = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($titleDetailKey) { @@ -517,6 +519,9 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title $caption[] = (string) $pt->v; } } + if (isset($chartDetail->strRef->f)) { + $titleFormula = (string) $chartDetail->strRef->f; + } } break; @@ -527,11 +532,24 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title case 'layout': $titleLayout = $this->chartLayoutDetails($chartDetail); + break; + case 'txPr': + if (isset($chartDetail->children($this->aNamespace)->p)) { + $titleFont = $this->parseFont($chartDetail->children($this->aNamespace)->p); + } + break; } } + $title = new Title($caption, $titleLayout, (bool) $titleOverlay); + if (!empty($titleFormula)) { + $title->setCellReference($titleFormula); + } + if ($titleFont !== null) { + $title->setFont($titleFont); + } - return new Title($caption, $titleLayout, (bool) $titleOverlay); + return $title; } private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout @@ -1185,6 +1203,7 @@ private function parseFont(SimpleXMLElement $titleDetailPart): ?Font $fontArray['italic'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i'); $fontArray['underscore'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u'); $fontArray['strikethrough'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike'); + $fontArray['cap'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'cap'); if (isset($titleDetailPart->pPr->defRPr->latin)) { $fontArray['latin'] = self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface'); diff --git a/src/PhpSpreadsheet/RichText/Run.php b/src/PhpSpreadsheet/RichText/Run.php index c2b7156e60..05d90579e1 100644 --- a/src/PhpSpreadsheet/RichText/Run.php +++ b/src/PhpSpreadsheet/RichText/Run.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\RichText; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\Style\Font; class Run extends TextElement implements ITextElement @@ -35,6 +36,15 @@ public function getFont() return $this->font; } + public function getFontOrThrow(): Font + { + if ($this->font === null) { + throw new SpreadsheetException('unexpected null font'); + } + + return $this->font; + } + /** * Set font. * diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 9f2ef0cd89..f7daa45750 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -78,6 +78,11 @@ public static function setChartRenderer(string $rendererClassName): void self::$chartRenderer = $rendererClassName; } + public static function unsetChartRenderer(): void + { + self::$chartRenderer = null; + } + /** * Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use. * diff --git a/src/PhpSpreadsheet/Shared/Drawing.php b/src/PhpSpreadsheet/Shared/Drawing.php index edaad86153..f7ce022c51 100644 --- a/src/PhpSpreadsheet/Shared/Drawing.php +++ b/src/PhpSpreadsheet/Shared/Drawing.php @@ -110,7 +110,7 @@ public static function pixelsToPoints($pixelValue): float /** * Convert points to pixels. * - * @param int $pointValue Value in points + * @param float|int $pointValue Value in points * * @return int Value in pixels */ diff --git a/src/PhpSpreadsheet/Shared/File.php b/src/PhpSpreadsheet/Shared/File.php index 737a6eb591..e5e470e3bc 100644 --- a/src/PhpSpreadsheet/Shared/File.php +++ b/src/PhpSpreadsheet/Shared/File.php @@ -94,9 +94,9 @@ public static function realpath(string $filename): string $pathArray = explode('/', $filename); while (in_array('..', $pathArray) && $pathArray[0] != '..') { $iMax = count($pathArray); - for ($i = 0; $i < $iMax; ++$i) { - if ($pathArray[$i] == '..' && $i > 0) { - unset($pathArray[$i], $pathArray[$i - 1]); + for ($i = 1; $i < $iMax; ++$i) { + if ($pathArray[$i] == '..') { + array_splice($pathArray, $i - 1, 2); break; } diff --git a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php index 3c4e03c5a4..f95c9025fd 100644 --- a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php +++ b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php @@ -161,8 +161,7 @@ public function stream_seek($offset, $whence): bool // @codingStandardsIgnoreLin $this->pos = $offset; } elseif ($whence == SEEK_CUR && -$offset <= $this->pos) { $this->pos += $offset; - // @phpstan-ignore-next-line - } elseif ($whence == SEEK_END && -$offset <= count($this->data)) { + } elseif ($whence == SEEK_END && -$offset <= count($this->data)) { // @phpstan-ignore-line $this->pos = strlen($this->data) + $offset; } else { return false; diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index fa8cbcca7a..bc1a103df6 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -701,7 +701,7 @@ public function getSheetByName($worksheetName) { $worksheetCount = count($this->workSheetCollection); for ($i = 0; $i < $worksheetCount; ++$i) { - if ($this->workSheetCollection[$i]->getTitle() === trim($worksheetName, "'")) { + if (strcasecmp($this->workSheetCollection[$i]->getTitle(), trim($worksheetName, "'")) === 0) { return $this->workSheetCollection[$i]; } } diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 09a24d10ab..ea26b8ce64 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -13,6 +13,13 @@ class Font extends Supervisor const UNDERLINE_SINGLE = 'single'; const UNDERLINE_SINGLEACCOUNTING = 'singleAccounting'; + const CAP_ALL = 'all'; + const CAP_SMALL = 'small'; + const CAP_NONE = 'none'; + private const VALID_CAPS = [self::CAP_ALL, self::CAP_SMALL, self::CAP_NONE]; + + protected ?string $cap = null; + /** * Font Name. * @@ -236,6 +243,9 @@ public function applyFromArray(array $styleArray): static if (isset($styleArray['scheme'])) { $this->setScheme($styleArray['scheme']); } + if (isset($styleArray['cap'])) { + $this->setCap($styleArray['cap']); + } } return $this; @@ -795,6 +805,7 @@ public function getHashCode() $this->hashChartColor($this->chartColor), $this->hashChartColor($this->underlineColor), (string) $this->baseLine, + (string) $this->cap, ] ) . __CLASS__ @@ -806,6 +817,7 @@ protected function exportArray1(): array $exportedArray = []; $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'cap', $this->getCap()); $this->exportArray2($exportedArray, 'chartColor', $this->getChartColor()); $this->exportArray2($exportedArray, 'color', $this->getColor()); $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); @@ -847,4 +859,23 @@ public function setScheme(string $scheme): self return $this; } + + /** + * Set capitalization attribute. If not one of the permitted + * values (all, small, or none), set it to null. + * This will be honored only for the font for chart titles. + * None is distinguished from null because null will inherit + * the current value, whereas 'none' will override it. + */ + public function setCap(string $cap): self + { + $this->cap = in_array($cap, self::VALID_CAPS, true) ? $cap : null; + + return $this; + } + + public function getCap(): ?string + { + return $this->cap; + } } diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 6e735f1265..0194d49f7f 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -131,6 +131,10 @@ class BaseDrawing implements IComparable */ protected $rotation = 0; + protected bool $flipVertical = false; + + protected bool $flipHorizontal = false; + /** * Shadow. */ @@ -556,4 +560,28 @@ public static function checkGd(): void // @codeCoverageIgnoreEnd } } + + public function setFlipHorizontal(bool $flipHorizontal): self + { + $this->flipHorizontal = $flipHorizontal; + + return $this; + } + + public function getFlipHorizontal(): bool + { + return $this->flipHorizontal; + } + + public function setFlipVertical(bool $flipVertical): self + { + $this->flipVertical = $flipVertical; + + return $this; + } + + public function getFlipVertical(): bool + { + return $this->flipVertical; + } } diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index d6e5582809..9ce7652690 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -81,14 +81,14 @@ public static function validateCellRange(AddressRange|array|string $cellRange): if (is_array($cellRange)) { switch (count($cellRange)) { - case 2: + case 4: $from = [$cellRange[0], $cellRange[1]]; - $to = [$cellRange[0], $cellRange[1]]; + $to = [$cellRange[2], $cellRange[3]]; break; - case 4: + case 2: $from = [$cellRange[0], $cellRange[1]]; - $to = [$cellRange[2], $cellRange[3]]; + $to = [$cellRange[0], $cellRange[1]]; break; default: diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 8ed1d22eb7..35d3ff866d 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -662,6 +662,16 @@ public function getChartByName($chartName) return false; } + public function getChartByNameOrThrow(string $chartName): Chart + { + $chart = $this->getChartByName($chartName); + if ($chart !== false) { + return $chart; + } + + throw new Exception("Sheet does not have a chart named $chartName."); + } + /** * Refresh column dimensions. * @@ -3879,4 +3889,47 @@ private function getXfIndex(string $coordinate): ?int return $xfIndex; } + + private string $backgroundImage = ''; + + private string $backgroundMime = ''; + + private string $backgroundExtension = ''; + + public function getBackgroundImage(): string + { + return $this->backgroundImage; + } + + public function getBackgroundMime(): string + { + return $this->backgroundMime; + } + + public function getBackgroundExtension(): string + { + return $this->backgroundExtension; + } + + /** + * Set background image. + * Used on read/write for Xlsx. + * Used on write for Html. + * + * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents + */ + public function setBackgroundImage(string $backgroundImage): self + { + $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => '']; + $mime = $imageArray['mime']; + if ($mime !== '') { + $extension = explode('/', $mime); + $extension = $extension[1]; + $this->backgroundImage = $backgroundImage; + $this->backgroundMime = $mime; + $this->backgroundExtension = $extension; + } + + return $this; + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 2b38abfc4e..5df0525f11 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -23,6 +23,7 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; @@ -31,6 +32,10 @@ class Html extends BaseWriter { + private const DEFAULT_CELL_WIDTH_POINTS = 42; + + private const DEFAULT_CELL_WIDTH_PIXELS = 56; + /** * Spreadsheet object. */ @@ -146,6 +151,12 @@ class Html extends BaseWriter */ private $editHtmlCallback; + /** @var BaseDrawing[] */ + private $sheetDrawings; + + /** @var Chart[] */ + private $sheetCharts; + /** * Create a new HTML. */ @@ -475,11 +486,14 @@ public function generateSheetData(): string foreach ($sheets as $sheet) { // Write table header $html .= $this->generateTableHeader($sheet); + $this->sheetCharts = []; + $this->sheetDrawings = []; // Get worksheet dimension [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); [$minCol, $minRow] = Coordinate::indexesFromString($min); [$maxCol, $maxRow] = Coordinate::indexesFromString($max); + $this->extendRowsAndColumns($sheet, $maxCol, $maxRow); [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); @@ -506,8 +520,6 @@ public function generateSheetData(): string $html .= $endTag; } - --$row; - $html .= $this->extendRowsForChartsAndImages($sheet, $row); // Write table footer $html .= $this->generateTableFooter(); @@ -559,78 +571,33 @@ public function generateNavigation(): string return $html; } - /** - * Extend Row if chart is placed after nominal end of row. - * This code should be exercised by sample: - * Chart/32_Chart_read_write_PDF.php. - * - * @param int $row Row to check for charts - */ - private function extendRowsForCharts(Worksheet $worksheet, int $row): array + private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void { - $rowMax = $row; - $colMax = 'A'; - $anyfound = false; if ($this->includeCharts) { foreach ($worksheet->getChartCollection() as $chart) { if ($chart instanceof Chart) { - $anyfound = true; $chartCoordinates = $chart->getTopLeftPosition(); - $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']); - $chartCol = Coordinate::columnIndexFromString($chartTL[0]); + $this->sheetCharts[$chartCoordinates['cell']] = $chart; + $chartTL = Coordinate::indexesFromString($chartCoordinates['cell']); if ($chartTL[1] > $rowMax) { $rowMax = $chartTL[1]; - if ($chartCol > Coordinate::columnIndexFromString($colMax)) { - $colMax = $chartTL[0]; - } + } + if ($chartTL[0] > $colMax) { + $colMax = $chartTL[0]; } } } } - - return [$rowMax, $colMax, $anyfound]; - } - - private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string - { - [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row); - foreach ($worksheet->getDrawingCollection() as $drawing) { - $anyfound = true; - $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates()); - $imageCol = Coordinate::columnIndexFromString($imageTL[0]); + $imageTL = Coordinate::indexesFromString($drawing->getCoordinates()); + $this->sheetDrawings[$drawing->getCoordinates()] = $drawing; if ($imageTL[1] > $rowMax) { $rowMax = $imageTL[1]; - if ($imageCol > Coordinate::columnIndexFromString($colMax)) { - $colMax = $imageTL[0]; - } } - } - - // Don't extend rows if not needed - if ($row === $rowMax || !$anyfound) { - return ''; - } - - $html = ''; - ++$colMax; - ++$row; - while ($row <= $rowMax) { - $html .= ''; - for ($col = 'A'; $col != $colMax; ++$col) { - $htmlx = $this->writeImageInCell($worksheet, $col . $row); - $htmlx .= $this->includeCharts ? $this->writeChartInCell($worksheet, $col . $row) : ''; - if ($htmlx) { - $html .= "$htmlx"; - } else { - $html .= ""; - } + if ($imageTL[0] > $colMax) { + $colMax = $imageTL[0]; } - ++$row; - $html .= '' . PHP_EOL; } - - return $html; } /** @@ -654,19 +621,16 @@ public static function winFileToUrl($filename, bool $mpdf = false) /** * Generate image tag in cell. * - * @param Worksheet $worksheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet * @param string $coordinates Cell coordinates */ - private function writeImageInCell(Worksheet $worksheet, string $coordinates): string + private function writeImageInCell(string $coordinates): string { // Construct HTML $html = ''; // Write images - foreach ($worksheet->getDrawingCollection() as $drawing) { - if ($drawing->getCoordinates() != $coordinates) { - continue; - } + $drawing = $this->sheetDrawings[$coordinates] ?? null; + if ($drawing !== null) { $filedesc = $drawing->getDescription(); $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image'; if ($drawing instanceof Drawing) { @@ -740,29 +704,47 @@ private function writeChartInCell(Worksheet $worksheet, string $coordinates): st $html = ''; // Write charts - foreach ($worksheet->getChartCollection() as $chart) { - if ($chart instanceof Chart) { - $chartCoordinates = $chart->getTopLeftPosition(); - if ($chartCoordinates['cell'] == $coordinates) { - $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; - if (!$chart->render($chartFileName)) { - return ''; - } + $chart = $this->sheetCharts[$coordinates] ?? null; + if ($chart !== null) { + $chartCoordinates = $chart->getTopLeftPosition(); + $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; + $renderedWidth = $chart->getRenderedWidth(); + $renderedHeight = $chart->getRenderedHeight(); + if ($renderedWidth === null || $renderedHeight === null) { + $this->adjustRendererPositions($chart, $worksheet); + } + $title = $chart->getTitle(); + $caption = null; + $filedesc = ''; + if ($title !== null) { + $calculatedTitle = $title->getCalculatedTitle($worksheet->getParent()); + if ($calculatedTitle !== null) { + $caption = $title->getCaption(); + $title->setCaption($calculatedTitle); + } + $filedesc = $title->getCaptionText($worksheet->getParent()); + } + $renderSuccessful = $chart->render($chartFileName); + $chart->setRenderedWidth($renderedWidth); + $chart->setRenderedHeight($renderedHeight); + if (isset($title, $caption)) { + $title->setCaption($caption); + } + if (!$renderSuccessful) { + return ''; + } - $html .= PHP_EOL; - $imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => '']; - $filedesc = $chart->getTitle(); - $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; - $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; - $picture = file_get_contents($chartFileName); - if ($picture !== false) { - $base64 = base64_encode($picture); - $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + $html .= PHP_EOL; + $imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => '']; - $html .= '' . $filedesc . '' . PHP_EOL; - } - unlink($chartFileName); - } + $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; + $picture = file_get_contents($chartFileName); + unlink($chartFileName); + if ($picture !== false) { + $base64 = base64_encode($picture); + $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + + $html .= '' . $filedesc . '' . PHP_EOL; } } @@ -770,6 +752,37 @@ private function writeChartInCell(Worksheet $worksheet, string $coordinates): st return $html; } + private function adjustRendererPositions(Chart $chart, Worksheet $sheet): void + { + $topLeft = $chart->getTopLeftPosition(); + $bottomRight = $chart->getBottomRightPosition(); + $tlCell = $topLeft['cell']; + $brCell = $bottomRight['cell']; + if ($tlCell !== '' && $brCell !== '') { + $tlCoordinate = Coordinate::indexesFromString($tlCell); + $brCoordinate = Coordinate::indexesFromString($brCell); + $totalHeight = 0.0; + $totalWidth = 0.0; + $defaultRowHeight = $sheet->getDefaultRowDimension()->getRowHeight(); + $defaultRowHeight = SharedDrawing::pointsToPixels(($defaultRowHeight >= 0) ? $defaultRowHeight : SharedFont::getDefaultRowHeightByFont($this->defaultFont)); + if ($tlCoordinate[1] <= $brCoordinate[1] && $tlCoordinate[0] <= $brCoordinate[0]) { + for ($row = $tlCoordinate[1]; $row <= $brCoordinate[1]; ++$row) { + $height = $sheet->getRowDimension($row)->getRowHeight('pt'); + $totalHeight += ($height >= 0) ? $height : $defaultRowHeight; + } + $rightEdge = $brCoordinate[2]; + ++$rightEdge; + for ($column = $tlCoordinate[2]; $column !== $rightEdge; ++$column) { + $width = $sheet->getColumnDimension($column)->getWidth(); + $width = ($width < 0) ? self::DEFAULT_CELL_WIDTH_PIXELS : SharedDrawing::cellDimensionToPixels($sheet->getColumnDimension($column)->getWidth(), $this->defaultFont); + $totalWidth += $width; + } + $chart->setRenderedWidth($totalWidth); + $chart->setRenderedHeight($totalHeight); + } + } + } + /** * Generate CSS styles. * @@ -835,6 +848,11 @@ private function buildCssPerSheet(Worksheet $sheet, array &$css): void $css["table.sheet$sheetIndex"]['page-break-inside'] = 'avoid'; $css["table.sheet$sheetIndex"]['break-inside'] = 'avoid'; } + $picture = $sheet->getBackgroundImage(); + if ($picture !== '') { + $base64 = base64_encode($picture); + $css["table.sheet$sheetIndex"]['background-image'] = 'url(data:' . $sheet->getBackgroundMime() . ';base64,' . $base64 . ')'; + } // Build styles // Calculate column widths @@ -844,8 +862,8 @@ private function buildCssPerSheet(Worksheet $sheet, array &$css): void $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1; $column = -1; while ($column++ < $highestColumnIndex) { - $this->columnWidths[$sheetIndex][$column] = 42; // approximation - $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt'; + $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt'; } // col elements, loop through columnDimensions and set width @@ -1396,7 +1414,7 @@ private function generateRowSpans(string $html, int $rowSpan, int $colSpan): str private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void { // Image? - $htmlx = $this->writeImageInCell($worksheet, $coordinate); + $htmlx = $this->writeImageInCell($coordinate); // Chart? $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); // Column start diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index f08c90e86d..fb840cbf7f 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -9,7 +10,6 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -use PhpOffice\PhpSpreadsheet\Writer\Exception; use PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Comment; use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Style; @@ -225,7 +225,7 @@ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void if ($this->getParentWriter()->getPreCalculateFormulas()) { try { $formulaValue = $cell->getCalculatedValue(); - } catch (Exception) { + } catch (CalculationException $e) { // don't do anything } } diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index af456f6d38..3eec0bbdc3 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -749,12 +749,12 @@ private function writeDocumentSummaryInformation(): string $dataSection_Content .= $dataProp['data']['data']; $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']); - /* Condition below can never be true - } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) - $dataSection_Content .= $dataProp['data']['data']; + /* Condition below can never be true + } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + $dataSection_Content .= $dataProp['data']['data']; - $dataSection_Content_Offset += 4 + 8; - */ + $dataSection_Content_Offset += 4 + 8; + */ } else { $dataSection_Content .= $dataProp['data']['data']; diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 1df49fc8e4..e0654aca5a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -365,7 +365,7 @@ public function save($filename, int $flags = 0): void // Add worksheet relationships (drawings, ...) for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { // Add relationships - $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1); + $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1, $zipContent); // Add unparsedLoadedData $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); @@ -410,6 +410,9 @@ public function save($filename, int $flags = 0): void } } } + if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']) && !isset($zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'])) { + $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = ''; + } // Add comment relationship parts $legacy = $unparsedLoadedData['sheets'][$this->spreadSheet->getSheet($i)->getCodeName()]['legacyDrawing'] ?? null; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 9bf65ea5ef..a3f27d1b93 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -99,9 +99,11 @@ public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, mixed $ $objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly()); $objWriter->endElement(); - $objWriter->startElement('c:dispBlanksAs'); - $objWriter->writeAttribute('val', $chart->getDisplayBlanksAs()); - $objWriter->endElement(); + if ($chart->getDisplayBlanksAs() !== '') { + $objWriter->startElement('c:dispBlanksAs'); + $objWriter->writeAttribute('val', $chart->getDisplayBlanksAs()); + $objWriter->endElement(); + } $objWriter->startElement('c:showDLblsOverMax'); $objWriter->writeAttribute('val', '0'); @@ -151,6 +153,9 @@ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void if ($title === null) { return; } + if ($this->writeCalculatedTitle($objWriter, $title)) { + return; + } $objWriter->startElement('c:title'); $objWriter->startElement('c:tx'); @@ -169,8 +174,8 @@ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void $objWriter->endElement(); $caption = $title->getCaption(); - if ((is_array($caption)) && (count($caption) > 0)) { - $caption = $caption[0]; + if (is_array($caption)) { + $caption = $caption[0] ?? ''; } $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); @@ -187,6 +192,60 @@ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void $objWriter->endElement(); } + /** + * Write Calculated Chart Title. + */ + private function writeCalculatedTitle(XMLWriter $objWriter, Title $title): bool + { + $calc = $title->getCalculatedTitle($this->getParentWriter()->getSpreadsheet()); + if (empty($calc)) { + return false; + } + + $objWriter->startElement('c:title'); + $objWriter->startElement('c:tx'); + $objWriter->startElement('c:strRef'); + $objWriter->writeElement('c:f', $title->getCellReference()); + $objWriter->startElement('c:strCache'); + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', '1'); + $objWriter->endElement(); // c:ptCount + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', '0'); + $objWriter->writeElement('c:v', $calc); + $objWriter->endElement(); // c:pt + + $objWriter->endElement(); // c:strCache + $objWriter->endElement(); // c:strRef + $objWriter->endElement(); // c:tx + + $this->writeLayout($objWriter, $title->getLayout()); + + $objWriter->startElement('c:overlay'); + $objWriter->writeAttribute('val', ($title->getOverlay()) ? '1' : '0'); + $objWriter->endElement(); // c:overlay + // c:spPr + + // c:txPr + $labelFont = $title->getFont(); + if ($labelFont !== null) { + $objWriter->startElement('c:txPr'); + + $objWriter->startElement('a:bodyPr'); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $this->writeLabelFont($objWriter, $labelFont, null); + + $objWriter->endElement(); // c:txPr + } + + $objWriter->endElement(); // c:title + + return true; + } + /** * Write Chart Legend. */ @@ -1804,6 +1863,10 @@ private function writeLabelFont(XMLWriter $objWriter, ?Font $labelFont, ?Propert if ($labelFont->getItalic() === true) { $objWriter->writeAttribute('i', '1'); } + $cap = $labelFont->getCap(); + if ($cap !== null) { + $objWriter->writeAttribute('cap', $cap); + } $fontColor = $labelFont->getChartColor(); if ($fontColor !== null) { $this->writeColor($objWriter, $fontColor); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php index 8faa3ffc2d..3357e657f3 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -186,6 +186,13 @@ public function writeContentTypes(Spreadsheet $spreadsheet, $includeCharts = fal } } } + + $bgImage = $spreadsheet->getSheet($i)->getBackgroundImage(); + $mimeType = $spreadsheet->getSheet($i)->getBackgroundMime(); + $extension = $spreadsheet->getSheet($i)->getBackgroundExtension(); + if ($bgImage !== '' && !isset($aMediaContentTypes[$mimeType])) { + $this->writeDefaultContentType($objWriter, $extension, $mimeType); + } } // unparsed defaults diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 6763b508f2..cb8b1c4038 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -294,6 +294,8 @@ public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relati // a:xfrm $objWriter->startElement('a:xfrm'); $objWriter->writeAttribute('rot', (string) SharedDrawing::degreesToAngle($drawing->getRotation())); + self::writeAttributeIf($objWriter, $drawing->getFlipVertical(), 'flipV', '1'); + self::writeAttributeIf($objWriter, $drawing->getFlipHorizontal(), 'flipH', '1'); if ($isTwoCellAnchor) { $objWriter->startElement('a:ext'); $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); @@ -579,4 +581,11 @@ private static function stringEmu(int $pixelValue): string { return (string) SharedDrawing::pixelsToEMU($pixelValue); } + + private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void + { + if ($condition) { + $objWriter->writeAttribute($attr, $val); + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 977dc9abf6..dce5882ce6 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -169,7 +169,7 @@ public function writeWorkbookRelationships(Spreadsheet $spreadsheet) * * @return string XML Output */ - public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1) + public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1, array &$zipContent = []) { // Create XML writer $objWriter = null; @@ -221,6 +221,20 @@ public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\ ); } + $backgroundImage = $worksheet->getBackgroundImage(); + if ($backgroundImage !== '') { + $rId = 'Bg'; + $uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999)); + $relPath = "../media/$uniqueName." . $worksheet->getBackgroundExtension(); + $this->writeRelationship( + $objWriter, + $rId, + Namespaces::IMAGE, + $relPath + ); + $zipContent["xl/media/$uniqueName." . $worksheet->getBackgroundExtension()] = $backgroundImage; + } + // Write hyperlink relationships? $i = 1; foreach ($worksheet->getHyperlinkCollection() as $hyperlink) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 7efaaf9396..de9cc49e7e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -139,6 +139,9 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, array $string // IgnoredErrors $this->writeIgnoredErrors($objWriter); + // BackgroundImage must come after ignored, before table + $this->writeBackgroundImage($objWriter, $worksheet); + // Table $this->writeTable($objWriter, $worksheet); @@ -1042,6 +1045,9 @@ private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $ private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void { $tableCount = $worksheet->getTableCollection()->count(); + if ($tableCount === 0) { + return; + } $objWriter->startElement('tableParts'); $objWriter->writeAttribute('count', (string) $tableCount); @@ -1055,6 +1061,18 @@ private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $works $objWriter->endElement(); } + /** + * Write Background Image. + */ + private function writeBackgroundImage(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void + { + if ($worksheet->getBackgroundImage() !== '') { + $objWriter->startElement('picture'); + $objWriter->writeAttribute('r:id', 'rIdBg'); + $objWriter->endElement(); + } + } + /** * Write PageSetup. */ @@ -1098,19 +1116,31 @@ private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $w private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void { // headerFooter + $headerFooter = $worksheet->getHeaderFooter(); + $oddHeader = $headerFooter->getOddHeader(); + $oddFooter = $headerFooter->getOddFooter(); + $evenHeader = $headerFooter->getEvenHeader(); + $evenFooter = $headerFooter->getEvenFooter(); + $firstHeader = $headerFooter->getFirstHeader(); + $firstFooter = $headerFooter->getFirstFooter(); + if ("$oddHeader$oddFooter$evenHeader$evenFooter$firstHeader$firstFooter" === '') { + return; + } + $objWriter->startElement('headerFooter'); $objWriter->writeAttribute('differentOddEven', ($worksheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false')); $objWriter->writeAttribute('differentFirst', ($worksheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false')); $objWriter->writeAttribute('scaleWithDoc', ($worksheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false')); $objWriter->writeAttribute('alignWithMargins', ($worksheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false')); - $objWriter->writeElement('oddHeader', $worksheet->getHeaderFooter()->getOddHeader()); - $objWriter->writeElement('oddFooter', $worksheet->getHeaderFooter()->getOddFooter()); - $objWriter->writeElement('evenHeader', $worksheet->getHeaderFooter()->getEvenHeader()); - $objWriter->writeElement('evenFooter', $worksheet->getHeaderFooter()->getEvenFooter()); - $objWriter->writeElement('firstHeader', $worksheet->getHeaderFooter()->getFirstHeader()); - $objWriter->writeElement('firstFooter', $worksheet->getHeaderFooter()->getFirstFooter()); - $objWriter->endElement(); + self::writeElementIf($objWriter, $oddHeader !== '', 'oddHeader', $oddHeader); + self::writeElementIf($objWriter, $oddFooter !== '', 'oddFooter', $oddFooter); + self::writeElementIf($objWriter, $evenHeader !== '', 'evenHeader', $evenHeader); + self::writeElementIf($objWriter, $evenFooter !== '', 'evenFooter', $evenFooter); + self::writeElementIf($objWriter, $firstHeader !== '', 'firstHeader', $firstHeader); + self::writeElementIf($objWriter, $firstFooter !== '', 'firstFooter', $firstFooter); + + $objWriter->endElement(); // headerFooter } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PiTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PiTest.php new file mode 100644 index 0000000000..130f02f980 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PiTest.php @@ -0,0 +1,32 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + if ($number !== null) { + $sheet->getCell('A1')->setValue($number); + } + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=PI()'); + } else { + $sheet->getCell('B1')->setValue('=PI(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public static function providerPI(): array + { + return require 'tests/data/Calculation/MathTrig/PI.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php index 1f1d85ee35..e1a527496f 100644 --- a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php @@ -300,6 +300,41 @@ public static function providerGetRangeBoundaries(): array return require 'tests/data/CellGetRangeBoundaries.php'; } + /** + * @dataProvider providerCoordinateIsInsideRange + */ + public static function testCoordinateIsInsideRange(bool $expectedResult, string $range, string $coordinate): void + { + $result = Coordinate::coordinateIsInsideRange($range, $coordinate); + self::assertEquals($result, $expectedResult); + } + + public static function providerCoordinateIsInsideRange(): array + { + return require 'tests/data/Cell/CoordinateIsInsideRange.php'; + } + + /** + * @dataProvider providerCoordinateIsInsideRangeException + */ + public static function testCoordinateIsInsideRangeException(string $expectedResult, string $range, string $coordinate): void + { + try { + Coordinate::coordinateIsInsideRange($range, $coordinate); + } catch (\Exception $e) { + self::assertInstanceOf(Exception::class, $e); + self::assertEquals($e->getMessage(), $expectedResult); + + return; + } + self::fail('An expected exception has not been raised.'); + } + + public static function providerCoordinateIsInsideRangeException(): array + { + return require 'tests/data/Cell/CoordinateIsInsideRangeException.php'; + } + /** * @dataProvider providerExtractAllCellReferencesInRange */ diff --git a/tests/PhpSpreadsheetTests/Chart/ChartsByNameTest.php b/tests/PhpSpreadsheetTests/Chart/ChartsByNameTest.php new file mode 100644 index 0000000000..a3583672f8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ChartsByNameTest.php @@ -0,0 +1,97 @@ +getActiveSheet(); + $sheet->setTitle('Only Sheet'); + $sheet->fromArray( + [ + ['Some Title'], + [], + [null, null, 'Data'], + [null, 'L1', 1.3], + [null, 'L2', 1.3], + [null, 'L3', 2.3], + [null, 'L4', 1.6], + [null, 'L5', 1.5], + [null, 'L6', 1.4], + [null, 'L7', 2.2], + [null, 'L8', 1.8], + [null, 'L9', 1.1], + [null, 'L10', 1.8], + [null, 'L11', 1.6], + [null, 'L12', 2.7], + [null, 'L13', 2.2], + [null, 'L14', 1.3], + ] + ); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4', null, 1), // 2010 + ]; + // Set the X-Axis Labels + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4:$B$17'), + ]; + // Set the Data values for each data series we want to plot + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, '\'Only Sheet\'!$C$4:$C$17'), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + DataSeries::GROUPING_STANDARD, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + + // Create the chart + $chart = new Chart( + name: 'namedchart1', + plotArea: $plotArea, + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('G7'); + $chart->setBottomRightPosition('N21'); + // Add the chart to the worksheet + $sheet->addChart($chart); + $sheet->setSelectedCells('D1'); + self::assertSame($chart, $sheet->getChartByName('namedchart1')); + self::assertSame($chart, $sheet->getChartByNameOrThrow('namedchart1')); + self::assertFalse($sheet->getChartByName('namedchart2')); + + try { + $sheet->getChartByNameOrThrow('namedchart2'); + $exceptionRaised = false; + } catch (SpreadsheetException $e) { + self::assertSame('Sheet does not have a chart named namedchart2.', $e->getMessage()); + $exceptionRaised = true; + } + + self::assertTrue($exceptionRaised); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php b/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php new file mode 100644 index 0000000000..2546d60de6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php @@ -0,0 +1,141 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testDynamicTitle(): void + { + // based on samples/templates/issue.3797.2007.xlsx + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Only Sheet'); + $sheet->fromArray( + [ + ['Some Title'], + [], + [null, null, 'Data'], + [null, 'L1', 1.3], + [null, 'L2', 1.3], + [null, 'L3', 2.3], + [null, 'L4', 1.6], + [null, 'L5', 1.5], + [null, 'L6', 1.4], + [null, 'L7', 2.2], + [null, 'L8', 1.8], + [null, 'L9', 1.1], + [null, 'L10', 1.8], + [null, 'L11', 1.6], + [null, 'L12', 2.7], + [null, 'L13', 2.2], + [null, 'L14', 1.3], + ] + ); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4', null, 1), // 2010 + ]; + // Set the X-Axis Labels + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4:$B$17'), + ]; + // Set the Data values for each data series we want to plot + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, '\'Only Sheet\'!$C$4:$C$17'), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + DataSeries::GROUPING_STANDARD, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + $title = new Title(); + $title->setCellReference('\'Only Sheet\'!$A$1'); + $font = new Font(); + $font->setCap(Font::CAP_ALL); + $title->setFont($font); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + null, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null, // yAxisLabel + null, // xAxis + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('G7'); + $chart->setBottomRightPosition('N21'); + // Add the chart to the worksheet + $sheet->addChart($chart); + $sheet->setSelectedCells('D1'); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $rsheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $original = $chart2->getTitle()?->getCalculatedTitle($reloadedSpreadsheet); + self::assertSame('Some Title', $original); + $rsheet->getCell('A1')->setValue('Changed Title'); + self::assertNotNull($chart2->getTitle()); + self::assertSame('Changed Title', $chart2->getTitle()->getCalculatedTitle($reloadedSpreadsheet)); + self::assertSame(Font::CAP_ALL, $chart2->getTitle()->getFont()?->getCap()); + + $writer = new Html($reloadedSpreadsheet); + Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); + $writer->setIncludeCharts(true); + $content = $writer->generateHtmlAll(); + self::assertStringContainsString('alt="Changed Title"', $content); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/PlotAreaTest.php b/tests/PhpSpreadsheetTests/Chart/PlotAreaTest.php new file mode 100644 index 0000000000..819e41cec8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/PlotAreaTest.php @@ -0,0 +1,48 @@ +getPlotAreaOrThrow()); + } + + public function testNoPlotArea(): void + { + $chart = new Chart('chart1'); + $this->expectException(ChartException::class); + $this->expectExceptionMessage('Chart has no PlotArea'); + $chart->getPlotAreaOrThrow(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php index ae305d42f0..191aa9d84b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php @@ -65,7 +65,7 @@ public static function providerIssue2232(): array /** * @dataProvider providerIssue2232locale */ - public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $preserveBoolString, mixed $b4Value, mixed $b5Value): void + public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $preserveBoolString, mixed $b2Value, mixed $b3Value, mixed $b4Value, mixed $b5Value): void { if ($useStringBinder) { $binder = new StringValueBinder(); @@ -81,6 +81,8 @@ public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $ $filename = 'tests/data/Reader/CSV/issue.2232.csv'; $spreadsheet = $reader->load($filename); $sheet = $spreadsheet->getActiveSheet(); + self::assertSame($b2Value, $sheet->getCell('B2')->getValue()); + self::assertSame($b3Value, $sheet->getCell('B3')->getValue()); self::assertSame($b4Value, $sheet->getCell('B4')->getValue()); self::assertSame($b5Value, $sheet->getCell('B5')->getValue()); $spreadsheet->disconnectWorksheets(); @@ -89,10 +91,9 @@ public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $ public static function providerIssue2232locale(): array { return [ - [true, true, 'Faux', 'Vrai'], - [true, true, 'Faux', 'Vrai'], - [false, false, false, true], - [false, false, false, true], + 'string binder preserve boolean string' => [true, true, 'FaLSe', 'tRUE', 'Faux', 'Vrai'], + 'string binder convert boolean string' => [true, false, false, true, false, true], + 'default binder' => [false, null, false, true, false, true], ]; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Slk/SlkSharedFormulasTest.php b/tests/PhpSpreadsheetTests/Reader/Slk/SlkSharedFormulasTest.php new file mode 100644 index 0000000000..15af9f8db8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Slk/SlkSharedFormulasTest.php @@ -0,0 +1,38 @@ +load($testbook); + $sheet = $spreadsheet->getActiveSheet(); + $range = 'A1:' . $sheet->getHighestDataColumn() . $sheet->getHighestDataRow(); + $values = $sheet->RangeToArray($range, null, false, false, false, false); // just get values, don't calculate + $expected = [ + [1, 10, 100, 101, 102], + ['=A1+1', '=B1+1', '=C1+1', '=D1+1', '=E1+1'], + ['=A2+1', '=B2+1', '=C2+1', '=D2+1', '=E2+1'], + ['=A3+1', '=B3+1', '=C3+1', '=D3+1', '=E3+1'], + ['=A4+1', '=B4+1', '=C4+1', '=D4+1', '=E4+1'], + ]; + self::assertSame($expected, $values); + $calcValues = $sheet->RangeToArray($range, null, true, false, false, false); // get calculated values + $expectedCalc = [ + [1, 10, 100, 101, 102], + [2, 11, 101, 102, 103], + [3, 12, 102, 103, 104], + [4, 13, 103, 104, 105], + [5, 14, 104, 105, 106], + ]; + self::assertSame($expectedCalc, $calcValues); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php new file mode 100644 index 0000000000..9f8c3266c9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php @@ -0,0 +1,88 @@ +tempfile !== '') { + unlink($this->tempfile); + $this->tempfile = ''; + } + } + + public function readCharts(XlsxReader $reader): void + { + $reader->setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testReadWithoutCharts(): void + { + $reader = new XlsxReader(); + //$this->readCharts($reader); // Commented out - don't want to read charts. + $spreadsheet = $reader->load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(0, $charts); + $this->tempfile = File::temporaryFileName(); + $writer = new XlsxWriter($spreadsheet); + $this->writeCharts($writer); + $writer->save($this->tempfile); + $spreadsheet->disconnectWorksheets(); + $file = 'zip://'; + $file .= $this->tempfile; + $file .= '#xl/worksheets/_rels/sheet1.xml.rels'; + $data = (string) file_get_contents($file); + // PhpSpreadsheet still generates this target even though charts aren't included + self::assertStringContainsString('Target="../drawings/drawing1.xml"', $data); + $file = 'zip://'; + $file .= $this->tempfile; + $file .= '#xl/drawings/drawing1.xml'; + $data = file_get_contents($file); + self::assertSame('', $data); // fake file because rels needs it + } + + public function testReadWithCharts(): void + { + $reader = new XlsxReader(); + $this->readCharts($reader); + $spreadsheet = $reader->load(self::$testbook); + $xsheet = $spreadsheet->getActiveSheet(); + $xcharts = $xsheet->getChartCollection(); + self::assertCount(1, $xcharts); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $xsheet->getChartCollection(); + self::assertCount(1, $charts); + // In Excel, a default title ('Chart Title') is shown. + // I can't find that anywhere in the Xml. + self::assertSame('', $charts[0]?->getTitle()?->getCaptionText()); + // Just test anything on the chart. + self::assertSame($sheet->getCell('B2')->getValue(), $charts[0]->getPlotArea()?->getPlotGroup()[0]->getPlotValues()[0]->getDataValues()[0]); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3770Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3770Test.php new file mode 100644 index 0000000000..469c02e513 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3770Test.php @@ -0,0 +1,47 @@ +load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + // Assert anything to confirm read succeeded + self::assertSame('Универсальный передаточный документ', $sheet->getCell('A1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3807Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3807Test.php new file mode 100644 index 0000000000..db98262ebe --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3807Test.php @@ -0,0 +1,36 @@ +load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + // Assert anything to confirm read succeeded + self::assertSame(1, $sheet->getCell('B1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue731Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue731Test.php new file mode 100644 index 0000000000..929320c55c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue731Test.php @@ -0,0 +1,38 @@ +load(self::$testbook); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $reloadedSheet = $reloadedSpreadsheet->getActiveSheet(); + $expected = [ + [0, false, false], + [90, false, false], + [270, false, false], + [0, false, true], + [0, true, false], + [20, false, false], + [20, false, true], + [0, true, true], + ]; + $actual = []; + foreach ($reloadedSheet->getDrawingCollection() as $drawing) { + $actual[] = [$drawing->getRotation(), $drawing->getFlipHorizontal(), $drawing->getFlipVertical()]; + } + self::assertSame($expected, $actual); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/RichTextTest.php b/tests/PhpSpreadsheetTests/RichTextTest.php index 3ebd099c0e..513dcf7a9d 100644 --- a/tests/PhpSpreadsheetTests/RichTextTest.php +++ b/tests/PhpSpreadsheetTests/RichTextTest.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\TextElement; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -48,4 +49,20 @@ public function testTextElements(): void self::assertSame([['ABC', '-3.5']], $sheet->toArray()); $spreadsheet->disconnectWorksheets(); } + + public function testNullFont(): void + { + $richText = new RichText(); + $textRun = $richText->createTextRun('hello'); + self::assertNotNull($textRun->getFontOrThrow()); + $textRun->setFont(null); + + try { + $textRun->getFontOrThrow(); + $foundFont = true; + } catch (SpreadsheetException $e) { + $foundFont = false; + } + self::assertFalse($foundFont, 'expected exception not received'); + } } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php index fe4e986439..12fe4abcb1 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -70,11 +70,22 @@ public function testAddSheetDuplicateTitle(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage("Workbook already contains a worksheet named 'someSheet2'. Rename this worksheet first."); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); $spreadsheet->addSheet($sheet); } + public function testAddSheetDuplicateTitleWithDifferentCase(): void + { + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(Exception::class); + $this->expectExceptionMessage("Workbook already contains a worksheet named 'SomeSheet2'. Rename this worksheet first."); + $sheet = new Worksheet(); + $sheet->setTitle('SomeSheet2'); + $spreadsheet->addSheet($sheet); + } + public function testAddSheetNoAdjustActive(): void { $spreadsheet = $this->getSpreadsheet(); @@ -101,6 +112,7 @@ public function testRemoveSheetIndexTooHigh(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('You tried to remove a sheet by the out of bounds index: 4. The actual number of sheets is 3.'); $spreadsheet->removeSheetByIndex(4); } @@ -126,6 +138,7 @@ public function testGetSheetIndexTooHigh(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Your requested sheet index: 4 is out of bounds. The actual number of sheets is 3.'); $spreadsheet->getSheet(4); } @@ -133,6 +146,7 @@ public function testGetIndexNonExistent(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Sheet does not exist.'); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); $spreadsheet->getIndex($sheet); @@ -178,6 +192,7 @@ public function testSetActiveSheetIndexTooHigh(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('You tried to set a sheet active by the out of bounds index: 4. The actual number of sheets is 3.'); $spreadsheet->setActiveSheetIndex(4); } @@ -185,6 +200,7 @@ public function testSetActiveSheetNoSuchName(): void { $spreadsheet = $this->getSpreadsheet(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Workbook does not contain sheet:unknown'); $spreadsheet->setActiveSheetIndexByName('unknown'); } @@ -213,6 +229,7 @@ public function testAddExternal(): void public function testAddExternalDuplicateName(): void { $this->expectException(Exception::class); + $this->expectExceptionMessage("Workbook already contains a worksheet named 'someSheet1'. Rename the external sheet first."); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->createSheet()->setTitle('someSheet1'); $sheet->getCell('A1')->setValue(1); diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php new file mode 100644 index 0000000000..df73c9cd86 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php @@ -0,0 +1,31 @@ +getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue(2); + $sheet->getCell('A2')->setValue(3); + $sheet->getCell('B2')->setValue(4); + $imageFile = 'tests/data/Writer/XLSX/backgroundtest.png'; + $image = (string) file_get_contents($imageFile); + $sheet->setBackgroundImage($image); + self::assertSame('image/png', $sheet->getBackgroundMime()); + self::assertSame('png', $sheet->getBackgroundExtension()); + $writer = new Html($spreadsheet); + $header = $writer->generateHTMLHeader(true); + self::assertStringContainsString('table.sheet0 { background-image:url(data:image/png;base64,', $header); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php new file mode 100644 index 0000000000..73a32db3ae --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php @@ -0,0 +1,50 @@ +getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue(2); + $sheet->getCell('A2')->setValue(3); + $sheet->getCell('B2')->setValue(4); + $imageFile = 'tests/data/Writer/XLSX/backgroundtest.png'; + $image = (string) file_get_contents($imageFile); + $sheet->setBackgroundImage($image); + self::assertSame('image/png', $sheet->getBackgroundMime()); + self::assertSame('png', $sheet->getBackgroundExtension()); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame($image, $reloadedWorksheet->getBackgroundImage()); + self::assertSame('image/png', $reloadedWorksheet->getBackgroundMime()); + self::assertSame('png', $reloadedWorksheet->getBackgroundExtension()); + self::assertSame(2, $reloadedWorksheet->getCell('B1')->getValue()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testInvalidImage(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $imageFile = __FILE__; + $image = (string) file_get_contents($imageFile); + self::assertNotSame('', $image); + $sheet->setBackgroundImage($image); + self::assertSame('', $sheet->getBackgroundImage()); + self::assertSame('', $sheet->getBackgroundMime()); + self::assertSame('', $sheet->getBackgroundExtension()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4cd23357de..5b48e70f9b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -32,6 +32,6 @@ function phpunit10ErrorHandler(int $errno, string $errstr, string $filename, int } if (!method_exists(\PHPUnit\Framework\TestCase::class, 'setOutputCallback')) { - ini_set('error_reporting', E_ALL); + ini_set('error_reporting', (string) E_ALL); set_error_handler('phpunit10ErrorHandler'); } diff --git a/tests/data/Calculation/MathTrig/PI.php b/tests/data/Calculation/MathTrig/PI.php new file mode 100644 index 0000000000..317cd1786c --- /dev/null +++ b/tests/data/Calculation/MathTrig/PI.php @@ -0,0 +1,8 @@ + ['exception', 1], +]; diff --git a/tests/data/Cell/CoordinateIsInsideRange.php b/tests/data/Cell/CoordinateIsInsideRange.php new file mode 100644 index 0000000000..c71c712307 --- /dev/null +++ b/tests/data/Cell/CoordinateIsInsideRange.php @@ -0,0 +1,33 @@ + [true, 'Sheet!A1:E20', 'sheet!B4'], + 'apostrophes 1st sheetname not 2nd' => [true, '\'Sheet\'!A1:E20', 'sheet!B4'], + 'apostrophes 2nd sheetname not 1st' => [true, 'Sheet!A1:E20', '\'sheet\'!B4'], + [false, 'Sheet!A1:E20', 'Sheet!F36'], + [true, 'Sheet!$A$1:$E$20', 'Sheet!$B$4'], + [false, 'Sheet!$A$1:$E$20', 'Sheet!$F$36'], + [false, 'Sheet!A1:E20', 'B4'], + [false, 'Sheet!A1:E20', 'F36'], + [false, 'Sheet!$A$1:$E$20', '$B$4'], + [false, 'Sheet!$A$1:$E$20', '$F$36'], + [false, 'A1:E20', 'Sheet!B4'], + [false, 'A1:E20', 'Sheet!F36'], + [false, '$A$1:$E$20', 'Sheet!$B$4'], + [false, '$A$1:$E$20', 'Sheet!$F$36'], + [true, '\'Sheet space\'!A1:E20', '\'Sheet space\'!B4'], + [false, '\'Sheet space\'!A1:E20', '\'Sheet space\'!F36'], + [true, '\'Sheet space\'!$A$1:$E$20', '\'Sheet space\'!$B$4'], + [false, '\'Sheet space\'!$A$1:$E$20', '\'Sheet space\'!$F$36'], +]; diff --git a/tests/data/Cell/CoordinateIsInsideRangeException.php b/tests/data/Cell/CoordinateIsInsideRangeException.php new file mode 100644 index 0000000000..2ec8ca56c2 --- /dev/null +++ b/tests/data/Cell/CoordinateIsInsideRangeException.php @@ -0,0 +1,8 @@ +