diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b722bd84..a86794c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Nothing yet. +- Address Excel Inappropriate Number Format Substitution. [PR #4532](https://github.com/PHPOffice/PhpSpreadsheet/pull/4532) ### Removed @@ -30,6 +30,15 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Html Writer Conditional Formatting Inline Css. [Issue #4539](https://github.com/PHPOffice/PhpSpreadsheet/issues/4539) [PR #4541](https://github.com/PHPOffice/PhpSpreadsheet/pull/4541) +- Do not use htmlspecialchars when formatting XML. [Issue #4537](https://github.com/PHPOffice/PhpSpreadsheet/issues/4537) [PR #4540](https://github.com/PHPOffice/PhpSpreadsheet/pull/4540) +- Writer Html/Pdf support RTL alignment of tables. [Issue #1104](https://github.com/PHPOffice/PhpSpreadsheet/issues/1104) [PR #4535](https://github.com/PHPOffice/PhpSpreadsheet/pull/4535) +- Xlsx Reader use dynamic arrays if spreadsheet did so. [PR #4533](https://github.com/PHPOffice/PhpSpreadsheet/pull/4533) +- Ods Reader Nested table-row. [Issue #4528](https://github.com/PHPOffice/PhpSpreadsheet/issues/4528) [Issue #2507](https://github.com/PHPOffice/PhpSpreadsheet/issues/2507) [PR #4531](https://github.com/PHPOffice/PhpSpreadsheet/pull/4531) +- Recognize application/x-empty mimetype. [Issue #4521](https://github.com/PHPOffice/PhpSpreadsheet/issues/4521) [PR #4524](https://github.com/PHPOffice/PhpSpreadsheet/pull/4524) +- Micro-optimization in getSheetByName. [PR #4499](https://github.com/PHPOffice/PhpSpreadsheet/pull/4499) +- Bug in resizeMatricesExtend. [Issue #4451](https://github.com/PHPOffice/PhpSpreadsheet/issues/4451) [PR #4474](https://github.com/PHPOffice/PhpSpreadsheet/pull/4474) +- Allow Replace of Dummy Function with Custom Function. [PR #4544](https://github.com/PHPOffice/PhpSpreadsheet/pull/4544) +- Preserve 0x0a in Strings if Desired. [Issue #347](https://github.com/PHPOffice/PhpSpreadsheet/issues/347) [PR #4536](https://github.com/PHPOffice/PhpSpreadsheet/pull/4536) ## 2025-06-22 - 4.4.0 diff --git a/composer.lock b/composer.lock index 6044de2da..b478732b8 100644 --- a/composer.lock +++ b/composer.lock @@ -1051,58 +1051,59 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.75.0", + "version": "v3.76.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c" + "reference": "0e3c484cef0ae9314b0f85986a36296087432c40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c", - "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/0e3c484cef0ae9314b0f85986a36296087432c40", + "reference": "0e3c484cef0ae9314b0f85986a36296087432c40", "shasum": "" }, "require": { "clue/ndjson-react": "^1.0", "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", + "composer/xdebug-handler": "^3.0.5", "ext-filter": "*", "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", "fidry/cpu-core-counter": "^1.2", "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", + "react/child-process": "^0.6.6", "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", + "react/promise": "^2.11 || ^3.0", "react/socket": "^1.0", "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", - "symfony/finder": "^5.4 || ^6.4 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", - "symfony/polyfill-mbstring": "^1.31", - "symfony/polyfill-php80": "^1.31", - "symfony/polyfill-php81": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.2", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "symfony/console": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0", + "symfony/polyfill-mbstring": "^1.32", + "symfony/polyfill-php80": "^1.32", + "symfony/polyfill-php81": "^1.32", + "symfony/process": "^5.4.47 || ^6.4.20 || ^7.2", + "symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.6", "infection/infection": "^0.29.14", - "justinrainbow/json-schema": "^5.3 || ^6.2", - "keradus/cli-executor": "^2.1", + "justinrainbow/json-schema": "^5.3 || ^6.4", + "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.7", + "php-coveralls/php-coveralls": "^2.8", "php-cs-fixer/accessible-object": "^1.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", - "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", - "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" + "phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25", + "symfony/polyfill-php84": "^1.32", + "symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1", + "symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1143,7 +1144,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.75.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.76.0" }, "funding": [ { @@ -1151,7 +1152,7 @@ "type": "github" } ], - "time": "2025-03-31T18:40:42+00:00" + "time": "2025-06-30T14:15:06+00:00" }, { "name": "masterminds/html5", @@ -4108,16 +4109,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.0", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186" + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", "shasum": "" }, "require": { @@ -4188,20 +4189,20 @@ "type": "thanks_dev" } ], - "time": "2025-05-11T03:36:00+00:00" + "time": "2025-06-17T22:17:01+00:00" }, { "name": "symfony/console", - "version": "v6.4.20", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36" + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2e4af9c952617cc3f9559ff706aee420a8464c36", - "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36", + "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93", + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93", "shasum": "" }, "require": { @@ -4266,7 +4267,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.20" + "source": "https://github.com/symfony/console/tree/v6.4.23" }, "funding": [ { @@ -4282,20 +4283,20 @@ "type": "tidelift" } ], - "time": "2025-03-03T17:16:38+00:00" + "time": "2025-06-27T19:37:22+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -4308,7 +4309,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4333,7 +4334,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -4349,7 +4350,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/event-dispatcher", @@ -4433,16 +4434,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -4456,7 +4457,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4489,7 +4490,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -4505,7 +4506,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", @@ -4706,7 +4707,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4765,7 +4766,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -4785,7 +4786,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -4843,7 +4844,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -4863,7 +4864,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4924,7 +4925,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -4944,19 +4945,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -5004,7 +5006,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -5020,20 +5022,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -5084,7 +5086,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -5100,11 +5102,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -5160,7 +5162,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, "funding": [ { @@ -5241,16 +5243,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -5268,7 +5270,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5304,7 +5306,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -5320,7 +5322,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/stopwatch", @@ -5386,16 +5388,16 @@ }, { "name": "symfony/string", - "version": "v6.4.15", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", "shasum": "" }, "require": { @@ -5452,7 +5454,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.15" + "source": "https://github.com/symfony/string/tree/v6.4.21" }, "funding": [ { @@ -5468,7 +5470,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:12+00:00" + "time": "2025-04-18T15:23:29+00:00" }, { "name": "tecnickcom/tcpdf", @@ -5619,5 +5621,5 @@ "platform-overrides": { "php": "8.1.99" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/docs/topics/Excel Anomalies.md b/docs/topics/Excel Anomalies.md new file mode 100644 index 000000000..d41337a5f --- /dev/null +++ b/docs/topics/Excel Anomalies.md @@ -0,0 +1,46 @@ +# Excel Anomalies + +This is documentation for some behavior in Excel itself which we +just do not understand, or which may come as a surprise to the user. + +## Date Number Format + +My system short date format is set to `yyyy-mm-dd`. Excel, for a very long time, did not include that amongst its formatting choices for dates, so it needed to be added as a custom format - no big deal. It has recently been added to the list of date formats, but ... + +I used Excel to create a spreadsheet, and included some dates, specifying `yyyy-mm-dd` formatting. When I looked at the resulting spreadsheet, I was surprised to see that Excel had stored the style not as `yyyy-mm-dd`, but rather as builtin style 14 (system short date format). Apparently the fact that the Excel styling matched my system choice was sufficient for it to override my choice! This is an astonishingly user-hostile implementation. Even though there are formats which, by design, "respond to changes in regional date and time settings", and even though the format I selected was not among those, Excel decided it was appropriate to vary the display even when I said I wanted an unvarying format. I assume, but have not confirmed, that this applies to formats other than `yyyy-mm-dd`. + +Note that this is not a problem when using PhpSpreadsheet to set the style, only when you let Excel do it. And, in that case, after a little experimentation, I figured out a format that Excel doesn't sabotage `[Black]yyyy-mm-dd`. + +If you have a spreadsheet that has been altered in this way, it can be fixed with the following PhpSpreadsheet code: +```php + foreach ($spreadsheet->getCellXfCollection() as $style) { + $numberFormat = $style->getNumberFormat(); + // okay to use NumberFormat::SHORT_DATE_INDEX below + if ($numberFormat->getBuiltInFormatCode() === 14) { + $numberFormat->setFormatCode('yyyy-mm-dd'); + } + } +``` +Starting with PhpSpreadsheet 4.5.0, this can be simplified to: +```php + $spreadsheet->replaceBuiltinNumberFormat( + \PhpOffice\PhpSpreadsheet\Style\NumberFormat::SHORT_DATE_INDEX, + 'yyyy-mm-dd' + ); +``` + +## Negative Time Intervals + +You have a time in one cell, and a time in another, and you want to subtract and display the result in `h:mm` format. No problem if the result is positive. But, if it's negative, Excel just fills the cell with `#`. There is a solution of sorts. If you use a 1904 base date (default on Mac), the negative interval will work just fine. Alas, no dice if you use a 1900 base data (default on Windows). No idea why they can't fix that - the existing implementation can't really be something that anybody actually wants. Note that it is *not* safe to change the base date for an existing spreadsheet, so, if this is something you want to do, make sure you change the base date before populating any data. + +## Long-ago Dates + +Excel does not support dates before either 1900-01-01 (Windows default) or 1904-01-01 (Mac default). For the 1900 base year, there is the additional problem that non-existent date 1900-02-29 is squeezed between 1900-02-28 and 1900-03-01. + +## Weird Fractions + +Similar fraction formats have inconsistent results in Excel. For example, if a cell contains the value 1 and the cell's format is `0 0/0`, it will display as `1 0/1`. But, if the cell's format is `? ??/???`, it will display as `1`. See [this issue](https://github.com/PHPOffice/PhpSpreadsheet/issues/3625), which remains open because, in the absence of usable documentation, we aren't sure how to handle things. + +## COUNTIF and Text Cells + +In Excel, COUNTIF appears to ignore text cells, behavior which doesn't seem to be documented anywhere. See [this issue](https://github.com/PHPOffice/PhpSpreadsheet/issues/3802), which remains open because, in the absence of usable documentation, we aren't sure how to handle things. \ No newline at end of file diff --git a/docs/topics/The Dating Game.md b/docs/topics/The Dating Game.md index 5b0c2812f..ed9a47ac3 100644 --- a/docs/topics/The Dating Game.md +++ b/docs/topics/The Dating Game.md @@ -14,20 +14,20 @@ Open/Libre Office and Gnumeric don't have this limitation, and negative date/tim To write a date in a cell using PhpSpreadsheet, we need to calculate the serialized Excel datestamp for that date. Methods to do this are available in the Shared\Date class, which provides a number of methods for conversion between different date options typically used in PHP applications (Unix timestamp, PHP DateTime objects and some recognisable formatted strings) and the Excel serialized value; and vice versa. - Shared\Date::convertIsoDate() - - Converts a date/time in [ISO-8601 standard format](https://en.wikipedia.org/wiki/ISO_8601) to an Excel serialized timestamp + - Converts a date/time in [ISO-8601 standard format](https://en.wikipedia.org/wiki/ISO_8601) to an Excel serialized timestamp - Shared\Date::PHPToExcel() - - Converts a Unix timestamp, a PHP DateTime object, or a recognisable formatted string to an Excel serialized timestamp + - Converts a Unix timestamp, a PHP DateTime object, or a recognisable formatted string to an Excel serialized timestamp - Shared\Date::dateTimeToExcel() - - Converts a Unix timestamp to an Excel serialized timestamp + - Converts a Unix timestamp to an Excel serialized timestamp - Shared\Date::timestampToExcel() - - Converts a PHP DateTime object to an Excel serialized timestamp + - Converts a PHP DateTime object to an Excel serialized timestamp - Shared\Date::formattedPHPToExcel() - - Converts year, month, day, hour, minute, and second to an Excel serialized timestamp + - Converts year, month, day, hour, minute, and second to an Excel serialized timestamp - Shared\Date::excelToDateTimeObject() - - Converts an Excel serialized timestamp to a PHP DateTime object + - Converts an Excel serialized timestamp to a PHP DateTime object - Shared\Date::excelToTimestamp() - - Converts an Excel serialized timestamp to a Unix timestamp. - - The use of Unix timestamps, and therefore this function, is discouraged: they are not Y2038-safe on a 32-bit system, and have no timezone info. + - Converts an Excel serialized timestamp to a Unix timestamp. + - The use of Unix timestamps, and therefore this function, is discouraged: they are not Y2038-safe on a 32-bit system, and have no timezone info. We probably also want to set the number format mask for the cell so that it will be displayed as a human-readable date. ```php @@ -184,7 +184,7 @@ MS Excel allows any separator character between hours/minutes/seconds; PhpSpread ### Duration (Elapsed Time) -Excel also supports formatting a value as a duration; a total number of hours, minutes or seconds rather than a time of day. +Excel also supports formatting a value as a duration; a total number of hours, minutes or seconds rather than a time of day. However, please note that negative durations are supported only if using base year 1904 (Mac default). | Code | Description | Displays as | |---------|----------------------------------------------------------------|-------------| diff --git a/docs/topics/defined-names.md b/docs/topics/defined-names.md index d62b06fee..87ee09eea 100644 --- a/docs/topics/defined-names.md +++ b/docs/topics/defined-names.md @@ -545,17 +545,18 @@ $this->spreadsheet->addDefinedName( ### Naming Names The names that you assign to Defined Name must follow the following set of rules: + - The first character of a name must be one of the following characters: - - letter (including UTF-8 letters) - - underscore (`_`) + - letter (including UTF-8 letters) + - underscore (`_`) - Remaining characters in the name can be - - letters (including UTF-8 letters) - - numbers (including UTF-8 numbers) - - periods (`.`) - - underscore characters (`_`) + - letters (including UTF-8 letters) + - numbers (including UTF-8 numbers) + - periods (`.`) + - underscore characters (`_`) - The following are not allowed: - - Space characters are not allowed as part of a name. - - Names can't look like cell addresses, such as A35 or R2C2 + - Space characters are not allowed as part of a name. + - Names can't look like cell addresses, such as A35 or R2C2 - Names are not case sensitive. For example, `North` and `NORTH` are treated as the same name. ### Limitations diff --git a/samples/Pdf/21f_Drawing_mpdf.php b/samples/Pdf/21f_Drawing.php similarity index 78% rename from samples/Pdf/21f_Drawing_mpdf.php rename to samples/Pdf/21f_Drawing.php index 357b4690a..489914c3a 100644 --- a/samples/Pdf/21f_Drawing_mpdf.php +++ b/samples/Pdf/21f_Drawing.php @@ -2,12 +2,10 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; -use PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf; require __DIR__ . '/../Header.php'; -/** @var PhpOffice\PhpSpreadsheet\Helper\Sample $helper */ -require_once __DIR__ . '/Mpdf2.php'; +/** @var PhpOffice\PhpSpreadsheet\Helper\Sample $helper */ $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); @@ -41,11 +39,6 @@ $helper->log('Merge drawing cells for Pdf'); $spreadsheet->mergeDrawingCellsForPdf(); $helper->log('Write to Mpdf'); -$writer = new Mpdf($spreadsheet); -$filename = $helper->getFileName(__FILE__, 'pdf'); -$writer->save($filename); -$helper->log("Saved $filename"); -if (PHP_SAPI !== 'cli') { - echo 'Download ' . basename($filename) . '
'; -} +$helper->write($spreadsheet, __FILE__, ['Mpdf']); + $spreadsheet->disconnectWorksheets(); diff --git a/samples/Pdf/21g_Direction.php b/samples/Pdf/21g_Direction.php new file mode 100644 index 000000000..180fad1a3 --- /dev/null +++ b/samples/Pdf/21g_Direction.php @@ -0,0 +1,28 @@ +getActiveSheet(); + +$sheet1 = $spreadsheet->getActiveSheet(); +$sheet2 = $spreadsheet->createSheet(); +$sheet3 = $spreadsheet->createSheet(); +$cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], +]; +$sheet1->fromArray($cells); +$sheet1->setRightToLeft(true); +$sheet2->fromArray($cells); +$sheet3->fromArray($cells); +$sheet3->setRightToLeft(true); + +$helper->log('Write to html, mpdf, tcpdf'); +// Save +$helper->write($spreadsheet, __FILE__, ['Html', 'Mpdf', 'Tcpdf']); + +$spreadsheet->disconnectWorksheets(); diff --git a/samples/Pdf/21h_DirectionMultiple.php b/samples/Pdf/21h_DirectionMultiple.php new file mode 100644 index 000000000..894dfa3f6 --- /dev/null +++ b/samples/Pdf/21h_DirectionMultiple.php @@ -0,0 +1,36 @@ +getActiveSheet(); + +$sheet1 = $spreadsheet->getActiveSheet(); +$sheet2 = $spreadsheet->createSheet(); +$sheet3 = $spreadsheet->createSheet(); +$cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], +]; +$sheet1->fromArray($cells); +$sheet1->setRightToLeft(true); +$sheet2->fromArray($cells); +$sheet3->fromArray($cells); +$sheet3->setRightToLeft(true); + +$helper->log('Write to html, mpdf'); +// Save +$helper->write( + $spreadsheet, + __FILE__, + ['Html', 'Mpdf'], + writerCallback: function (HtmlWriter $writer): void { + $writer->writeAllSheets(); + } +); + +$spreadsheet->disconnectWorksheets(); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 1a9ccae98..72105814a 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -852,15 +852,15 @@ class Calculation extends CalculationLocale if ($matrix2Columns < $matrix1Columns) { for ($i = 0; $i < $matrix2Rows; ++$i) { /** @var mixed[][] $matrix2 */ - $x = $matrix2[$i][$matrix2Columns - 1]; + $x = ($matrix2Columns === 1) ? $matrix2[$i][0] : null; for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) { $matrix2[$i][$j] = $x; } } } if ($matrix2Rows < $matrix1Rows) { - $x = $matrix2[$matrix2Rows - 1]; - for ($i = 0; $i < $matrix1Rows; ++$i) { + $x = ($matrix2Rows === 1) ? $matrix2[0] : array_fill(0, $matrix2Columns, null); + for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) { $matrix2[$i] = $x; } } @@ -870,15 +870,15 @@ class Calculation extends CalculationLocale if ($matrix1Columns < $matrix2Columns) { for ($i = 0; $i < $matrix1Rows; ++$i) { /** @var mixed[][] $matrix1 */ - $x = $matrix1[$i][$matrix1Columns - 1]; + $x = ($matrix1Columns === 1) ? $matrix1[$i][0] : null; for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) { $matrix1[$i][$j] = $x; } } } if ($matrix1Rows < $matrix2Rows) { - $x = $matrix1[$matrix1Rows - 1]; - for ($i = 0; $i < $matrix2Rows; ++$i) { + $x = ($matrix1Rows === 1) ? $matrix1[0] : array_fill(0, $matrix1Columns, null); + for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) { $matrix1[$i] = $x; } } @@ -2375,7 +2375,7 @@ class Calculation extends CalculationLocale for ($row = 0; $row < $rows; ++$row) { for ($column = 0; $column < $columns; ++$column) { /** @var mixed[][] $operand1 */ - if ($operand1[$row][$column] === null) { + if (($operand1[$row][$column] ?? null) === null) { $operand1[$row][$column] = 0; } elseif (!self::isNumericOrBool($operand1[$row][$column])) { $operand1[$row][$column] = self::makeError($operand1[$row][$column]); @@ -2383,7 +2383,7 @@ class Calculation extends CalculationLocale continue; } /** @var mixed[][] $operand2 */ - if ($operand2[$row][$column] === null) { + if (($operand2[$row][$column] ?? null) === null) { $operand2[$row][$column] = 0; } elseif (!self::isNumericOrBool($operand2[$row][$column])) { $operand1[$row][$column] = self::makeError($operand2[$row][$column]); diff --git a/src/PhpSpreadsheet/Calculation/CalculationBase.php b/src/PhpSpreadsheet/Calculation/CalculationBase.php index 9dc67fbc2..5bef04d7e 100644 --- a/src/PhpSpreadsheet/Calculation/CalculationBase.php +++ b/src/PhpSpreadsheet/Calculation/CalculationBase.php @@ -30,7 +30,10 @@ class CalculationBase public static function addFunction(string $key, array $value): bool { $key = strtoupper($key); - if (array_key_exists($key, FunctionArray::$phpSpreadsheetFunctions)) { + if ( + array_key_exists($key, FunctionArray::$phpSpreadsheetFunctions) + && !self::isDummy($key) + ) { return false; } $value['custom'] = true; @@ -39,6 +42,20 @@ class CalculationBase return true; } + private static function isDummy(string $key): bool + { + // key is already known to exist + $functionCall = FunctionArray::$phpSpreadsheetFunctions[$key]['functionCall'] ?? null; + if (!is_array($functionCall)) { + return false; + } + if (($functionCall[1] ?? '') !== 'DUMMY') { + return false; + } + + return true; + } + public static function removeFunction(string $key): bool { $key = strtoupper($key); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php index 53e845d09..486194e46 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -62,7 +62,7 @@ class Filter return array_filter( array_values($lookupArray), - fn ($index): bool => (bool) $matchArray[$index], + fn ($index): bool => (bool) ($matchArray[$index] ?? null), ARRAY_FILTER_USE_KEY ); } diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index b53d1d1e0..8cf71f9d9 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -278,7 +278,14 @@ class Cell implements Stringable case DataType::TYPE_INLINE: // Rich text $value2 = StringHelper::convertToString($value, true); - $this->value = DataType::checkString(($value instanceof RichText) ? $value : $value2); + // Cells?->Worksheet?->Spreadsheet + $binder = $this->parent?->getParent()?->getParent()?->getValueBinder(); + $preserveCr = false; + if ($binder !== null && method_exists($binder, 'getPreserveCr')) { + /** @var bool */ + $preserveCr = $binder->getPreserveCr(); + } + $this->value = DataType::checkString(($value instanceof RichText) ? $value : $value2, $preserveCr); break; case DataType::TYPE_NUMERIC: diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php index 39969d37a..774a57b23 100644 --- a/src/PhpSpreadsheet/Cell/DataType.php +++ b/src/PhpSpreadsheet/Cell/DataType.php @@ -53,7 +53,7 @@ class DataType * * @return RichText|string Sanitized value */ - public static function checkString(null|RichText|string $textValue): RichText|string + public static function checkString(null|RichText|string $textValue, bool $preserveCr = false): RichText|string { if ($textValue instanceof RichText) { // TODO: Sanitize Rich-Text string (max. character count is 32,767) @@ -64,7 +64,9 @@ class DataType $textValue = StringHelper::substring((string) $textValue, 0, self::MAX_STRING_LENGTH); // we require that newline is represented as "\n" in core, not as "\r\n" or "\r" - $textValue = str_replace(["\r\n", "\r"], "\n", $textValue); + if (!$preserveCr) { + $textValue = str_replace(["\r\n", "\r"], "\n", $textValue); + } return $textValue; } diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index 10c5c93c5..0d7f286f7 100644 --- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -108,4 +108,18 @@ class DefaultValueBinder implements IValueBinder return DataType::TYPE_STRING; } + + protected bool $preserveCr = false; + + public function getPreserveCr(): bool + { + return $this->preserveCr; + } + + public function setPreserveCr(bool $preserveCr): self + { + $this->preserveCr = $preserveCr; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index 783c4f00c..21d6a3cff 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -605,6 +605,7 @@ class Csv extends BaseReader 'text/csv', 'text/plain', 'inode/x-empty', + 'application/x-empty', // has now replaced previous 'text/html', ]; diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index af831cf46..e63c05327 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -532,6 +532,9 @@ class Html extends BaseReader $sheet->setShowGridlines(in_array('gridlines', $classes, true)); $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true)); } + if ('rtl' === ($attributeArray['dir'] ?? '')) { + $sheet->setRightToLeft(true); + } $this->currentColumn = 'A'; $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); $column = $this->setTableStartColumn($column); diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 420ae2f7b..97f6a45b2 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -354,317 +354,90 @@ class Ods extends BaseReader continue; } - $key = $childNode->nodeName; - - // Remove ns from node name - if (str_contains($key, ':')) { - $keyChunks = explode(':', $key); - $key = array_pop($keyChunks); - } + $key = self::extractNodeName($childNode->nodeName); switch ($key) { case 'table-header-rows': - /// TODO :: Figure this out. This is only a partial implementation I guess. - // ($rowData it's not used at all and I'm not sure that PHPExcel - // has an API for this) + case 'table-rows': + $this->processTableHeaderRows( + $childNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-row-group': + $this->processTableRowGroup( + $childNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-header-columns': + case 'table-columns': + $this->processTableHeaderColumns( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-column-group': + $this->processTableColumnGroup( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); -// foreach ($rowData as $keyRowData => $cellData) { -// $rowData = $cellData; -// break; -// } break; case 'table-column': - if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { - $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); - } else { - $rowRepeats = 1; - } - $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); - if (isset($columnWidths[$tableStyleName])) { - $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); - $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex); - for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) { - /** @var string $tableColumnString */ - $spreadsheet->getActiveSheet() - ->getColumnDimension($tableColumnString) - ->setWidth($columnWidth->toUnit('cm'), 'cm'); - ++$tableColumnString; - } - } - $tableColumnIndex += $rowRepeats; + $this->processTableColumn( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); break; case 'table-row': - if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { - $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); - } else { - $rowRepeats = 1; - } - - $columnID = 'A'; - /** @var DOMElement|DOMText $cellData */ - foreach ($childNode->childNodes as $cellData) { - if ($cellData instanceof DOMText) { - continue; // should just be whitespace - } - if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { - if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { - $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); - } else { - $colRepeats = 1; - } - - for ($i = 0; $i < $colRepeats; ++$i) { - ++$columnID; - } - - continue; - } - - // Initialize variables - $formatting = $hyperlink = null; - $hasCalculatedValue = false; - $cellDataFormula = ''; - $cellDataType = ''; - $cellDataRef = ''; - - if ($cellData->hasAttributeNS($tableNs, 'formula')) { - $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula'); - $hasCalculatedValue = true; - } - if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) { - if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) { - $cellDataType = 'array'; - $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned'); - $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned'); - $lastRow = $rowID + $arrayRow - 1; - $lastCol = $columnID; - while ($arrayCol > 1) { - ++$lastCol; - --$arrayCol; - } - $cellDataRef = "$columnID$rowID:$lastCol$lastRow"; - } - } - - // Annotations - $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation'); - - if ($annotation->length > 0 && $annotation->item(0) !== null) { - $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); - $textNodeLength = $textNode->length; - $newLineOwed = false; - for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) { - $textNodeItem = $textNode->item($textNodeIndex); - if ($textNodeItem !== null) { - $text = $this->scanElementForText($textNodeItem); - if ($newLineOwed) { - $spreadsheet->getActiveSheet() - ->getComment($columnID . $rowID) - ->getText() - ->createText("\n"); - } - $newLineOwed = true; - - $spreadsheet->getActiveSheet() - ->getComment($columnID . $rowID) - ->getText() - ->createText($this->parseRichText($text)); - } - } - } - - // Content - - /** @var DOMElement[] $paragraphs */ - $paragraphs = []; - - foreach ($cellData->childNodes as $item) { - /** @var DOMElement $item */ - - // Filter text:p elements - if ($item->nodeName == 'text:p') { - $paragraphs[] = $item; - } - } - - if (count($paragraphs) > 0) { - // Consolidate if there are multiple p records (maybe with spans as well) - $dataArray = []; - - // Text can have multiple text:p and within those, multiple text:span. - // text:p newlines, but text:span does not. - // Also, here we assume there is no text data is span fields are specified, since - // we have no way of knowing proper positioning anyway. - - foreach ($paragraphs as $pData) { - $dataArray[] = $this->scanElementForText($pData); - } - $allCellDataText = implode("\n", $dataArray); - - $type = $cellData->getAttributeNS($officeNs, 'value-type'); - - switch ($type) { - case 'string': - $type = DataType::TYPE_STRING; - $dataValue = $allCellDataText; - - foreach ($paragraphs as $paragraph) { - $link = $paragraph->getElementsByTagNameNS($textNs, 'a'); - if ($link->length > 0 && $link->item(0) !== null) { - $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href'); - } - } - - break; - case 'boolean': - $type = DataType::TYPE_BOOL; - $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false; - - break; - case 'percentage': - $type = DataType::TYPE_NUMERIC; - $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); - - // percentage should always be float - //if (floor($dataValue) == $dataValue) { - // $dataValue = (int) $dataValue; - //} - $formatting = NumberFormat::FORMAT_PERCENTAGE_00; - - break; - case 'currency': - $type = DataType::TYPE_NUMERIC; - $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); - - if (floor($dataValue) == $dataValue) { - $dataValue = (int) $dataValue; - } - $formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER; - - break; - case 'float': - $type = DataType::TYPE_NUMERIC; - $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); - - if (floor($dataValue) == $dataValue) { - if ($dataValue == (int) $dataValue) { - $dataValue = (int) $dataValue; - } - } - - break; - case 'date': - $type = DataType::TYPE_NUMERIC; - $value = $cellData->getAttributeNS($officeNs, 'date-value'); - $dataValue = Date::convertIsoDate($value); - - if ($dataValue != floor($dataValue)) { - $formatting = NumberFormat::FORMAT_DATE_XLSX15 - . ' ' - . NumberFormat::FORMAT_DATE_TIME4; - } else { - $formatting = NumberFormat::FORMAT_DATE_XLSX15; - } - - break; - case 'time': - $type = DataType::TYPE_NUMERIC; - - $timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); - - $dataValue = Date::PHPToExcel( - strtotime( - '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? []) - ) - ); - $formatting = NumberFormat::FORMAT_DATE_TIME4; - - break; - default: - $dataValue = null; - } - } else { - $type = DataType::TYPE_NULL; - $dataValue = null; - } - - if ($hasCalculatedValue) { - $type = DataType::TYPE_FORMULA; - $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); - $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula); - } - - if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { - $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); - } else { - $colRepeats = 1; - } - - if ($type !== null) { // @phpstan-ignore-line - for ($i = 0; $i < $colRepeats; ++$i) { - if ($i > 0) { - ++$columnID; - } - - if ($type !== DataType::TYPE_NULL) { - for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { - $rID = $rowID + $rowAdjust; - - $cell = $spreadsheet->getActiveSheet() - ->getCell($columnID . $rID); - - // Set value - if ($hasCalculatedValue) { - $cell->setValueExplicit($cellDataFormula, $type); - if ($cellDataType === 'array') { - $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]); - } - } elseif ($type !== '' || $dataValue !== null) { - $cell->setValueExplicit($dataValue, $type); - } - - if ($hasCalculatedValue) { - $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC); - } - - // Set other properties - if ($formatting !== null) { - $spreadsheet->getActiveSheet() - ->getStyle($columnID . $rID) - ->getNumberFormat() - ->setFormatCode($formatting); - } else { - $spreadsheet->getActiveSheet() - ->getStyle($columnID . $rID) - ->getNumberFormat() - ->setFormatCode(NumberFormat::FORMAT_GENERAL); - } - - if ($hyperlink !== null) { - if ($hyperlink[0] === '#') { - $hyperlink = 'sheet://' . substr($hyperlink, 1); - } - $cell->getHyperlink() - ->setUrl($hyperlink); - } - } - } - } - } - - // Merged cells - $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet); - - ++$columnID; - } - $rowID += $rowRepeats; + $this->processTableRow( + $childNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); break; } } - $pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); - $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); + $pageSettings->setVisibilityForWorksheet( + $spreadsheet->getActiveSheet(), + $worksheetStyleName + ); + $pageSettings->setPrintSettingsForWorksheet( + $spreadsheet->getActiveSheet(), + $worksheetStyleName + ); ++$worksheetID; } @@ -681,6 +454,491 @@ class Ods extends BaseReader return $spreadsheet; } + private function processTableHeaderRows( + DOMElement $childNode, + string $tableNs, + int &$rowID, + string $worksheetName, + string $officeNs, + string $textNs, + string $xlinkNs, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-row': + $this->processTableRow( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + } + } + } + + private function processTableRowGroup( + DOMElement $childNode, + string $tableNs, + int &$rowID, + string $worksheetName, + string $officeNs, + string $textNs, + string $xlinkNs, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-row': + $this->processTableRow( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-header-rows': + case 'table-rows': + $this->processTableHeaderRows( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-row-group': + $this->processTableRowGroup( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + } + } + } + + private function processTableRow( + DOMElement $childNode, + string $tableNs, + int &$rowID, + string $worksheetName, + string $officeNs, + string $textNs, + string $xlinkNs, + Spreadsheet $spreadsheet + ): void { + if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { + $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); + } else { + $rowRepeats = 1; + } + + $columnID = 'A'; + /** @var DOMElement|DOMText $cellData */ + foreach ($childNode->childNodes as $cellData) { + if ($cellData instanceof DOMText) { + continue; // should just be whitespace + } + if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $colRepeats = 1; + } + + for ($i = 0; $i < $colRepeats; ++$i) { + ++$columnID; + } + + continue; + } + + // Initialize variables + $formatting = $hyperlink = null; + $hasCalculatedValue = false; + $cellDataFormula = ''; + $cellDataType = ''; + $cellDataRef = ''; + + if ($cellData->hasAttributeNS($tableNs, 'formula')) { + $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula'); + $hasCalculatedValue = true; + } + if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) { + if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) { + $cellDataType = 'array'; + $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned'); + $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned'); + $lastRow = $rowID + $arrayRow - 1; + $lastCol = $columnID; + while ($arrayCol > 1) { + ++$lastCol; + --$arrayCol; + } + $cellDataRef = "$columnID$rowID:$lastCol$lastRow"; + } + } + + // Annotations + $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation'); + + if ($annotation->length > 0 && $annotation->item(0) !== null) { + $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); + $textNodeLength = $textNode->length; + $newLineOwed = false; + for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) { + $textNodeItem = $textNode->item($textNodeIndex); + if ($textNodeItem !== null) { + $text = $this->scanElementForText($textNodeItem); + if ($newLineOwed) { + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText("\n"); + } + $newLineOwed = true; + + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText( + $this->parseRichText($text) + ); + } + } + } + + // Content + + /** @var DOMElement[] $paragraphs */ + $paragraphs = []; + + foreach ($cellData->childNodes as $item) { + /** @var DOMElement $item */ + + // Filter text:p elements + if ($item->nodeName == 'text:p') { + $paragraphs[] = $item; + } + } + + if (count($paragraphs) > 0) { + // Consolidate if there are multiple p records (maybe with spans as well) + $dataArray = []; + + // Text can have multiple text:p and within those, multiple text:span. + // text:p newlines, but text:span does not. + // Also, here we assume there is no text data is span fields are specified, since + // we have no way of knowing proper positioning anyway. + + foreach ($paragraphs as $pData) { + $dataArray[] = $this->scanElementForText($pData); + } + $allCellDataText = implode("\n", $dataArray); + + $type = $cellData->getAttributeNS($officeNs, 'value-type'); + + switch ($type) { + case 'string': + $type = DataType::TYPE_STRING; + $dataValue = $allCellDataText; + + foreach ($paragraphs as $paragraph) { + $link = $paragraph->getElementsByTagNameNS($textNs, 'a'); + if ($link->length > 0 && $link->item(0) !== null) { + $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href'); + } + } + + break; + case 'boolean': + $type = DataType::TYPE_BOOL; + $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false; + + break; + case 'percentage': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + // percentage should always be float + //if (floor($dataValue) == $dataValue) { + // $dataValue = (int) $dataValue; + //} + $formatting = NumberFormat::FORMAT_PERCENTAGE_00; + + break; + case 'currency': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + if (floor($dataValue) == $dataValue) { + $dataValue = (int) $dataValue; + } + $formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER; + + break; + case 'float': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + if (floor($dataValue) == $dataValue) { + if ($dataValue == (int) $dataValue) { + $dataValue = (int) $dataValue; + } + } + + break; + case 'date': + $type = DataType::TYPE_NUMERIC; + $value = $cellData->getAttributeNS($officeNs, 'date-value'); + $dataValue = Date::convertIsoDate($value); + + if ($dataValue != floor($dataValue)) { + $formatting = NumberFormat::FORMAT_DATE_XLSX15 + . ' ' + . NumberFormat::FORMAT_DATE_TIME4; + } else { + $formatting = NumberFormat::FORMAT_DATE_XLSX15; + } + + break; + case 'time': + $type = DataType::TYPE_NUMERIC; + + $timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); + + $dataValue = Date::PHPToExcel( + strtotime( + '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? []) + ) + ); + $formatting = NumberFormat::FORMAT_DATE_TIME4; + + break; + default: + $dataValue = null; + } + } else { + $type = DataType::TYPE_NULL; + $dataValue = null; + } + + if ($hasCalculatedValue) { + $type = DataType::TYPE_FORMULA; + $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); + $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula); + } + + if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $colRepeats = 1; + } + + if ($type !== null) { // @phpstan-ignore-line + for ($i = 0; $i < $colRepeats; ++$i) { + if ($i > 0) { + ++$columnID; + } + + if ($type !== DataType::TYPE_NULL) { + for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { + $rID = $rowID + $rowAdjust; + + $cell = $spreadsheet->getActiveSheet() + ->getCell($columnID . $rID); + + // Set value + if ($hasCalculatedValue) { + $cell->setValueExplicit($cellDataFormula, $type); + if ($cellDataType === 'array') { + $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]); + } + } elseif ($type !== '' || $dataValue !== null) { + $cell->setValueExplicit($dataValue, $type); + } + + if ($hasCalculatedValue) { + $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC); + } + + // Set other properties + if ($formatting !== null) { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode($formatting); + } else { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_GENERAL); + } + + if ($hyperlink !== null) { + if ($hyperlink[0] === '#') { + $hyperlink = 'sheet://' . substr($hyperlink, 1); + } + $cell->getHyperlink() + ->setUrl($hyperlink); + } + } + } + } + } + + // Merged cells + $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet); + + ++$columnID; + } + $rowID += $rowRepeats; + } + + private static function extractNodeName(string $key): string + { + // Remove ns from node name + if (str_contains($key, ':')) { + $keyChunks = explode(':', $key); + $key = array_pop($keyChunks); + } + + return $key; + } + + /** + * @param string[] $columnWidths + */ + private function processTableHeaderColumns( + DOMElement $childNode, + string $tableNs, + array $columnWidths, + int &$tableColumnIndex, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-column': + $this->processTableColumn( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + } + } + } + + /** + * @param string[] $columnWidths + */ + private function processTableColumnGroup( + DOMElement $childNode, + string $tableNs, + array $columnWidths, + int &$tableColumnIndex, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-column': + $this->processTableColumn( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-header-columns': + case 'table-columns': + $this->processTableHeaderColumns( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-column-group': + $this->processTableColumnGroup( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + } + } + } + + /** + * @param string[] $columnWidths + */ + private function processTableColumn( + DOMElement $childNode, + string $tableNs, + array $columnWidths, + int &$tableColumnIndex, + Spreadsheet $spreadsheet + ): void { + if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $rowRepeats = 1; + } + $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); + if (isset($columnWidths[$tableStyleName])) { + $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); + $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex); + for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) { + /** @var string $tableColumnString */ + $spreadsheet->getActiveSheet() + ->getColumnDimension($tableColumnString) + ->setWidth($columnWidth->toUnit('cm'), 'cm'); + ++$tableColumnString; + } + } + $tableColumnIndex += $rowRepeats; + } + private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void { $dom = new DOMDocument('1.01', 'UTF-8'); @@ -688,9 +946,7 @@ class Ods extends BaseReader $this->getSecurityScannerOrThrow() ->scan($zip->getFromName('settings.xml')) ); - //$xlinkNs = $dom->lookupNamespaceUri('xlink'); $configNs = (string) $dom->lookupNamespaceUri('config'); - //$oooNs = $dom->lookupNamespaceUri('ooo'); $officeNs = (string) $dom->lookupNamespaceUri('office'); $settings = $dom->getElementsByTagNameNS($officeNs, 'settings') ->item(0); diff --git a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php index 60fc5a125..fb28f398f 100644 --- a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php +++ b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php @@ -27,7 +27,7 @@ class LoadSpreadsheet extends Xls // Initialisations $xls->spreadsheet = $this->newSpreadsheet(); - $xls->spreadsheet->setValueBinder($this->valueBinder); + $xls->spreadsheet->setValueBinder($xls->valueBinder); $xls->spreadsheet->removeSheetByIndex(0); // remove 1st sheet if (!$xls->readDataOnly) { $xls->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index b3e133f78..7daed146e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -449,6 +450,15 @@ class Xlsx extends BaseReader $relTarget = substr($relTarget, 4); } switch ($rel['Type']) { + case "$xmlNamespaceBase/sheetMetadata": + if ($this->fileExistsInArchive($zip, "xl/{$relTarget}")) { + $excel->getCalculationEngine() + ?->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + } + + break; case "$xmlNamespaceBase/theme": if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) { break; // issue3770 diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 045c7b472..323aff408 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -9,19 +9,236 @@ use Stringable; class StringHelper { - /** - * Control characters array. - * - * @var string[] - */ - private static array $controlCharacters = []; + private const CONTROL_CHARACTERS_KEYS = [ + "\x00", + "\x01", + "\x02", + "\x03", + "\x04", + "\x05", + "\x06", + "\x07", + "\x08", + "\x09", + "\x0a", + "\x0b", + "\x0c", + "\x0d", + "\x0e", + "\x0f", + "\x10", + "\x11", + "\x12", + "\x13", + "\x14", + "\x15", + "\x16", + "\x17", + "\x18", + "\x19", + "\x1a", + "\x1b", + "\x1c", + "\x1d", + "\x1e", + "\x1f", + ]; + private const CONTROL_CHARACTERS_VALUES = [ + '_x0000_', + '_x0001_', + '_x0002_', + '_x0003_', + '_x0004_', + '_x0005_', + '_x0006_', + '_x0007_', + '_x0008_', + '_x0009_', + '_x000A_', + '_x000B_', + '_x000C_', + '_x000D_', + '_x000E_', + '_x000F_', + '_x0010_', + '_x0011_', + '_x0012_', + '_x0013_', + '_x0014_', + '_x0015_', + '_x0016_', + '_x0017_', + '_x0018_', + '_x0019_', + '_x001A_', + '_x001B_', + '_x001C_', + '_x001D_', + '_x001E_', + '_x001F_', + ]; /** * SYLK Characters array. - * - * @var string[] */ - private static array $SYLKCharacters = []; + private const SYLK_CHARACTERS = [ + "\x1B 0" => "\x00", + "\x1B 1" => "\x01", + "\x1B 2" => "\x02", + "\x1B 3" => "\x03", + "\x1B 4" => "\x04", + "\x1B 5" => "\x05", + "\x1B 6" => "\x06", + "\x1B 7" => "\x07", + "\x1B 8" => "\x08", + "\x1B 9" => "\x09", + "\x1B :" => "\x0a", + "\x1B ;" => "\x0b", + "\x1B <" => "\x0c", + "\x1B =" => "\x0d", + "\x1B >" => "\x0e", + "\x1B ?" => "\x0f", + "\x1B!0" => "\x10", + "\x1B!1" => "\x11", + "\x1B!2" => "\x12", + "\x1B!3" => "\x13", + "\x1B!4" => "\x14", + "\x1B!5" => "\x15", + "\x1B!6" => "\x16", + "\x1B!7" => "\x17", + "\x1B!8" => "\x18", + "\x1B!9" => "\x19", + "\x1B!:" => "\x1a", + "\x1B!;" => "\x1b", + "\x1B!<" => "\x1c", + "\x1B!=" => "\x1d", + "\x1B!>" => "\x1e", + "\x1B!?" => "\x1f", + "\x1B'?" => "\x7f", + "\x1B(0" => '€', // 128 in CP1252 + "\x1B(2" => '‚', // 130 in CP1252 + "\x1B(3" => 'ƒ', // 131 in CP1252 + "\x1B(4" => '„', // 132 in CP1252 + "\x1B(5" => '…', // 133 in CP1252 + "\x1B(6" => '†', // 134 in CP1252 + "\x1B(7" => '‡', // 135 in CP1252 + "\x1B(8" => 'ˆ', // 136 in CP1252 + "\x1B(9" => '‰', // 137 in CP1252 + "\x1B(:" => 'Š', // 138 in CP1252 + "\x1B(;" => '‹', // 139 in CP1252 + "\x1BNj" => 'Œ', // 140 in CP1252 + "\x1B(>" => 'Ž', // 142 in CP1252 + "\x1B)1" => '‘', // 145 in CP1252 + "\x1B)2" => '’', // 146 in CP1252 + "\x1B)3" => '“', // 147 in CP1252 + "\x1B)4" => '”', // 148 in CP1252 + "\x1B)5" => '•', // 149 in CP1252 + "\x1B)6" => '–', // 150 in CP1252 + "\x1B)7" => '—', // 151 in CP1252 + "\x1B)8" => '˜', // 152 in CP1252 + "\x1B)9" => '™', // 153 in CP1252 + "\x1B):" => 'š', // 154 in CP1252 + "\x1B);" => '›', // 155 in CP1252 + "\x1BNz" => 'œ', // 156 in CP1252 + "\x1B)>" => 'ž', // 158 in CP1252 + "\x1B)?" => 'Ÿ', // 159 in CP1252 + "\x1B*0" => ' ', // 160 in CP1252 + "\x1BN!" => '¡', // 161 in CP1252 + "\x1BN\"" => '¢', // 162 in CP1252 + "\x1BN#" => '£', // 163 in CP1252 + "\x1BN(" => '¤', // 164 in CP1252 + "\x1BN%" => '¥', // 165 in CP1252 + "\x1B*6" => '¦', // 166 in CP1252 + "\x1BN'" => '§', // 167 in CP1252 + "\x1BNH " => '¨', // 168 in CP1252 + "\x1BNS" => '©', // 169 in CP1252 + "\x1BNc" => 'ª', // 170 in CP1252 + "\x1BN+" => '«', // 171 in CP1252 + "\x1B*<" => '¬', // 172 in CP1252 + "\x1B*=" => '­', // 173 in CP1252 + "\x1BNR" => '®', // 174 in CP1252 + "\x1B*?" => '¯', // 175 in CP1252 + "\x1BN0" => '°', // 176 in CP1252 + "\x1BN1" => '±', // 177 in CP1252 + "\x1BN2" => '²', // 178 in CP1252 + "\x1BN3" => '³', // 179 in CP1252 + "\x1BNB " => '´', // 180 in CP1252 + "\x1BN5" => 'µ', // 181 in CP1252 + "\x1BN6" => '¶', // 182 in CP1252 + "\x1BN7" => '·', // 183 in CP1252 + "\x1B+8" => '¸', // 184 in CP1252 + "\x1BNQ" => '¹', // 185 in CP1252 + "\x1BNk" => 'º', // 186 in CP1252 + "\x1BN;" => '»', // 187 in CP1252 + "\x1BN<" => '¼', // 188 in CP1252 + "\x1BN=" => '½', // 189 in CP1252 + "\x1BN>" => '¾', // 190 in CP1252 + "\x1BN?" => '¿', // 191 in CP1252 + "\x1BNAA" => 'À', // 192 in CP1252 + "\x1BNBA" => 'Á', // 193 in CP1252 + "\x1BNCA" => 'Â', // 194 in CP1252 + "\x1BNDA" => 'Ã', // 195 in CP1252 + "\x1BNHA" => 'Ä', // 196 in CP1252 + "\x1BNJA" => 'Å', // 197 in CP1252 + "\x1BNa" => 'Æ', // 198 in CP1252 + "\x1BNKC" => 'Ç', // 199 in CP1252 + "\x1BNAE" => 'È', // 200 in CP1252 + "\x1BNBE" => 'É', // 201 in CP1252 + "\x1BNCE" => 'Ê', // 202 in CP1252 + "\x1BNHE" => 'Ë', // 203 in CP1252 + "\x1BNAI" => 'Ì', // 204 in CP1252 + "\x1BNBI" => 'Í', // 205 in CP1252 + "\x1BNCI" => 'Î', // 206 in CP1252 + "\x1BNHI" => 'Ï', // 207 in CP1252 + "\x1BNb" => 'Ð', // 208 in CP1252 + "\x1BNDN" => 'Ñ', // 209 in CP1252 + "\x1BNAO" => 'Ò', // 210 in CP1252 + "\x1BNBO" => 'Ó', // 211 in CP1252 + "\x1BNCO" => 'Ô', // 212 in CP1252 + "\x1BNDO" => 'Õ', // 213 in CP1252 + "\x1BNHO" => 'Ö', // 214 in CP1252 + "\x1B-7" => '×', // 215 in CP1252 + "\x1BNi" => 'Ø', // 216 in CP1252 + "\x1BNAU" => 'Ù', // 217 in CP1252 + "\x1BNBU" => 'Ú', // 218 in CP1252 + "\x1BNCU" => 'Û', // 219 in CP1252 + "\x1BNHU" => 'Ü', // 220 in CP1252 + "\x1B-=" => 'Ý', // 221 in CP1252 + "\x1BNl" => 'Þ', // 222 in CP1252 + "\x1BN{" => 'ß', // 223 in CP1252 + "\x1BNAa" => 'à', // 224 in CP1252 + "\x1BNBa" => 'á', // 225 in CP1252 + "\x1BNCa" => 'â', // 226 in CP1252 + "\x1BNDa" => 'ã', // 227 in CP1252 + "\x1BNHa" => 'ä', // 228 in CP1252 + "\x1BNJa" => 'å', // 229 in CP1252 + "\x1BNq" => 'æ', // 230 in CP1252 + "\x1BNKc" => 'ç', // 231 in CP1252 + "\x1BNAe" => 'è', // 232 in CP1252 + "\x1BNBe" => 'é', // 233 in CP1252 + "\x1BNCe" => 'ê', // 234 in CP1252 + "\x1BNHe" => 'ë', // 235 in CP1252 + "\x1BNAi" => 'ì', // 236 in CP1252 + "\x1BNBi" => 'í', // 237 in CP1252 + "\x1BNCi" => 'î', // 238 in CP1252 + "\x1BNHi" => 'ï', // 239 in CP1252 + "\x1BNs" => 'ð', // 240 in CP1252 + "\x1BNDn" => 'ñ', // 241 in CP1252 + "\x1BNAo" => 'ò', // 242 in CP1252 + "\x1BNBo" => 'ó', // 243 in CP1252 + "\x1BNCo" => 'ô', // 244 in CP1252 + "\x1BNDo" => 'õ', // 245 in CP1252 + "\x1BNHo" => 'ö', // 246 in CP1252 + "\x1B/7" => '÷', // 247 in CP1252 + "\x1BNy" => 'ø', // 248 in CP1252 + "\x1BNAu" => 'ù', // 249 in CP1252 + "\x1BNBu" => 'ú', // 250 in CP1252 + "\x1BNCu" => 'û', // 251 in CP1252 + "\x1BNHu" => 'ü', // 252 in CP1252 + "\x1B/=" => 'ý', // 253 in CP1252 + "\x1BN|" => 'þ', // 254 in CP1252 + "\x1BNHy" => 'ÿ', // 255 in CP1252 + ]; /** * Decimal separator. @@ -48,185 +265,6 @@ class StringHelper */ private static string $iconvOptions = '//IGNORE//TRANSLIT'; - /** - * Build control characters array. - */ - private static function buildControlCharacters(): void - { - for ($i = 0; $i <= 31; ++$i) { - if ($i != 9 && $i != 10 && $i != 13) { - $find = '_x' . sprintf('%04s', strtoupper(dechex($i))) . '_'; - $replace = chr($i); - self::$controlCharacters[$find] = $replace; - } - } - } - - /** - * Build SYLK characters array. - */ - private static function buildSYLKCharacters(): void - { - self::$SYLKCharacters = [ - "\x1B 0" => chr(0), - "\x1B 1" => chr(1), - "\x1B 2" => chr(2), - "\x1B 3" => chr(3), - "\x1B 4" => chr(4), - "\x1B 5" => chr(5), - "\x1B 6" => chr(6), - "\x1B 7" => chr(7), - "\x1B 8" => chr(8), - "\x1B 9" => chr(9), - "\x1B :" => chr(10), - "\x1B ;" => chr(11), - "\x1B <" => chr(12), - "\x1B =" => chr(13), - "\x1B >" => chr(14), - "\x1B ?" => chr(15), - "\x1B!0" => chr(16), - "\x1B!1" => chr(17), - "\x1B!2" => chr(18), - "\x1B!3" => chr(19), - "\x1B!4" => chr(20), - "\x1B!5" => chr(21), - "\x1B!6" => chr(22), - "\x1B!7" => chr(23), - "\x1B!8" => chr(24), - "\x1B!9" => chr(25), - "\x1B!:" => chr(26), - "\x1B!;" => chr(27), - "\x1B!<" => chr(28), - "\x1B!=" => chr(29), - "\x1B!>" => chr(30), - "\x1B!?" => chr(31), - "\x1B'?" => chr(127), - "\x1B(0" => '€', // 128 in CP1252 - "\x1B(2" => '‚', // 130 in CP1252 - "\x1B(3" => 'ƒ', // 131 in CP1252 - "\x1B(4" => '„', // 132 in CP1252 - "\x1B(5" => '…', // 133 in CP1252 - "\x1B(6" => '†', // 134 in CP1252 - "\x1B(7" => '‡', // 135 in CP1252 - "\x1B(8" => 'ˆ', // 136 in CP1252 - "\x1B(9" => '‰', // 137 in CP1252 - "\x1B(:" => 'Š', // 138 in CP1252 - "\x1B(;" => '‹', // 139 in CP1252 - "\x1BNj" => 'Œ', // 140 in CP1252 - "\x1B(>" => 'Ž', // 142 in CP1252 - "\x1B)1" => '‘', // 145 in CP1252 - "\x1B)2" => '’', // 146 in CP1252 - "\x1B)3" => '“', // 147 in CP1252 - "\x1B)4" => '”', // 148 in CP1252 - "\x1B)5" => '•', // 149 in CP1252 - "\x1B)6" => '–', // 150 in CP1252 - "\x1B)7" => '—', // 151 in CP1252 - "\x1B)8" => '˜', // 152 in CP1252 - "\x1B)9" => '™', // 153 in CP1252 - "\x1B):" => 'š', // 154 in CP1252 - "\x1B);" => '›', // 155 in CP1252 - "\x1BNz" => 'œ', // 156 in CP1252 - "\x1B)>" => 'ž', // 158 in CP1252 - "\x1B)?" => 'Ÿ', // 159 in CP1252 - "\x1B*0" => ' ', // 160 in CP1252 - "\x1BN!" => '¡', // 161 in CP1252 - "\x1BN\"" => '¢', // 162 in CP1252 - "\x1BN#" => '£', // 163 in CP1252 - "\x1BN(" => '¤', // 164 in CP1252 - "\x1BN%" => '¥', // 165 in CP1252 - "\x1B*6" => '¦', // 166 in CP1252 - "\x1BN'" => '§', // 167 in CP1252 - "\x1BNH " => '¨', // 168 in CP1252 - "\x1BNS" => '©', // 169 in CP1252 - "\x1BNc" => 'ª', // 170 in CP1252 - "\x1BN+" => '«', // 171 in CP1252 - "\x1B*<" => '¬', // 172 in CP1252 - "\x1B*=" => '­', // 173 in CP1252 - "\x1BNR" => '®', // 174 in CP1252 - "\x1B*?" => '¯', // 175 in CP1252 - "\x1BN0" => '°', // 176 in CP1252 - "\x1BN1" => '±', // 177 in CP1252 - "\x1BN2" => '²', // 178 in CP1252 - "\x1BN3" => '³', // 179 in CP1252 - "\x1BNB " => '´', // 180 in CP1252 - "\x1BN5" => 'µ', // 181 in CP1252 - "\x1BN6" => '¶', // 182 in CP1252 - "\x1BN7" => '·', // 183 in CP1252 - "\x1B+8" => '¸', // 184 in CP1252 - "\x1BNQ" => '¹', // 185 in CP1252 - "\x1BNk" => 'º', // 186 in CP1252 - "\x1BN;" => '»', // 187 in CP1252 - "\x1BN<" => '¼', // 188 in CP1252 - "\x1BN=" => '½', // 189 in CP1252 - "\x1BN>" => '¾', // 190 in CP1252 - "\x1BN?" => '¿', // 191 in CP1252 - "\x1BNAA" => 'À', // 192 in CP1252 - "\x1BNBA" => 'Á', // 193 in CP1252 - "\x1BNCA" => 'Â', // 194 in CP1252 - "\x1BNDA" => 'Ã', // 195 in CP1252 - "\x1BNHA" => 'Ä', // 196 in CP1252 - "\x1BNJA" => 'Å', // 197 in CP1252 - "\x1BNa" => 'Æ', // 198 in CP1252 - "\x1BNKC" => 'Ç', // 199 in CP1252 - "\x1BNAE" => 'È', // 200 in CP1252 - "\x1BNBE" => 'É', // 201 in CP1252 - "\x1BNCE" => 'Ê', // 202 in CP1252 - "\x1BNHE" => 'Ë', // 203 in CP1252 - "\x1BNAI" => 'Ì', // 204 in CP1252 - "\x1BNBI" => 'Í', // 205 in CP1252 - "\x1BNCI" => 'Î', // 206 in CP1252 - "\x1BNHI" => 'Ï', // 207 in CP1252 - "\x1BNb" => 'Ð', // 208 in CP1252 - "\x1BNDN" => 'Ñ', // 209 in CP1252 - "\x1BNAO" => 'Ò', // 210 in CP1252 - "\x1BNBO" => 'Ó', // 211 in CP1252 - "\x1BNCO" => 'Ô', // 212 in CP1252 - "\x1BNDO" => 'Õ', // 213 in CP1252 - "\x1BNHO" => 'Ö', // 214 in CP1252 - "\x1B-7" => '×', // 215 in CP1252 - "\x1BNi" => 'Ø', // 216 in CP1252 - "\x1BNAU" => 'Ù', // 217 in CP1252 - "\x1BNBU" => 'Ú', // 218 in CP1252 - "\x1BNCU" => 'Û', // 219 in CP1252 - "\x1BNHU" => 'Ü', // 220 in CP1252 - "\x1B-=" => 'Ý', // 221 in CP1252 - "\x1BNl" => 'Þ', // 222 in CP1252 - "\x1BN{" => 'ß', // 223 in CP1252 - "\x1BNAa" => 'à', // 224 in CP1252 - "\x1BNBa" => 'á', // 225 in CP1252 - "\x1BNCa" => 'â', // 226 in CP1252 - "\x1BNDa" => 'ã', // 227 in CP1252 - "\x1BNHa" => 'ä', // 228 in CP1252 - "\x1BNJa" => 'å', // 229 in CP1252 - "\x1BNq" => 'æ', // 230 in CP1252 - "\x1BNKc" => 'ç', // 231 in CP1252 - "\x1BNAe" => 'è', // 232 in CP1252 - "\x1BNBe" => 'é', // 233 in CP1252 - "\x1BNCe" => 'ê', // 234 in CP1252 - "\x1BNHe" => 'ë', // 235 in CP1252 - "\x1BNAi" => 'ì', // 236 in CP1252 - "\x1BNBi" => 'í', // 237 in CP1252 - "\x1BNCi" => 'î', // 238 in CP1252 - "\x1BNHi" => 'ï', // 239 in CP1252 - "\x1BNs" => 'ð', // 240 in CP1252 - "\x1BNDn" => 'ñ', // 241 in CP1252 - "\x1BNAo" => 'ò', // 242 in CP1252 - "\x1BNBo" => 'ó', // 243 in CP1252 - "\x1BNCo" => 'ô', // 244 in CP1252 - "\x1BNDo" => 'õ', // 245 in CP1252 - "\x1BNHo" => 'ö', // 246 in CP1252 - "\x1B/7" => '÷', // 247 in CP1252 - "\x1BNy" => 'ø', // 248 in CP1252 - "\x1BNAu" => 'ù', // 249 in CP1252 - "\x1BNBu" => 'ú', // 250 in CP1252 - "\x1BNCu" => 'û', // 251 in CP1252 - "\x1BNHu" => 'ü', // 252 in CP1252 - "\x1B/=" => 'ý', // 253 in CP1252 - "\x1BN|" => 'þ', // 254 in CP1252 - "\x1BNHy" => 'ÿ', // 255 in CP1252 - ]; - } - /** * Get whether iconv extension is available. */ @@ -258,17 +296,6 @@ class StringHelper return self::$isIconvEnabled; } - private static function buildCharacterSets(): void - { - if (empty(self::$controlCharacters)) { - self::buildControlCharacters(); - } - - if (empty(self::$SYLKCharacters)) { - self::buildSYLKCharacters(); - } - } - /** * Convert from OpenXML escaped control character to PHP control character. * @@ -284,9 +311,7 @@ class StringHelper */ public static function controlCharacterOOXML2PHP(string $textValue): string { - self::buildCharacterSets(); - - return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $textValue); + return str_replace(self::CONTROL_CHARACTERS_VALUES, self::CONTROL_CHARACTERS_KEYS, $textValue); } /** @@ -304,9 +329,7 @@ class StringHelper */ public static function controlCharacterPHP2OOXML(string $textValue): string { - self::buildCharacterSets(); - - return str_replace(array_values(self::$controlCharacters), array_keys(self::$controlCharacters), $textValue); + return str_replace(self::CONTROL_CHARACTERS_KEYS, self::CONTROL_CHARACTERS_VALUES, $textValue); } /** @@ -619,14 +642,12 @@ class StringHelper */ public static function SYLKtoUTF8(string $textValue): string { - self::buildCharacterSets(); - // If there is no escape character in the string there is nothing to do - if (!str_contains($textValue, '')) { + if (!str_contains($textValue, "\x1b")) { return $textValue; } - foreach (self::$SYLKCharacters as $k => $v) { + foreach (self::SYLK_CHARACTERS as $k => $v) { $textValue = str_replace($k, $v, $textValue); } diff --git a/src/PhpSpreadsheet/Shared/XMLWriter.php b/src/PhpSpreadsheet/Shared/XMLWriter.php index 2703e98e9..f1800066e 100644 --- a/src/PhpSpreadsheet/Shared/XMLWriter.php +++ b/src/PhpSpreadsheet/Shared/XMLWriter.php @@ -91,6 +91,6 @@ class XMLWriter extends \XMLWriter $rawTextData = implode("\n", $rawTextData); } - return $this->writeRaw(htmlspecialchars($rawTextData ?? '')); + return $this->text($rawTextData ?? ''); } } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 656f3b559..82de53542 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -691,10 +691,10 @@ class Spreadsheet implements JsonSerializable */ public function getSheetByName(string $worksheetName): ?Worksheet { - $worksheetCount = count($this->workSheetCollection); - for ($i = 0; $i < $worksheetCount; ++$i) { - if (strcasecmp($this->workSheetCollection[$i]->getTitle(), trim($worksheetName, "'")) === 0) { - return $this->workSheetCollection[$i]; + $trimWorksheetName = trim($worksheetName, "'"); + foreach ($this->workSheetCollection as $worksheet) { + if (strcasecmp($worksheet->getTitle(), $trimWorksheetName) === 0) { + return $worksheet; } } @@ -1784,4 +1784,21 @@ class Spreadsheet implements JsonSerializable } } } + + /** + * Excel will sometimes replace user's formatting choice + * with a built-in choice that it thinks is equivalent. + * Its choice is often not equivalent after all. + * Such treatment is astonishingly user-hostile. + * This function will undo such changes. + */ + public function replaceBuiltinNumberFormat(int $builtinFormatIndex, string $formatCode): void + { + foreach ($this->cellXfCollection as $style) { + $numberFormat = $style->getNumberFormat(); + if ($numberFormat->getBuiltInFormatCode() === $builtinFormatIndex) { + $numberFormat->setFormatCode($formatCode); + } + } + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index e1ed1faaf..d783ec945 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -158,6 +158,10 @@ class Html extends BaseWriter private string $getFalse = 'FALSE'; + protected bool $rtlSheets = false; + + protected bool $ltrSheets = false; + /** * Create a new HTML. */ @@ -186,11 +190,31 @@ class Html extends BaseWriter $this->maybeCloseFileHandle(); } + protected function checkRtlAndLtr(): void + { + $this->rtlSheets = false; + $this->ltrSheets = false; + if ($this->sheetIndex === null) { + foreach ($this->spreadsheet->getAllSheets() as $sheet) { + if ($sheet->getRightToLeft()) { + $this->rtlSheets = true; + } else { + $this->ltrSheets = true; + } + } + } else { + if ($this->spreadsheet->getSheet($this->sheetIndex)->getRightToLeft()) { + $this->rtlSheets = true; + } + } + } + /** * Save Spreadsheet as html to variable. */ public function generateHtmlAll(): string { + $this->checkRtlAndLtr(); $sheets = $this->generateSheetPrep(); foreach ($sheets as $sheet) { $sheet->calculateArrays($this->preCalculateFormulas); @@ -369,7 +393,8 @@ class Html extends BaseWriter // Construct HTML $properties = $this->spreadsheet->getProperties(); $html = '' . PHP_EOL; - $html .= '' . PHP_EOL; + $rtl = ($this->rtlSheets && !$this->ltrSheets) ? " dir='rtl'" : ''; + $html .= '' . PHP_EOL; $html .= ' ' . PHP_EOL; $html .= ' ' . PHP_EOL; $html .= ' ' . PHP_EOL; @@ -1013,6 +1038,9 @@ class Html extends BaseWriter // .s {} $css['.s']['text-align'] = 'left'; // STRING + $css['.floatright']['float'] = 'right'; + $css['.floatleft']['float'] = 'left'; + // Calculate cell style hashes foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style); @@ -1222,21 +1250,52 @@ class Html extends BaseWriter return $html; } + private function getDir(Worksheet $worksheet): string + { + if ($worksheet->getRightToLeft()) { + return " dir='rtl'"; + } + if ($this->rtlSheets) { + return " dir='ltr'"; + } + + return ''; + } + + private function getFloat(Worksheet $worksheet): string + { + $float = ''; + if ($worksheet->getRightToLeft()) { + if ($this->ltrSheets) { + $float = ' floatright'; + } + } else { + if ($this->rtlSheets) { + $float = ' floatleft'; + } + } + + return $float; + } + private function generateTableTagInline(Worksheet $worksheet, string $id): string { $style = isset($this->cssStyles['table']) ? $this->assembleCSS($this->cssStyles['table']) : ''; - + $rtl = $this->getDir($worksheet); + $float = $this->getFloat($worksheet); $prntgrid = $worksheet->getPrintGridlines(); $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines(); if ($viewgrid && $prntgrid) { - $html = " " . PHP_EOL; + $html = "
" . PHP_EOL; } elseif ($viewgrid) { - $html = "
" . PHP_EOL; + $html = "
" . PHP_EOL; } elseif ($prntgrid) { - $html = "
" . PHP_EOL; + $html = "
" . PHP_EOL; + } elseif ($float === '') { + $html = "
" . PHP_EOL; } else { - $html = "
" . PHP_EOL; + $html = "
" . PHP_EOL; } return $html; @@ -1245,9 +1304,11 @@ class Html extends BaseWriter private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void { if (!$this->useInlineCss) { + $rtl = $this->getDir($worksheet); + $float = $this->getFloat($worksheet); $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : ''; - $html .= "
" . PHP_EOL; + $html .= "
" . PHP_EOL; } else { $html .= $this->generateTableTagInline($worksheet, $id); } @@ -1266,10 +1327,12 @@ class Html extends BaseWriter // Construct HTML $html = ''; $id = $showid ? "id='sheet$sheetIndex'" : ''; + $clear = ($this->rtlSheets && $this->ltrSheets) ? '; clear:both' : ''; + if ($showid) { - $html .= "
" . PHP_EOL; + $html .= "
" . PHP_EOL; } else { - $html .= "
" . PHP_EOL; + $html .= "
" . PHP_EOL; } $this->generateTableTag($worksheet, $id, $html, $sheetIndex); diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index 540882708..fe5cfe25c 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -67,14 +67,28 @@ class Tcpdf extends Pdf // Set the appropriate font $pdf->SetFont($this->getFont()); + $this->checkRtlAndLtr(); + if ($this->rtlSheets && !$this->ltrSheets) { + $pdf->setRTL(true); + } $pdf->writeHTML($this->generateHTMLAll()); // Document info - $pdf->SetTitle($this->spreadsheet->getProperties()->getTitle()); - $pdf->SetAuthor($this->spreadsheet->getProperties()->getCreator()); - $pdf->SetSubject($this->spreadsheet->getProperties()->getSubject()); - $pdf->SetKeywords($this->spreadsheet->getProperties()->getKeywords()); - $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); + $pdf->SetTitle( + $this->spreadsheet->getProperties()->getTitle() + ); + $pdf->SetAuthor( + $this->spreadsheet->getProperties()->getCreator() + ); + $pdf->SetSubject( + $this->spreadsheet->getProperties()->getSubject() + ); + $pdf->SetKeywords( + $this->spreadsheet->getProperties()->getKeywords() + ); + $pdf->SetCreator( + $this->spreadsheet->getProperties()->getCreator() + ); // Write to file fwrite($fileHandle, $pdf->output('', 'S')); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 6437e2c19..9bb4f68f0 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -10,7 +10,6 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces; use PhpOffice\PhpSpreadsheet\RichText\RichText; -use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Style\Conditional; @@ -1440,10 +1439,7 @@ class Worksheet extends WriterPart $objWriter->writeElement( 't', StringHelper::controlCharacterPHP2OOXML( - htmlspecialchars( - $cellValue, - Settings::htmlEntityFlags() - ) + $cellValue ) ); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Calculation/CustomFunction.php b/tests/PhpSpreadsheetTests/Calculation/CustomFunction.php index 1ce5cf5d2..7e0fea0a7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CustomFunction.php +++ b/tests/PhpSpreadsheetTests/Calculation/CustomFunction.php @@ -6,6 +6,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class CustomFunction { @@ -19,4 +20,25 @@ class CustomFunction return $number ** 4; } + + /** + * ASC. + * Converts full-width (double-byte) characters to half-width (single-byte) characters. + * There are many difficulties with implementing this into PhpSpreadsheet. + */ + public static function ASC(mixed $stringValue): string + { + /*if (is_array($stringValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $stringValue); + }*/ + $stringValue = StringHelper::convertToString($stringValue, convertBool: true); + if (function_exists('mb_convert_kana')) { + return mb_convert_kana($stringValue, 'a', 'UTF-8'); + } + // Fallback if mb_convert_kana is not available. + // PhpSpreadsheet heavily relies on mbstring, so this is more of a theoretical fallback. + // A comprehensive manual conversion is extensive. + + return $stringValue; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/CustomFunctionTest.php b/tests/PhpSpreadsheetTests/Calculation/CustomFunctionTest.php index 1c536fad4..03262dfc0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CustomFunctionTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CustomFunctionTest.php @@ -31,4 +31,19 @@ class CustomFunctionTest extends TestCase self::assertSame('#NAME?', $calculation->calculateFormula('=FOURTHPOWER(3)')); self::assertFalse(Calculation::removeFunction('WHATEVER')); } + + public static function testReplaceDummyFunction(): void + { + $functions = Calculation::getFunctions(); + $key = 'ASC'; + $oldValue = $functions[$key] ?? null; + self::assertIsArray($oldValue); + $calculation = Calculation::getInstance(); + $value = $oldValue; + $value['functionCall'] = [CustomFunction::class, 'ASC']; + self::assertTrue(Calculation::addFunction($key, $value)); + self::assertSame('ABC', $calculation->calculateFormula('=ASC("ABC")')); + self::assertTrue(Calculation::removeFunction('ASC')); + self::assertTrue(Calculation::addFunction($key, $oldValue)); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Issue4451Test.php b/tests/PhpSpreadsheetTests/Calculation/Issue4451Test.php new file mode 100644 index 000000000..3b9107d18 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Issue4451Test.php @@ -0,0 +1,128 @@ +setAccessible(true); + + // Call the method using reflection + $reflectionMethod->invokeArgs($calculation, [&$matrix1, &$matrix2, count($matrix1), 1, count($matrix2), 1]); + + self::assertSame([[1], [3], [null]], $matrix1); //* @phpstan-ignore-line + } + + /** + * These 2 tests are contrived. They prove that method + * works as desired, but Excel will actually return + * a CALC error, a result I don't know how to duplicate. + */ + public static function testExtendFirstColumn(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Products'); + $calculationEngine = Calculation::getInstance($spreadsheet); + $calculationEngine->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + + $sheet->getCell('D5')->setValue(5); + $sheet->getCell('E5')->setValue(20); + $sheet->fromArray( + [ + [5, 20, 'Apples'], + [10, 20, 'Bananas'], + [5, 20, 'Cherries'], + [5, 40, 'Grapes'], + [25, 50, 'Peaches'], + [30, 60, 'Pears'], + [35, 70, 'Papayas'], + [40, 80, 'Mangos'], + [null, 20, 'Unknown'], + ], + null, + 'K1', + true + ); + $kRows = $sheet->getHighestDataRow('K'); + self::assertSame(8, $kRows); + $lRows = $sheet->getHighestDataRow('L'); + self::assertSame(9, $lRows); + $mRows = $sheet->getHighestDataRow('M'); + self::assertSame(9, $mRows); + $sheet->getCell('A1') + ->setValue( + "=FILTER(Products!M1:M$mRows," + . "(Products!K1:K$kRows=D5)" + . "*(Products!L1:L$lRows=E5))" + ); + + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertSame([['Apples'], ['Cherries']], $result); + $spreadsheet->disconnectWorksheets(); + } + + public static function testExtendSecondColumn(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Products'); + $calculationEngine = Calculation::getInstance($spreadsheet); + $calculationEngine->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + + $sheet->getCell('D5')->setValue(5); + $sheet->getCell('E5')->setValue(20); + $sheet->fromArray( + [ + [5, 20, 'Apples'], + [10, 20, 'Bananas'], + [5, 20, 'Cherries'], + [5, 40, 'Grapes'], + [25, 50, 'Peaches'], + [30, 60, 'Pears'], + [35, 70, 'Papayas'], + [40, 80, 'Mangos'], + [null, 20, 'Unknown'], + ], + null, + 'K1', + true + ); + $kRows = $sheet->getHighestDataRow('K'); + self::assertSame(8, $kRows); + //$lRows = $sheet->getHighestDataRow('L'); + //self::assertSame(9, $lRows); + $lRows = 2; + $mRows = $sheet->getHighestDataRow('M'); + self::assertSame(9, $mRows); + $sheet->getCell('A1') + ->setValue( + "=FILTER(Products!M1:M$mRows," + . "(Products!K1:K$kRows=D5)" + . "*(Products!L1:L$lRows=E5))" + ); + + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertSame([['Apples']], $result); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Issue4521Test.php b/tests/PhpSpreadsheetTests/Issue4521Test.php new file mode 100644 index 000000000..9c7ff3949 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Issue4521Test.php @@ -0,0 +1,56 @@ +outfile !== '') { + unlink($this->outfile); + $this->outfile = ''; + } + } + + public function testEmptyFile(): void + { + $this->outfile = File::temporaryFilename(); + file_put_contents($this->outfile, ''); + $spreadsheet = IOFactory::load($this->outfile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('A', $sheet->getHighestColumn()); + self::assertSame(1, $sheet->getHighestRow()); + $spreadsheet->disconnectWorksheets(); + } + + public function testCrlfFile(): void + { + if (PHP_MAJOR_VERSION === $this->weirdMimetypeMajor) { + if ( + PHP_MINOR_VERSION === $this->weirdMimetypeMinor1 + || PHP_MINOR_VERSION === $this->weirdMimetypeMinor2 + ) { + self::markTestSkipped('Php mimetype bug with this release'); + } + } + $this->outfile = File::temporaryFilename(); + file_put_contents($this->outfile, "\r\n"); + $spreadsheet = IOFactory::load($this->outfile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('A', $sheet->getHighestColumn()); + self::assertSame(1, $sheet->getHighestRow()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/DirectionTest.php b/tests/PhpSpreadsheetTests/Reader/Html/DirectionTest.php new file mode 100644 index 000000000..5021a54f3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/DirectionTest.php @@ -0,0 +1,92 @@ +", + '
', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
a1b1c1
a2b2c2
', + ]; + $html = implode("\n", $inlines); + $reader = new HtmlReader(); + $spreadsheet = $reader->loadFromString($html); + $sheet = $spreadsheet->getActiveSheet(); + self::assertTrue($sheet->getRightToLeft()); + self::assertSame('a1', $sheet->getCell('A1')->getValue()); + self::assertSame('c2', $sheet->getCell('C2')->getValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function testLtr(): void + { + $inlines = [ + "", + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
a1b1c1
a2b2c2
', + ]; + $html = implode("\n", $inlines); + $reader = new HtmlReader(); + $spreadsheet = $reader->loadFromString($html); + $sheet = $spreadsheet->getActiveSheet(); + self::assertFalse($sheet->getRightToLeft()); + self::assertSame('a1', $sheet->getCell('A1')->getValue()); + self::assertSame('c2', $sheet->getCell('C2')->getValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function testDefault(): void + { + $inlines = [ + "", + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
a1b1c1
a2b2c2
', + ]; + $html = implode("\n", $inlines); + $reader = new HtmlReader(); + $spreadsheet = $reader->loadFromString($html); + $sheet = $spreadsheet->getActiveSheet(); + self::assertFalse($sheet->getRightToLeft()); + self::assertSame('a1', $sheet->getCell('A1')->getValue()); + self::assertSame('c2', $sheet->getCell('C2')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/NestedTableRowTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/NestedTableRowTest.php new file mode 100644 index 000000000..45e0646b9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/NestedTableRowTest.php @@ -0,0 +1,42 @@ +load($infile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Atterissage', $sheet->getCell('AS1')->getValue()); + self::assertNull($sheet->getCell('AS2')->getValue()); + self::assertSame('jour', $sheet->getCell('AS3')->getValue()); + self::assertSame('=SUM(Y3:INDIRECT(CONCATENATE("Y",$C$3)))', $sheet->getCell('AS4')->getValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function testTableRowGroup(): void + { + $infile = 'tests/data/Reader/Ods/issue.2507.ods'; + $reader = new OdsReader(); + $spreadsheet = $reader->load($infile); + $sheet = $spreadsheet->getActiveSheet(); + $values = $sheet->rangeToArray('B3:C7', null, false, false); + $expected = [ + ['Номенклатура', "Складское наличие,\nКол-во"], // before table-row-group + ['Квадрат 140х140мм ст.5ХНМ (т)', 0.225], // within table-row-group + ['Квадрат 200х200мм ст.3 (т)', 1.700], + ['Квадрат 210х210мм ст.65Г (т)', 0.280], + ['Квадрат 250х250мм ст.45 (т)', 0.133], + ]; + self::assertSame($expected, $values); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ReadDynamTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ReadDynamTest.php new file mode 100644 index 000000000..8ad7053f1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ReadDynamTest.php @@ -0,0 +1,71 @@ +setUseCSEArrays(true); + } + + public function testCse(): void + { + $spreadsheetOld = new Spreadsheet(); + $sheetOld = $spreadsheetOld->getActiveSheet(); + $calcOld = Calculation::getInstance($spreadsheetOld); + $calcOld->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheetOld->fromArray( + [1, 2, 2, 4, 3, 2, 1, 3, 3, 3, 5], + null, + 'A14', + true + ); + $sheetOld->setCellValue('A15', '=UNIQUE(A14:K14, TRUE)'); + /** @var callable */ + $callableWriter = [$this, 'writeCse']; + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Xlsx', null, $callableWriter); + $spreadsheetOld->disconnectWorksheets(); + $calc = Calculation::getInstance($spreadsheet); + self::assertSame( + Calculation::RETURN_ARRAY_AS_VALUE, + $calc->getInstanceArrayReturnType() + ); + $spreadsheet->disconnectWorksheets(); + } + + public function testDynam(): void + { + $spreadsheetOld = new Spreadsheet(); + $sheetOld = $spreadsheetOld->getActiveSheet(); + $calcOld = Calculation::getInstance($spreadsheetOld); + $calcOld->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheetOld->fromArray( + [1, 2, 2, 4, 3, 2, 1, 3, 3, 3, 5], + null, + 'A14', + true + ); + $sheetOld->setCellValue('A15', '=UNIQUE(A14:K14, TRUE)'); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheetOld, 'Xlsx'); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Xlsx'); + $spreadsheetOld->disconnectWorksheets(); + $calc = Calculation::getInstance($spreadsheet); + self::assertSame( + Calculation::RETURN_ARRAY_AS_ARRAY, + $calc->getInstanceArrayReturnType() + ); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ReplaceBuiltinNumberFormatTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ReplaceBuiltinNumberFormatTest.php new file mode 100644 index 000000000..59b5cf3c3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ReplaceBuiltinNumberFormatTest.php @@ -0,0 +1,69 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->reloadedSpreadsheet !== null) { + $this->reloadedSpreadsheet->disconnectWorksheets(); + $this->reloadedSpreadsheet = null; + } + } + + public function testReplaceBuiltinNumberFormat(): void + { + $spreadsheet = $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->fromArray([45486, 1023, 45487, 45488, 45489]); + $sheet->getStyle('A1')->getNumberFormat() + ->setBuiltInFormatCode(NumberFormat::SHORT_DATE_INDEX); + $sheet->getStyle('B1')->getNumberFormat() + ->setFormatCode('#,##0.00'); + $sheet->getStyle('C1')->getNumberFormat() + ->setBuiltInFormatCode(NumberFormat::SHORT_DATE_INDEX); + $sheet->getStyle('D1')->getNumberFormat() + ->setFormatCode('dd-MMM-yyyy'); + $sheet->getStyle('E1')->getNumberFormat() + ->setBuiltInFormatCode(16); + $values = $sheet->toArray(); + $expected = [[ + '7/13/2024', // builtin style 14 + '1,023.00', // #,##0.00 + '7/14/2024', // builtin style 14 + '15-Jul-2024', // dd-MMM-yyyy + '16-Jul', // builtin style 16 + ]]; + self::assertSame($expected, $values); + $this->reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $this->reloadedSpreadsheet->replaceBuiltinNumberFormat( + NumberFormat::SHORT_DATE_INDEX, + 'yyyy-mm-dd' + ); + $rsheet = $this->reloadedSpreadsheet->getActiveSheet(); + $newValues = $rsheet->toArray(); + $newExpected = [[ + '2024-07-13', // yyyy-mm-dd changed from builtin style 14 + '1,023.00', // unchanged #,##0.00 + '2024-07-14', // yyyy-mm-dd changed from builtin style 14 + '15-Jul-2024', // unchanged dd-MMM-yyyy + '16-Jul', // unchanged builtin style 16 + ]]; + self::assertSame($newExpected, $newValues); + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Issue347Test.php b/tests/PhpSpreadsheetTests/Shared/Issue347Test.php new file mode 100644 index 000000000..c4e7707b6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Issue347Test.php @@ -0,0 +1,60 @@ +setPreserveCr(true); + $reader->setValueBinder($binder); + } + + #[DataProvider('providerType')] + public function testPreserveCr(string $format): void + { + $s1 = "AB\r\nC\tD"; + $spreadsheet = new Spreadsheet(); + $binder = new DefaultValueBinder(); + $binder->setPreserveCr(true); + $spreadsheet->setValueBinder($binder); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($s1); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format, readerCustomizer: $this->readerPreserveCr(...)); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $s2 = $rsheet->getCell('A1')->getValue(); + self::assertSame($s1, $s2); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + #[DataProvider('providerType')] + public function testNoPreserveCr(string $format): void + { + $s1 = "AB\r\nC\tD"; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($s1); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $s2 = $rsheet->getCell('A1')->getValue(); + self::assertNotEquals($s1, $s2); + self::assertSame("AB\nC\tD", $s2); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public static function providerType(): array + { + return [['Xls'], ['Xlsx'], ['Ods']]; + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/DirectionTest.php b/tests/PhpSpreadsheetTests/Writer/Html/DirectionTest.php new file mode 100644 index 000000000..9a406bc32 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/DirectionTest.php @@ -0,0 +1,86 @@ +getActiveSheet(); + $sheet1->setRightToLeft(true); + $sheet2 = $spreadsheet->createSheet(); + $sheet3 = $spreadsheet->createSheet(); + $sheet3->setRightToLeft(true); + $cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ]; + $sheet1->fromArray($cells); + $sheet2->fromArray($cells); + $sheet3->fromArray($cells); + $writer = new Html($spreadsheet); + $writer->writeAllSheets(); + $html = $writer->generateHTMLall(); + $rtlCount = substr_count($html, "dir='rtl'"); + self::assertSame(2, $rtlCount); + $ltrCount = substr_count($html, "dir='ltr'"); + self::assertSame(1, $ltrCount); + $spreadsheet->disconnectWorksheets(); + } + + public function testNoRtl(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + $sheet3 = $spreadsheet->createSheet(); + $cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ]; + $sheet1->fromArray($cells); + $sheet2->fromArray($cells); + $sheet3->fromArray($cells); + $writer = new Html($spreadsheet); + $writer->writeAllSheets(); + $html = $writer->generateHTMLall(); + $rtlCount = substr_count($html, "dir='rtl'"); + self::assertSame(0, $rtlCount); + $ltrCount = substr_count($html, "dir='ltr'"); + self::assertSame(0, $ltrCount); + $spreadsheet->disconnectWorksheets(); + } + + public function testOnlyRtl(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + $sheet3 = $spreadsheet->createSheet(); + $cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ]; + $sheet1->fromArray($cells); + $sheet1->setRightToLeft(true); + $sheet2->fromArray($cells); + $sheet2->setRightToLeft(true); + $sheet3->fromArray($cells); + $sheet3->setRightToLeft(true); + $writer = new Html($spreadsheet); + $writer->writeAllSheets(); + $html = $writer->generateHTMLall(); + $rtlCount = substr_count($html, "dir='rtl'"); + self::assertSame(4, $rtlCount, '3 sheets plus html tag'); + $ltrCount = substr_count($html, "dir='ltr'"); + self::assertSame(0, $ltrCount); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/Issue4537Test.php b/tests/PhpSpreadsheetTests/Writer/Ods/Issue4537Test.php new file mode 100644 index 000000000..e4a24707d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/Issue4537Test.php @@ -0,0 +1,64 @@ +outputFilename !== '') { + unlink($this->outputFilename); + $this->outputFilename = ''; + } + } + + public function testBackgroundImage(): void + { + $this->outputFilename = File::temporaryFilename(); + $testString = "\"He\": ''"; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValueExplicit($testString, DataType::TYPE_INLINE); + $sheet->getCell('A2')->setValue($testString); + $richText = new RichText(); + $richText->addText(new TextElement($testString)); + $sheet->getCell('A3')->setValue($richText); + $writer = new OdsWriter($spreadsheet); + $writer->save($this->outputFilename); + $spreadsheet->disconnectWorksheets(); + + $reader = new OdsReader(); + $reloadedSpreadsheet = $reader->load($this->outputFilename); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame($testString, $rsheet->getCell('A1')->getValueString()); + self::assertSame($testString, $rsheet->getCell('A2')->getValueString()); + self::assertSame($testString, $rsheet->getCell('A3')->getValueString()); + $reloadedSpreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFilename; + $file .= '#content.xml'; + $data = file_get_contents($file); + // expected does not escape apostrophes + $expected = '"He": \'<?>\''; + if ($data === false) { + self::fail('Unable to read content file'); + } else { + $count = substr_count($data, $expected); + self::assertSame(3, $count); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4537Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4537Test.php new file mode 100644 index 000000000..7cbd86bb4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4537Test.php @@ -0,0 +1,76 @@ +outputFilename !== '') { + unlink($this->outputFilename); + $this->outputFilename = ''; + } + } + + public function testBackgroundImage(): void + { + $this->outputFilename = File::temporaryFilename(); + $testString = "\"He\": ''"; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValueExplicit($testString, DataType::TYPE_INLINE); + $sheet->getCell('A2')->setValue($testString); + $richText = new RichText(); + $richText->addText(new TextElement($testString)); + $sheet->getCell('A3')->setValue($richText); + $writer = new XlsxWriter($spreadsheet); + $writer->save($this->outputFilename); + $spreadsheet->disconnectWorksheets(); + + $reader = new XlsxReader(); + $reloadedSpreadsheet = $reader->load($this->outputFilename); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame($testString, $rsheet->getCell('A1')->getValueString()); + self::assertSame($testString, $rsheet->getCell('A2')->getValueString()); + self::assertSame($testString, $rsheet->getCell('A3')->getValueString()); + $reloadedSpreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFilename; + $file .= '#xl/worksheets/sheet1.xml'; + $data = file_get_contents($file); + // expected, and expected1/2 below, do not escape apostrophes + $expected = 't="inlineStr">"He": \'<?>\''; + if ($data === false) { + self::fail('Unable to read worksheets file'); + } else { + self::assertStringContainsString($expected, $data, 'inline string'); + } + + $file = 'zip://'; + $file .= $this->outputFilename; + $file .= '#xl/sharedStrings.xml'; + $data = file_get_contents($file); + $expected1 = '"He": \'<?>\''; + $expected2 = '"He": \'<?>\''; + if ($data === false) { + self::fail('Unable to read sharedStrings file'); + } else { + self::assertStringContainsString($expected1, $data, 'string'); + self::assertStringContainsString($expected2, $data, 'rich text'); + } + } +} diff --git a/tests/data/Reader/Ods/issue.2507.ods b/tests/data/Reader/Ods/issue.2507.ods new file mode 100644 index 000000000..1475bc648 Binary files /dev/null and b/tests/data/Reader/Ods/issue.2507.ods differ diff --git a/tests/data/Reader/Ods/issue.4528.ods b/tests/data/Reader/Ods/issue.4528.ods new file mode 100644 index 000000000..8e3ab4778 Binary files /dev/null and b/tests/data/Reader/Ods/issue.4528.ods differ