diff --git a/CHANGELOG.md b/CHANGELOG.md index f60d7425..290474a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Add point size option for scatter charts +- Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) +- Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) + + Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. + +- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) and Gnumeric Reader [PR #2853](https://github.com/PHPOffice/PhpSpreadsheet/pull/2853) +- Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) +- Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) +- Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) ### Changed - Memory and speed improvements, particularly for the Cell Collection, and the Writers. - See [the Discussion](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance + See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions ### Deprecated @@ -28,7 +36,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) - +- Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) +- Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849) +- Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) +- Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) +- Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) ## 1.23.0 - 2022-04-24 diff --git a/README.md b/README.md index 2a94e0d3..40b025e7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,44 @@ PhpSpreadsheet is a library written in pure PHP and offers a set of classes that allow you to read and write various spreadsheet file formats such as Excel and LibreOffice Calc. +## PHP version support + +LTS: Support for PHP versions will only be maintained for a period of six months beyond the +[end of life of that PHP version](https://www.php.net/eol.php). + +Currently the required PHP minimum version is PHP __7.3__. + +See the `composer.json` for other requirements. + +## Installation + +Use [composer](https://getcomposer.org) to install PhpSpreadsheet into your project: + +```sh +composer require phpoffice/phpspreadsheet +``` + +If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: +```json lines +{ + "require": { + "phpoffice/phpspreadsheet": "^1.23" + }, + "config": { + "platform": { + "php": "7.3" + } + } +} +``` +and then run +```sh +composer install +``` +to ensure that the correct dependencies are retrieved to match your deployment environment. + +See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. + ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). diff --git a/docs/index.md b/docs/index.md index 98a2d3d8..ff137c26 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,9 +30,14 @@ for details. ### PHP version support -Support for PHP versions will only be maintained for a period of six months beyond the +LTS: Support for PHP versions will only be maintained for a period of six months beyond the [end of life of that PHP version](https://www.php.net/eol.php). +Currently the required PHP minimum version is PHP 7.3. The last PHP release was 7.3.33 on 6th December 2021, so PhpSpreadsheet will support PHP 7.3 until 6th June 2022. +PHP 7.4 is officially [End of Life](https://www.php.net/supported-versions.php) on 28th November 2022, and PhpSpreadsheet will continue to support PHP 7.4 for six months after that date. + +See the `composer.json` for other requirements. + ## Installation Use [composer](https://getcomposer.org) to install PhpSpreadsheet into your project: @@ -47,6 +52,26 @@ Or also download the documentation and samples if you plan to use them: composer require phpoffice/phpspreadsheet --prefer-source ``` +If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: +```json lines +{ + "require": { + "phpoffice/phpspreadsheet": "^1.23" + }, + "config": { + "platform": { + "php": "7.3" + } + } +} +``` +and then run +```sh +composer install +``` +to ensure that the correct dependencies are retrieved to match your deployment environment. + +See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. ## Hello World diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 399be82e..d18c0969 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -16,7 +16,7 @@ XLS XLSX - Excel2003XML + XML (Excel2003XML) Ods Gnumeric CSV @@ -732,12 +732,32 @@ + + Hidden Worksheets + ✔ + ✔ + + ✔ + ✔ + N/A + + + + + + + + + + + Coloured Tabs + N/A diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 19928f04..0bcc1909 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -449,8 +449,7 @@ $spreadsheet = $reader->loadSpreadsheetFromString($data); #### Setting CSV options Often, CSV files are not really "comma separated", or use semicolon (`;`) -as a separator. You can instruct -`\PhpOffice\PhpSpreadsheet\Reader\Csv` some options before reading a CSV +as a separator. You can set some options before reading a CSV file. The separator will be auto-detected, so in most cases it should not be necessary @@ -506,6 +505,12 @@ $reader->setSheetIndex(0); $spreadsheet = $reader->load('sample.csv'); ``` +The CSV reader will normally not load null strings into the spreadsheet. +To load them: +```php +$reader->setPreserveNullString(true); +``` + Finally, you can set a callback to be invoked when the constructor is executed, either through `new Csv()` or `IOFactory::load`, and have that callback set the customizable attributes to whatever @@ -584,8 +589,7 @@ $writer->save("05featuredemo.csv"); #### Setting CSV options Often, CSV files are not really "comma separated", or use semicolon (`;`) -as a separator. You can instruct -`\PhpOffice\PhpSpreadsheet\Writer\Csv` some options before writing a CSV +as a separator. You can set some options before writing a CSV file: ```php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3989e2cf..778090a8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -125,11 +125,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Parameter \\#3 \\$formula of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:translateSeparator\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$cellStack has no type specified\\.$#" count: 1 @@ -750,11 +745,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/LookupRef/Offset.php - - - message: "#^Parameter \\#1 \\$columnAddress of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:columnIndexFromString\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php - - message: "#^Binary operation \"/\" between array\\|float\\|int\\|string and array\\|float\\|int\\|string results in an error\\.$#" count: 2 @@ -1160,76 +1150,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Chart/Chart.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getBottomRightXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getBottomRightYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getTopLeftXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getTopLeftYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightCell\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightCell\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightXOffset\\(\\) has parameter \\$xOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightYOffset\\(\\) has parameter \\$yOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftXOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftXOffset\\(\\) has parameter \\$xOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftYOffset\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftYOffset\\(\\) has parameter \\$yOffset with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$legend \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\|null\\.$#" count: 1 @@ -1285,26 +1205,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Chart/DataSeries.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:refresh\\(\\) has parameter \\$flatten with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$dataSource \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$dataTypeValues has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$fillColor \\(array\\\\|string\\) does not accept array\\\\|string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - message: "#^Parameter \\#1 \\$angle of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setShadowAngle\\(\\) expects int, int\\|null given\\.$#" count: 1 @@ -1800,11 +1700,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Helper/Html.php - - - message: "#^Parameter \\#1 \\$text of method PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\ITextElement\\:\\:setText\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$bold has no type specified\\.$#" count: 1 @@ -1905,11 +1800,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Helper/Sample.php - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\IOFactory\\:\\:createReader\\(\\) should return PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\IReader but returns object\\.$#" count: 1 @@ -2035,36 +1925,11 @@ parameters: count: 6 path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$masterPrintStylesCrossReference has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$masterStylesCrossReference has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$officeNs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$pageLayoutStyles has no type specified\\.$#" count: 1 path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesFo has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesNs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\Properties\\:\\:load\\(\\) has parameter \\$namespacesMeta with no type specified\\.$#" count: 1 @@ -2396,7 +2261,7 @@ parameters: path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:boolean\\(\\) has no return type specified\\.$#" + message: "#^Comparison operation \"\\>\" between SimpleXMLElement\\|null and 0 results in an error\\.$#" count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php @@ -2470,11 +2335,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has parameter \\$add with no type specified\\.$#" count: 1 @@ -2535,21 +2395,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:stripWhiteSpaceFromStyleString\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:stripWhiteSpaceFromStyleString\\(\\) has parameter \\$string with no type specified\\.$#" count: 1 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:toCSSArray\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:toCSSArray\\(\\) has parameter \\$style with no type specified\\.$#" count: 1 @@ -2625,206 +2475,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - message: "#^Cannot call method getFont\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\Run\\|null\\.$#" - count: 12 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 3 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has parameter \\$chartDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has parameter \\$plotType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$marker with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$seriesDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValues\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValues\\(\\) has parameter \\$dataType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValues\\(\\) has parameter \\$seriesValueSet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValuesMultiLevel\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValuesMultiLevel\\(\\) has parameter \\$dataType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValuesMultiLevel\\(\\) has parameter \\$seriesValueSet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartLayoutDetails\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartLayoutDetails\\(\\) has parameter \\$chartDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartLayoutDetails\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartTitle\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:parseRichText\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readChartAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readChartAttributes\\(\\) has parameter \\$chartDetail with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has parameter \\$background with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has parameter \\$color with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#1 \\$position of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend constructor expects string, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#3 \\$overlay of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend constructor expects bool, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#3 \\$plotOrder of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeries constructor expects array\\, array\\ given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$pointCount of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues constructor expects int, null given\\.$#" - count: 4 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#6 \\$displayBlanksAs of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart constructor expects string, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Parameter \\#7 \\$plotDirection of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeries constructor expects string\\|null, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredColumn\\(\\) has no return type specified\\.$#" count: 1 @@ -3035,11 +2685,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/ReferenceHelper.php - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\Run\\:\\:\\$font \\(PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" count: 1 @@ -4035,26 +3680,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php - - - message: "#^Parameter \\#2 \\$subject of function preg_split expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php - - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\PercentageFormatter\\:\\:format\\(\\) has parameter \\$value with no type specified\\.$#" count: 1 path: src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php - - - message: "#^Parameter \\#1 \\$format of function sprintf expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\CellIterator\\:\\:adjustForExistingOnlyRange\\(\\) has no return type specified\\.$#" count: 1 @@ -4490,11 +4120,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Html.php - - - message: "#^Parameter \\#1 \\$string of function htmlspecialchars expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - message: "#^Parameter \\#1 \\$vAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapVAlign\\(\\) expects string, string\\|null given\\.$#" count: 1 @@ -4690,16 +4315,6 @@ parameters: count: 7 path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xls\\\\Parser\\:\\:\\$spreadsheet has no type specified\\.$#" count: 1 @@ -4800,21 +4415,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - - - message: "#^Parameter \\#2 \\$subject of function preg_match_all expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - message: "#^Parameter \\#4 \\$isError of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xls\\\\Worksheet\\:\\:writeBoolErr\\(\\) expects bool, int given\\.$#" count: 3 @@ -4947,7 +4547,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 44 + count: 42 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - @@ -4955,26 +4555,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Parameter \\#3 \\$id1 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$id1 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$id2 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#5 \\$id2 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#6 \\$yAxis of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null given\\.$#" count: 1 @@ -5062,7 +4642,7 @@ parameters: - message: "#^Cannot call method getBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5072,12 +4652,12 @@ parameters: - message: "#^Cannot call method getItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - message: "#^Cannot call method getName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5087,7 +4667,7 @@ parameters: - message: "#^Cannot call method getStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5102,7 +4682,7 @@ parameters: - message: "#^Cannot call method getUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5137,7 +4717,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 5 + count: 4 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5285,8 +4865,3 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Xlfn\\:\\:addXlfn\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php - diff --git a/samples/Chart/33_Chart_create_bubble.php b/samples/Chart/33_Chart_create_bubble.php new file mode 100644 index 00000000..33feea62 --- /dev/null +++ b/samples/Chart/33_Chart_create_bubble.php @@ -0,0 +1,124 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['Number of Products', 'Sales in USD', 'Market share'], + [14, 12200, 15], + [20, 60000, 33], + [18, 24400, 10], + [22, 32000, 42], + [], + [12, 8200, 18], + [15, 50000, 30], + [19, 22400, 15], + [25, 25000, 50], + ] +); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker + +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['2013']), // 2013 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['2014']), // 2014 +]; + +// Set the X-Axis values +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesCategories = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$7:$A$10', null, 4), +]; + +// Set the Y-Axis values +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$7:$B$10', null, 4), +]; + +// Set the Z-Axis values (bubble size) +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesBubbles = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$7:$C$10', null, 4), +]; + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_BUBBLECHART, // plotType + null, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $dataSeriesCategories, // plotCategory + $dataSeriesValues // plotValues +); +$series->setPlotBubbleSizes($dataSeriesBubbles); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +// Create the chart +$chart = new Chart( + 'chart1', // name + null, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('E1'); +$chart->setBottomRightPosition('M15'); + +// Add the chart to the worksheet +$worksheet->addChart($chart); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->getColumnDimension('B')->setAutoSize(true); +$worksheet->getColumnDimension('C')->setAutoSize(true); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php new file mode 100644 index 00000000..1d6e331c --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -0,0 +1,121 @@ +getActiveSheet(); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] +); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 +]; +// Set the X-Axis Labels +// changed from STRING to NUMBER +// added 2 additional x-axis values associated with each of the 3 metrics +// added FORMATE_CODE_NUMBER +$xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), +]; + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + //DataSeries::STYLE_LINEMARKER // plotStyle + DataSeries::STYLE_MARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Trend Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('P20'); +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$spreadsheet->disconnectWorksheets(); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteBubbleChart2.xlsx b/samples/templates/32readwriteBubbleChart2.xlsx new file mode 100644 index 00000000..206cfaea Binary files /dev/null and b/samples/templates/32readwriteBubbleChart2.xlsx differ diff --git a/samples/templates/32readwriteScatterChart6.xlsx b/samples/templates/32readwriteScatterChart6.xlsx new file mode 100644 index 00000000..ddfa3048 Binary files /dev/null and b/samples/templates/32readwriteScatterChart6.xlsx differ diff --git a/samples/templates/32readwriteScatterChart7.xlsx b/samples/templates/32readwriteScatterChart7.xlsx new file mode 100644 index 00000000..6c01bf40 Binary files /dev/null and b/samples/templates/32readwriteScatterChart7.xlsx differ diff --git a/samples/templates/32readwriteStockChart5.xlsx b/samples/templates/32readwriteStockChart5.xlsx new file mode 100644 index 00000000..364803f2 Binary files /dev/null and b/samples/templates/32readwriteStockChart5.xlsx differ diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 7a48a7cb..65105f8e 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3197,7 +3197,7 @@ class Calculation string $toSeparator ): string { // Function Names - $formula = preg_replace($from, $to, $formula); + $formula = (string) preg_replace($from, $to, $formula); // Temporarily adjust matrix separators so that they won't be confused with function arguments $formula = self::translateSeparator(';', '|', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); @@ -4180,7 +4180,7 @@ class Calculation $length = strlen($val); if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { - $val = preg_replace('/\s/u', '', $val); + $val = (string) preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function $valToUpper = strtoupper($val); } else { diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index 466fe4a6..52543a72 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -49,7 +49,7 @@ class DateValue $baseYear = SharedDateHelper::getExcelCalendar(); $dateValue = trim($dateValue ?? '', '"'); // Strip any ordinals because they're allowed in Excel (English only) - $dateValue = preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue) ?? ''; + $dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); // Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany) $dateValue = str_replace(['/', '.', '-', ' '], ' ', $dateValue); diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 8075811f..1cc980b8 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -159,7 +159,7 @@ class Functions } elseif (!is_numeric($condition)) { if ($condition !== '""') { // Not an empty string // Escape any quotes in the string value - $condition = preg_replace('/"/ui', '""', $condition); + $condition = (string) preg_replace('/"/ui', '""', $condition); } $condition = Calculation::wrapResult(strtoupper($condition)); } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 28e8df89..7408a66e 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -43,7 +43,7 @@ class Helpers if ($namedRange !== null) { $workSheet = $namedRange->getWorkSheet(); $sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle(); - $value = preg_replace('/^=/', '', $namedRange->getValue()); + $value = (string) preg_replace('/^=/', '', $namedRange->getValue()); self::adjustSheetTitle($sheetTitle, $value); $cellAddress1 = $sheetTitle . $value; $cellAddress = $cellAddress1; diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php index 1f848045..8bce07e9 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -50,7 +50,7 @@ class RowColumnInformation if (is_array($cellAddress)) { foreach ($cellAddress as $columnKey => $value) { - $columnKey = preg_replace('/[^a-z]/i', '', $columnKey); + $columnKey = (string) preg_replace('/[^a-z]/i', '', $columnKey); return (int) Coordinate::columnIndexFromString($columnKey); } @@ -66,8 +66,8 @@ class RowColumnInformation [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/[^a-z]/i', '', $startAddress); - $endAddress = preg_replace('/[^a-z]/i', '', $endAddress); + $startAddress = (string) preg_replace('/[^a-z]/i', '', $startAddress); + $endAddress = (string) preg_replace('/[^a-z]/i', '', $endAddress); return range( (int) Coordinate::columnIndexFromString($startAddress), @@ -75,7 +75,7 @@ class RowColumnInformation ); } - $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); + $cellAddress = (string) preg_replace('/[^a-z]/i', '', $cellAddress); return (int) Coordinate::columnIndexFromString($cellAddress); } @@ -159,14 +159,13 @@ class RowColumnInformation [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/\D/', '', $startAddress); - $endAddress = preg_replace('/\D/', '', $endAddress); + $startAddress = (string) preg_replace('/\D/', '', $startAddress); + $endAddress = (string) preg_replace('/\D/', '', $endAddress); return array_map( function ($value) { return [$value]; }, - // @phpstan-ignore-next-line range($startAddress, $endAddress) ); } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php index 7fd30233..f26da389 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -102,29 +102,27 @@ class Operations */ public static function product(...$args) { + $args = array_filter( + Functions::flattenArray($args), + function ($value) { + return $value !== null; + } + ); + // Return value - $returnValue = null; + $returnValue = (count($args) === 0) ? 0.0 : 1.0; // Loop through arguments - foreach (Functions::flattenArray($args) as $arg) { + foreach ($args as $arg) { // Is it a numeric value? if (is_numeric($arg)) { - if ($returnValue === null) { - $returnValue = $arg; - } else { - $returnValue *= $arg; - } + $returnValue *= $arg; } else { return ExcelError::VALUE(); } } - // Return - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return (float) $returnValue; } /** diff --git a/src/PhpSpreadsheet/Calculation/TextData/Trim.php b/src/PhpSpreadsheet/Calculation/TextData/Trim.php index e52c0095..27eceb93 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Trim.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Trim.php @@ -14,7 +14,7 @@ class Trim * @param mixed $stringValue String Value to check * Or can be an array of values * - * @return null|array|string + * @return array|string * If an array of values is passed as the argument, then the returned result will also be an array * with the same dimensions */ @@ -26,7 +26,7 @@ class Trim $stringValue = Helpers::extractString($stringValue); - return preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); + return (string) preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); } /** diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index eeed326d..089ebdb9 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -18,6 +18,7 @@ class Axis extends Properties private $axisNumber = [ 'format' => self::FORMAT_CODE_GENERAL, 'source_linked' => 1, + 'numeric' => null, ]; /** @@ -131,15 +132,26 @@ class Axis extends Properties 'size' => null, ]; + private const NUMERIC_FORMAT = [ + Properties::FORMAT_CODE_NUMBER, + Properties::FORMAT_CODE_DATE, + ]; + /** * Get Series Data Type. * * @param mixed $format_code */ - public function setAxisNumberProperties($format_code): void + public function setAxisNumberProperties($format_code, ?bool $numeric = null): void { - $this->axisNumber['format'] = (string) $format_code; + $format = (string) $format_code; + $this->axisNumber['format'] = $format; $this->axisNumber['source_linked'] = 0; + if (is_bool($numeric)) { + $this->axisNumber['numeric'] = $numeric; + } elseif (in_array($format, self::NUMERIC_FORMAT, true)) { + $this->axisNumber['numeric'] = true; + } } /** @@ -162,6 +174,11 @@ class Axis extends Properties return (string) $this->axisNumber['source_linked']; } + public function getAxisIsNumericFormat(): bool + { + return (bool) $this->axisNumber['numeric']; + } + /** * Set Axis Options Properties. * diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index bed89464..ec6342c5 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -124,7 +124,7 @@ class Chart * * @var string */ - private $bottomRightCellRef = 'A1'; + private $bottomRightCellRef = ''; /** * Bottom-Right X-Offset. @@ -140,6 +140,21 @@ class Chart */ private $bottomRightYOffset = 10; + /** @var ?int */ + private $rotX; + + /** @var ?int */ + private $rotY; + + /** @var ?int */ + private $rAngAx; + + /** @var ?int */ + private $perspective; + + /** @var bool */ + private $oneCellAnchor = false; + /** * Create a new Chart. * @@ -351,8 +366,9 @@ class Chart if ($this->yAxis !== null) { return $this->yAxis; } + $this->yAxis = new Axis(); - return new Axis(); + return $this->yAxis; } /** @@ -365,8 +381,9 @@ class Chart if ($this->xAxis !== null) { return $this->xAxis; } + $this->xAxis = new Axis(); - return new Axis(); + return $this->xAxis; } /** @@ -400,15 +417,15 @@ class Chart /** * Set the Top Left position for the chart. * - * @param string $cell + * @param string $cellAddress * @param int $xOffset * @param int $yOffset * * @return $this */ - public function setTopLeftPosition($cell, $xOffset = null, $yOffset = null) + public function setTopLeftPosition($cellAddress, $xOffset = null, $yOffset = null) { - $this->topLeftCellRef = $cell; + $this->topLeftCellRef = $cellAddress; if ($xOffset !== null) { $this->setTopLeftXOffset($xOffset); } @@ -446,13 +463,13 @@ class Chart /** * Set the Top Left cell position for the chart. * - * @param string $cell + * @param string $cellAddress * * @return $this */ - public function setTopLeftCell($cell) + public function setTopLeftCell($cellAddress) { - $this->topLeftCellRef = $cell; + $this->topLeftCellRef = $cellAddress; return $this; } @@ -491,6 +508,11 @@ class Chart ]; } + /** + * @param int $xOffset + * + * @return $this + */ public function setTopLeftXOffset($xOffset) { $this->topLeftXOffset = $xOffset; @@ -498,11 +520,16 @@ class Chart return $this; } - public function getTopLeftXOffset() + public function getTopLeftXOffset(): int { return $this->topLeftXOffset; } + /** + * @param int $yOffset + * + * @return $this + */ public function setTopLeftYOffset($yOffset) { $this->topLeftYOffset = $yOffset; @@ -510,7 +537,7 @@ class Chart return $this; } - public function getTopLeftYOffset() + public function getTopLeftYOffset(): int { return $this->topLeftYOffset; } @@ -518,15 +545,15 @@ class Chart /** * Set the Bottom Right position of the chart. * - * @param string $cell + * @param string $cellAddress * @param int $xOffset * @param int $yOffset * * @return $this */ - public function setBottomRightPosition($cell, $xOffset = null, $yOffset = null) + public function setBottomRightPosition($cellAddress = '', $xOffset = null, $yOffset = null) { - $this->bottomRightCellRef = $cell; + $this->bottomRightCellRef = $cellAddress; if ($xOffset !== null) { $this->setBottomRightXOffset($xOffset); } @@ -551,19 +578,22 @@ class Chart ]; } - public function setBottomRightCell($cell) + /** + * Set the Bottom Right cell for the chart. + * + * @return $this + */ + public function setBottomRightCell(string $cellAddress = '') { - $this->bottomRightCellRef = $cell; + $this->bottomRightCellRef = $cellAddress; return $this; } /** * Get the cell address where the bottom right of the chart is fixed. - * - * @return string */ - public function getBottomRightCell() + public function getBottomRightCell(): string { return $this->bottomRightCellRef; } @@ -602,6 +632,11 @@ class Chart ]; } + /** + * @param int $xOffset + * + * @return $this + */ public function setBottomRightXOffset($xOffset) { $this->bottomRightXOffset = $xOffset; @@ -609,11 +644,16 @@ class Chart return $this; } - public function getBottomRightXOffset() + public function getBottomRightXOffset(): int { return $this->bottomRightXOffset; } + /** + * @param int $yOffset + * + * @return $this + */ public function setBottomRightYOffset($yOffset) { $this->bottomRightYOffset = $yOffset; @@ -621,7 +661,7 @@ class Chart return $this; } - public function getBottomRightYOffset() + public function getBottomRightYOffset(): int { return $this->bottomRightYOffset; } @@ -658,4 +698,64 @@ class Chart return $renderer->render($outputDestination); } + + public function getRotX(): ?int + { + return $this->rotX; + } + + public function setRotX(?int $rotX): self + { + $this->rotX = $rotX; + + return $this; + } + + public function getRotY(): ?int + { + return $this->rotY; + } + + public function setRotY(?int $rotY): self + { + $this->rotY = $rotY; + + return $this; + } + + public function getRAngAx(): ?int + { + return $this->rAngAx; + } + + public function setRAngAx(?int $rAngAx): self + { + $this->rAngAx = $rAngAx; + + return $this; + } + + public function getPerspective(): ?int + { + return $this->perspective; + } + + public function setPerspective(?int $perspective): self + { + $this->perspective = $perspective; + + return $this; + } + + public function getOneCellAnchor(): bool + { + return $this->oneCellAnchor; + } + + public function setOneCellAnchor(bool $oneCellAnchor): self + { + $this->oneCellAnchor = $oneCellAnchor; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 067d30e5..dca1186e 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -107,6 +107,13 @@ class DataSeries */ private $plotValues = []; + /** + * Plot Bubble Sizes. + * + * @var DataSeriesValues[] + */ + private $plotBubbleSizes = []; + /** * Create a new DataSeries. * @@ -339,6 +346,28 @@ class DataSeries return false; } + /** + * Get Plot Bubble Sizes. + * + * @return DataSeriesValues[] + */ + public function getPlotBubbleSizes(): array + { + return $this->plotBubbleSizes; + } + + /** + * Set Plot Bubble Sizes. + * + * @param DataSeriesValues[] $plotBubbleSizes + */ + public function setPlotBubbleSizes(array $plotBubbleSizes): self + { + $this->plotBubbleSizes = $plotBubbleSizes; + + return $this; + } + /** * Get Number of Plot Series. * diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 745f0106..6747934a 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -12,7 +12,7 @@ class DataSeriesValues const DATASERIES_TYPE_STRING = 'String'; const DATASERIES_TYPE_NUMBER = 'Number'; - private static $dataTypeValues = [ + private const DATA_TYPE_VALUES = [ self::DATASERIES_TYPE_STRING, self::DATASERIES_TYPE_NUMBER, ]; @@ -27,7 +27,7 @@ class DataSeriesValues /** * Series Data Source. * - * @var string + * @var ?string */ private $dataSource; @@ -69,10 +69,13 @@ class DataSeriesValues /** * Fill color (can be array with colors if dataseries have custom colors). * - * @var string|string[] + * @var null|string|string[] */ private $fillColor; + /** @var string */ + private $schemeClr = ''; + /** * Line Width. * @@ -80,6 +83,12 @@ class DataSeriesValues */ private $lineWidth = 12700; + /** @var bool */ + private $scatterLines = true; + + /** @var bool */ + private $bubble3D = false; + /** * Create a new DataSeriesValues object. * @@ -90,8 +99,9 @@ class DataSeriesValues * @param mixed $dataValues * @param null|mixed $marker * @param null|string|string[] $fillColor + * @param string $pointSize */ - public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null) + public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3') { $this->setDataType($dataType); $this->dataSource = $dataSource; @@ -100,6 +110,9 @@ class DataSeriesValues $this->dataValues = $dataValues; $this->pointMarker = $marker; $this->fillColor = $fillColor; + if (is_numeric($pointSize)) { + $this->pointSize = (int) $pointSize; + } } /** @@ -126,7 +139,7 @@ class DataSeriesValues */ public function setDataType($dataType) { - if (!in_array($dataType, self::$dataTypeValues)) { + if (!in_array($dataType, self::DATA_TYPE_VALUES)) { throw new Exception('Invalid datatype for chart data series values'); } $this->dataType = $dataType; @@ -137,7 +150,7 @@ class DataSeriesValues /** * Get Series Data Source (formula). * - * @return string + * @return ?string */ public function getDataSource() { @@ -147,7 +160,7 @@ class DataSeriesValues /** * Set Series Data Source (formula). * - * @param string $dataSource + * @param ?string $dataSource * * @return $this */ @@ -239,7 +252,7 @@ class DataSeriesValues /** * Get fill color. * - * @return string|string[] HEX color or array with HEX colors + * @return null|string|string[] HEX color or array with HEX colors */ public function getFillColor() { @@ -249,7 +262,7 @@ class DataSeriesValues /** * Set fill color for series. * - * @param string|string[] $color HEX color or array with HEX colors + * @param null|string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ @@ -260,7 +273,7 @@ class DataSeriesValues $this->validateColor($colorValue); } } else { - $this->validateColor($color); + $this->validateColor("$color"); } $this->fillColor = $color; @@ -315,7 +328,7 @@ class DataSeriesValues */ public function isMultiLevelSeries() { - if (count($this->dataValues) > 0) { + if (!empty($this->dataValues)) { return is_array(array_values($this->dataValues)[0]); } @@ -379,7 +392,7 @@ class DataSeriesValues return $this; } - public function refresh(Worksheet $worksheet, $flatten = true): void + public function refresh(Worksheet $worksheet, bool $flatten = true): void { if ($this->dataSource !== null) { $calcEngine = Calculation::getInstance($worksheet->getParent()); @@ -421,4 +434,40 @@ class DataSeriesValues $this->pointCount = count($this->dataValues); } } + + public function getScatterLines(): bool + { + return $this->scatterLines; + } + + public function setScatterLines(bool $scatterLines): self + { + $this->scatterLines = $scatterLines; + + return $this; + } + + public function getBubble3D(): bool + { + return $this->bubble3D; + } + + public function setBubble3D(bool $bubble3D): self + { + $this->bubble3D = $bubble3D; + + return $this; + } + + public function getSchemeClr(): string + { + return $this->schemeClr; + } + + public function setSchemeClr(string $schemeClr): self + { + $this->schemeClr = $schemeClr; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index ef22fb52..9f1a5ce0 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -37,6 +37,7 @@ abstract class Properties const FORMAT_CODE_CURRENCY = '$#,##0.00'; const FORMAT_CODE_ACCOUNTING = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)'; const FORMAT_CODE_DATE = 'm/d/yyyy'; + const FORMAT_CODE_DATE_ISO8601 = 'yyyy-mm-dd'; const FORMAT_CODE_TIME = '[$-F400]h:mm:ss AM/PM'; const FORMAT_CODE_PERCENTAGE = '0.00%'; const FORMAT_CODE_FRACTION = '# ?/?'; diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 2d461c59..afdeea99 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -171,9 +171,9 @@ class Properties if (is_numeric($timestamp)) { $timestamp = (float) $timestamp; } else { - $timestamp = preg_replace('/[.][0-9]*$/', '', $timestamp) ?? ''; - $timestamp = preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp) ?? ''; - $timestamp = preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp) ?? ''; + $timestamp = (string) preg_replace('/[.][0-9]*$/', '', $timestamp); + $timestamp = (string) preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp); + $timestamp = (string) preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp); $timestamp = (float) (new DateTime($timestamp))->format('U'); } } diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index ce5a3d1d..632efebc 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -642,7 +642,7 @@ class Html $text = ltrim($text); } // Trim any spaces immediately after a line break - $text = preg_replace('/\n */mu', "\n", $text); + $text = (string) preg_replace('/\n */mu', "\n", $text); $element->setText($text); } } @@ -792,7 +792,7 @@ class Html protected function parseTextNode(DOMText $textNode): void { - $domText = preg_replace( + $domText = (string) preg_replace( '/\s+/u', ' ', str_replace(["\r", "\n"], ' ', $textNode->nodeValue ?? '') diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 257a02a8..8ce37003 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -85,7 +85,7 @@ class Sample $file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); $info = pathinfo($file); $category = str_replace('_', ' ', $info['dirname']); - $name = str_replace('_', ' ', preg_replace('/(|\.php)/', '', $info['filename'])); + $name = str_replace('_', ' ', (string) preg_replace('/(|\.php)/', '', $info['filename'])); if (!in_array($category, ['.', 'boostrap', 'templates'])) { if (!isset($files[$category])) { $files[$category] = []; diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index b604ceef..65a71edb 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -103,6 +103,9 @@ class Csv extends BaseReader */ protected $preserveNumericFormatting = false; + /** @var bool */ + private $preserveNullString = false; + /** * Create a new CSV Reader instance. */ @@ -300,9 +303,11 @@ class Csv extends BaseReader } } - public function setTestAutoDetect(bool $value): void + public function setTestAutoDetect(bool $value): self { $this->testAutodetect = $value; + + return $this; } private function setAutoDetect(?string $value): ?string @@ -390,7 +395,7 @@ class Csv extends BaseReader foreach ($rowData as $rowDatum) { $this->convertBoolean($rowDatum, $preserveBooleanString); $numberFormatMask = $this->convertFormattedNumber($rowDatum); - if ($rowDatum !== '' && $this->readFilter->readCell($columnLetter, $currentRow)) { + if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($this->contiguous) { if ($noOutputYet) { $noOutputYet = false; @@ -625,4 +630,16 @@ class Csv extends BaseReader return ($encoding === '') ? $dflt : $encoding; } + + public function setPreserveNullString(bool $value): self + { + $this->preserveNullString = $value; + + return $this; + } + + public function getPreserveNullString(): bool + { + return $this->preserveNullString; + } } diff --git a/src/PhpSpreadsheet/Reader/Csv/Delimiter.php b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php index fc298957..029d4a18 100644 --- a/src/PhpSpreadsheet/Reader/Csv/Delimiter.php +++ b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php @@ -140,12 +140,12 @@ class Delimiter $line = $line . $newLine; // Drop everything that is enclosed to avoid counting false positives in enclosures - $line = preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line); + $line = (string) preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line); // See if we have any enclosures left in the line // if we still have an enclosure then we need to read the next line as well - } while (preg_match('/(' . $enclosure . ')/', $line ?? '') > 0); + } while (preg_match('/(' . $enclosure . ')/', $line) > 0); - return $line ?? false; + return ($line !== '') ? $line : false; } } diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index ee2e8d3d..ca087e61 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -272,6 +272,11 @@ class Gnumeric extends BaseReader // name in line with the formula, not the reverse $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + $visibility = $sheetOrNull->attributes()['Visibility'] ?? 'GNM_SHEET_VISIBILITY_VISIBLE'; + if ((string) $visibility !== 'GNM_SHEET_VISIBILITY_VISIBLE') { + $this->spreadsheet->getActiveSheet()->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + } + if (!$this->readDataOnly) { (new PageSetup($this->spreadsheet)) ->printInformation($sheet) diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 437d931d..4edf3cf8 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -619,7 +619,7 @@ class Html extends BaseReader { foreach ($element->childNodes as $child) { if ($child instanceof DOMText) { - $domText = preg_replace('/\s+/u', ' ', trim($child->nodeValue ?? '')); + $domText = (string) preg_replace('/\s+/u', ' ', trim($child->nodeValue ?? '')); if (is_string($cellContent)) { // simply append the text if the cell content is a plain text string $cellContent .= $domText; diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 27c58edb..7e776ab7 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -588,6 +588,7 @@ class Ods extends BaseReader break; } } + $pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); ++$worksheetID; } diff --git a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php index f2ad1a3d..4abdf11e 100644 --- a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php +++ b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php @@ -13,17 +13,25 @@ class FormulaTranslator // Cell range 3-d reference // As we don't support 3-d ranges, we're just going to take a quick and dirty approach // and assume that the second worksheet reference is the same as the first - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); - // Cell range reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress ?? ''); - // Cell reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress ?? ''); - // Cell range reference - $excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress ?? ''); - // Simple cell reference - $excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress ?? ''); + $excelAddress = (string) preg_replace( + [ + '/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', + '/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', // Cell range reference in another sheet + '/\$?([^\.]+)\.([^\.]+)/miu', // Cell reference in another sheet + '/\.([^\.]+):\.([^\.]+)/miu', // Cell range reference + '/\.([^\.]+)/miu', // Simple cell reference + ], + [ + '$1!$2:$4', + '$1!$2:$3', + '$1!$2', + '$1:$2', + '$1', + ], + $excelAddress + ); - return $excelAddress ?? ''; + return $excelAddress; } public static function convertToExcelFormulaValue(string $openOfficeFormula): string @@ -37,16 +45,23 @@ class FormulaTranslator // Only replace in alternate array entries (i.e. non-quoted blocks) // so that conversion isn't done in string values if ($tKey = !$tKey) { - // Cell range reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); - // Cell reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); - // Cell range reference - $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); - // Simple cell reference - $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + $value = (string) preg_replace( + [ + '/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference in another sheet + '/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet + '/\[\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference + '/\[\.([^\.]+)\]/miu', // Simple cell reference + ], + [ + '$1!$2:$3', + '$1!$2', + '$1:$2', + '$1', + ], + $value + ); // Convert references to defined names/formulae - $value = str_replace('$$', '', $value ?? ''); + $value = str_replace('$$', '', $value); // Convert ODS function argument separators to Excel function argument separators $value = Calculation::translateSeparator(';', ',', $value, $inFunctionBracesLevel); @@ -69,7 +84,7 @@ class FormulaTranslator Calculation::FORMULA_CLOSE_MATRIX_BRACE ); - $value = preg_replace('/COM\.MICROSOFT\./ui', '', $value); + $value = (string) preg_replace('/COM\.MICROSOFT\./ui', '', $value); } } diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php index 8d24fd0c..4d2fd992 100644 --- a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -8,16 +8,41 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PageSettings { + /** + * @var string + */ private $officeNs; + /** + * @var string + */ private $stylesNs; + /** + * @var string + */ private $stylesFo; + /** + * @var string + */ + private $tableNs; + + /** + * @var string[] + */ + private $tableStylesCrossReference = []; + private $pageLayoutStyles = []; + /** + * @var string[] + */ private $masterStylesCrossReference = []; + /** + * @var string[] + */ private $masterPrintStylesCrossReference = []; public function __construct(DOMDocument $styleDom) @@ -32,6 +57,7 @@ class PageSettings $this->officeNs = $styleDom->lookupNamespaceUri('office'); $this->stylesNs = $styleDom->lookupNamespaceUri('style'); $this->stylesFo = $styleDom->lookupNamespaceUri('fo'); + $this->tableNs = $styleDom->lookupNamespaceUri('table'); } private function readPageSettingStyles(DOMDocument $styleDom): void @@ -98,12 +124,33 @@ class PageSettings foreach ($styleXReferences as $styleXreferenceSet) { $styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name'); $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name'); + $styleFamilyName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'family'); + if (!empty($styleFamilyName) && $styleFamilyName === 'table') { + $styleVisibility = 'true'; + foreach ($styleXreferenceSet->getElementsByTagNameNS($this->stylesNs, 'table-properties') as $tableProperties) { + $styleVisibility = $tableProperties->getAttributeNS($this->tableNs, 'display'); + } + $this->tableStylesCrossReference[$styleXRefName] = $styleVisibility; + } if (!empty($stylePageLayoutName)) { $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; } } } + public function setVisibilityForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->tableStylesCrossReference)) { + return; + } + + $worksheet->setSheetState( + $this->tableStylesCrossReference[$styleName] === 'false' + ? Worksheet::SHEETSTATE_HIDDEN + : Worksheet::SHEETSTATE_VISIBLE + ); + } + public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void { if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index af1104f5..10f12dfe 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4521,17 +4521,17 @@ class Xls extends BaseReader // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!) if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) { - $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells); + $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells); } // first row '1' + last row '65536' indicates that full column is selected if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) { - $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells); + $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells); } // first column 'A' + last column 'IV' indicates that full row is selected if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) { - $selectedCells = preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells); + $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells); } $this->phpSheet->setSelectedCells($selectedCells); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 166d9576..4cb487a6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -158,6 +158,10 @@ class Xlsx extends BaseReader Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING, ]; + private const REL_TO_CHART = [ + Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART, + ]; + /** * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. * @@ -227,7 +231,10 @@ class Xlsx extends BaseReader $worksheets = []; foreach ($relsWorkbook->Relationship as $elex) { $ele = self::getAttributes($elex); - if ((string) $ele['Type'] === "$namespace/worksheet") { + if ( + ((string) $ele['Type'] === "$namespace/worksheet") || + ((string) $ele['Type'] === "$namespace/chartsheet") + ) { $worksheets[(string) $ele['Id']] = $ele['Target']; } } @@ -406,17 +413,21 @@ class Xlsx extends BaseReader // Read the theme first, because we need the colour scheme when reading the styles [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); + $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; + $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS); $theme = null; $this->styleReader = new Styles(); foreach ($wbRels->Relationship as $relx) { $rel = self::getAttributes($relx); $relTarget = (string) $rel['Target']; + if (substr($relTarget, 0, 4) === '/xl/') { + $relTarget = substr($relTarget, 4); + } switch ($rel['Type']) { case "$xmlNamespaceBase/theme": $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; $themeOrderAdditional = count($themeOrderArray); - $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS); $xmlThemeName = self::getAttributes($xmlTheme); @@ -513,6 +524,12 @@ class Xlsx extends BaseReader case Namespaces::PURL_WORKSHEET: $worksheets[(string) $ele['Id']] = $ele['Target']; + break; + case Namespaces::CHARTSHEET: + if ($this->includeCharts === true) { + $worksheets[(string) $ele['Id']] = $ele['Target']; + } + break; // a vbaProject ? (: some macros) case Namespaces::VBA: @@ -691,6 +708,13 @@ class Xlsx extends BaseReader continue; } + $sheetReferenceId = (string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id'); + if (isset($worksheets[$sheetReferenceId]) === false) { + ++$countSkippedSheets; + $mapSheetId[$oldSheetId] = null; + + continue; + } // Map old sheet id in original workbook to new sheet id. // They will differ if loadSheetsOnly() is being used $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets; @@ -702,7 +726,8 @@ class Xlsx extends BaseReader // and we're simply bringing the worksheet name in line with the formula, not the // reverse $docSheet->setTitle((string) $eleSheetAttr['name'], false, false); - $fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id')]; + + $fileWorksheet = (string) $worksheets[$sheetReferenceId]; $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS); $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS); @@ -1189,14 +1214,23 @@ class Xlsx extends BaseReader . '/_rels/' . basename($fileWorksheet) . '.rels'; + if (substr($drawingFilename, 0, 7) === 'xl//xl/') { + $drawingFilename = substr($drawingFilename, 4); + } if ($zip->locateName($drawingFilename)) { $relsWorksheet = $this->loadZipNoNamespace($drawingFilename, Namespaces::RELATIONSHIPS); $drawings = []; foreach ($relsWorksheet->Relationship as $ele) { if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") { - $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + $eleTarget = (string) $ele['Target']; + if (substr($eleTarget, 0, 4) === '/xl/') { + $drawings[(string) $ele['Id']] = substr($eleTarget, 1); + } else { + $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + } } } + if ($xmlSheet->drawing && !$this->readDataOnly) { $unparsedDrawings = []; $fileDrawing = null; @@ -1205,6 +1239,7 @@ class Xlsx extends BaseReader $fileDrawing = $drawings[$drawingRelId]; $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels'; $relsDrawing = $this->loadZipNoNamespace($drawingFilename, $xmlNamespaceBase); + $images = []; $hyperlinks = []; if ($relsDrawing && $relsDrawing->Relationship) { @@ -1217,7 +1252,13 @@ class Xlsx extends BaseReader $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']); } elseif ($eleType === "$xmlNamespaceBase/chart") { if ($this->includeCharts) { - $charts[self::dirAdd($fileDrawing, $ele['Target'])] = [ + $eleTarget = (string) $ele['Target']; + if (substr($eleTarget, 0, 4) === '/xl/') { + $index = substr($eleTarget, 1); + } else { + $index = self::dirAdd($fileDrawing, $eleTarget); + } + $charts[$index] = [ 'id' => (string) $ele['Id'], 'sheet' => $docSheet->getTitle(), ]; @@ -1225,6 +1266,7 @@ class Xlsx extends BaseReader } } } + $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, ''); $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING); @@ -1308,6 +1350,7 @@ class Xlsx extends BaseReader 'width' => $width, 'height' => $height, 'worksheetTitle' => $docSheet->getTitle(), + 'oneCellAnchor' => true, ]; } } @@ -1403,6 +1446,27 @@ class Xlsx extends BaseReader } } } + if ($xmlDrawingChildren->absoluteAnchor) { + foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) { + if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) { + $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; + /** @var SimpleXMLElement $chartRef */ + $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; + $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); + $width = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cx')[0]); + $height = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cy')[0]); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => 'A1', + 'fromOffsetX' => 0, + 'fromOffsetY' => 0, + 'width' => $width, + 'height' => $height, + 'worksheetTitle' => $docSheet->getTitle(), + ]; + } + } + } if (empty($relsDrawing) && $xmlDrawing->count() == 0) { // Save Drawing without rels and children as unparsed $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML(); @@ -1422,7 +1486,7 @@ class Xlsx extends BaseReader } // unparsed drawing AlternateContent - $xmlAltDrawing = $this->loadZip($fileDrawing, Namespaces::COMPATIBILITY); + $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY); if ($xmlAltDrawing->AlternateContent) { foreach ($xmlAltDrawing->AlternateContent as $alternateContent) { @@ -1491,13 +1555,18 @@ class Xlsx extends BaseReader $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $newRangeSets = []; foreach ($rangeSets as $rangeSet) { - [$sheetName, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + if (empty($rangeSet)) { + continue; + } if (strpos($rangeSet, ':') === false) { $rangeSet = $rangeSet . ':' . $rangeSet; } $newRangeSets[] = str_replace('$', '', $rangeSet); } - $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + if (count($newRangeSets) > 0) { + $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + } break; default: @@ -1601,17 +1670,26 @@ class Xlsx extends BaseReader if ($this->includeCharts) { $chartEntryRef = ltrim((string) $contentType['PartName'], '/'); $chartElements = $this->loadZip($chartEntryRef); - $objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml')); - + $chartReader = new Chart($chartNS, $drawingNS); + $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml')); if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; if (isset($chartDetails[$chartPositionRef])) { $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); - $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + // For oneCellAnchor or absoluteAnchor positioned charts, + // toCoordinate is not in the data. Does it need to be calculated? if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) { - // For oneCellAnchor positioned charts, toCoordinate is not in the data. Does it need to be calculated? + // twoCellAnchor + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); + } else { + // oneCellAnchor or absoluteAnchor (e.g. Chart sheet) + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']); + if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) { + $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']); + } } } } @@ -1771,12 +1849,17 @@ class Xlsx extends BaseReader return $array[$key] ?? null; } - private static function dirAdd($base, $add) + private static function dirAdd($base, $add): string { - return preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); + $add = "$add"; + if (substr($add, 0, 4) === '/xl/') { + $add = substr($add, 4); + } + + return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } - private static function toCSSArray($style) + private static function toCSSArray($style): array { $style = self::stripWhiteSpaceFromStyleString($style); @@ -1807,12 +1890,12 @@ class Xlsx extends BaseReader return $style; } - public static function stripWhiteSpaceFromStyleString($string) + public static function stripWhiteSpaceFromStyleString($string): string { return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); } - private static function boolean($value) + private static function boolean($value): bool { if (is_object($value)) { $value = (string) $value; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index fdc56ce1..374da9f6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -22,7 +22,7 @@ class AutoFilter public function load(): void { // Remove all "$" in the auto filter range - $autoFilterRange = preg_replace('/\$/', '', $this->worksheetXml->autoFilter['ref'] ?? ''); + $autoFilterRange = (string) preg_replace('/\$/', '', $this->worksheetXml->autoFilter['ref'] ?? ''); if (strpos($autoFilterRange, ':') !== false) { $this->readAutoFilter($autoFilterRange, $this->worksheetXml); } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 680335b7..98507af8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -16,6 +16,18 @@ use SimpleXMLElement; class Chart { + /** @var string */ + private $cNamespace; + + /** @var string */ + private $aNamespace; + + public function __construct(string $cNamespace = Namespaces::CHART, string $aNamespace = Namespaces::DRAWINGML) + { + $this->cNamespace = $cNamespace; + $this->aNamespace = $aNamespace; + } + /** * @param string $name * @param string $format @@ -25,7 +37,7 @@ class Chart private static function getAttribute(SimpleXMLElement $component, $name, $format) { $attributes = $component->attributes(); - if (isset($attributes[$name])) { + if (@isset($attributes[$name])) { if ($format == 'string') { return (string) $attributes[$name]; } elseif ($format == 'integer') { @@ -42,58 +54,56 @@ class Chart return null; } - private static function readColor($color, $background = false) - { - if (isset($color['rgb'])) { - return (string) $color['rgb']; - } elseif (isset($color['indexed'])) { - return Color::indexedColor($color['indexed'] - 7, $background)->getARGB(); - } - } - /** * @param string $chartName * * @return \PhpOffice\PhpSpreadsheet\Chart\Chart */ - public static function readChart(SimpleXMLElement $chartElements, $chartName) + public function readChart(SimpleXMLElement $chartElements, $chartName) { - $namespacesChartMeta = $chartElements->getNamespaces(true); - $chartElementsC = $chartElements->children($namespacesChartMeta['c']); + $chartElementsC = $chartElements->children($this->cNamespace); $XaxisLabel = $YaxisLabel = $legend = $title = null; $dispBlanksAs = $plotVisOnly = null; $plotArea = null; + $rotX = $rotY = $rAngAx = $perspective = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { - $chartDetailsC = $chartDetails->children($namespacesChartMeta['c']); + $chartDetailsC = $chartDetails->children($this->cNamespace); switch ($chartDetailsKey) { + case 'view3D': + $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer'); + $rotY = self::getAttribute($chartDetails->rotY, 'val', 'integer'); + $rAngAx = self::getAttribute($chartDetails->rAngAx, 'val', 'integer'); + $perspective = self::getAttribute($chartDetails->perspective, 'val', 'integer'); + + break; case 'plotArea': $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { case 'layout': - $plotAreaLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $plotAreaLayout = $this->chartLayoutDetails($chartDetail); break; case 'catAx': if (isset($chartDetail->title)) { - $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } break; case 'dateAx': if (isset($chartDetail->title)) { - $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } break; case 'valAx': if (isset($chartDetail->title, $chartDetail->axPos)) { - $axisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + $axisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string'); switch ($axPos) { @@ -114,70 +124,72 @@ class Chart case 'barChart': case 'bar3DChart': $barDirection = self::getAttribute($chartDetail->barDir, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotDirection($barDirection); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotDirection("$barDirection"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'lineChart': case 'line3DChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($chartDetail); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'areaChart': case 'area3DChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($chartDetail); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'doughnutChart': case 'pieChart': case 'pie3DChart': $explosion = isset($chartDetail->ser->explosion); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($explosion); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$explosion"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'scatterChart': + /** @var string */ $scatterStyle = self::getAttribute($chartDetail->scatterStyle, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle($scatterStyle); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'bubbleChart': $bubbleScale = self::getAttribute($chartDetail->bubbleScale, 'val', 'integer'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($bubbleScale); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$bubbleScale"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'radarChart': + /** @var string */ $radarStyle = self::getAttribute($chartDetail->radarStyle, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle($radarStyle); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'surfaceChart': case 'surface3DChart': $wireFrame = self::getAttribute($chartDetail->wireframe, 'val', 'boolean'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($wireFrame); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$wireFrame"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'stockChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($plotAreaLayout); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($plotAreaLayout); break; } @@ -186,7 +198,7 @@ class Chart $plotAreaLayout = new Layout(); } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); - self::setChartAttributes($plotAreaLayout, $plotAttributes); + $this->setChartAttributes($plotAreaLayout, $plotAttributes); break; case 'plotVisOnly': @@ -198,7 +210,7 @@ class Chart break; case 'title': - $title = self::chartTitle($chartDetails, $namespacesChartMeta); + $title = $this->chartTitle($chartDetails); break; case 'legend': @@ -216,42 +228,54 @@ class Chart break; case 'layout': - $legendLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $legendLayout = $this->chartLayoutDetails($chartDetail); break; } } - $legend = new Legend($legendPos, $legendLayout, $legendOverlay); + $legend = new Legend("$legendPos", $legendLayout, (bool) $legendOverlay); break; } } } } - $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, $dispBlanksAs, $XaxisLabel, $YaxisLabel); + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel); + if (is_int($rotX)) { + $chart->setRotX($rotX); + } + if (is_int($rotY)) { + $chart->setRotY($rotY); + } + if (is_int($rAngAx)) { + $chart->setRAngAx($rAngAx); + } + if (is_int($perspective)) { + $chart->setPerspective($perspective); + } return $chart; } - private static function chartTitle(SimpleXMLElement $titleDetails, array $namespacesChartMeta) + private function chartTitle(SimpleXMLElement $titleDetails): Title { $caption = []; $titleLayout = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { switch ($titleDetailKey) { case 'tx': - $titleDetails = $chartDetail->rich->children($namespacesChartMeta['a']); + $titleDetails = $chartDetail->rich->children($this->aNamespace); foreach ($titleDetails as $titleKey => $titleDetail) { switch ($titleKey) { case 'p': - $titleDetailPart = $titleDetail->children($namespacesChartMeta['a']); - $caption[] = self::parseRichText($titleDetailPart); + $titleDetailPart = $titleDetail->children($this->aNamespace); + $caption[] = $this->parseRichText($titleDetailPart); } } break; case 'layout': - $titleLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $titleLayout = $this->chartLayoutDetails($chartDetail); break; } @@ -260,12 +284,12 @@ class Chart return new Title($caption, $titleLayout); } - private static function chartLayoutDetails($chartDetail, $namespacesChartMeta) + private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout { if (!isset($chartDetail->manualLayout)) { return null; } - $details = $chartDetail->manualLayout->children($namespacesChartMeta['c']); + $details = $chartDetail->manualLayout->children($this->cNamespace); if ($details === null) { return null; } @@ -277,13 +301,13 @@ class Chart return new Layout($layout); } - private static function chartDataSeries($chartDetail, $namespacesChartMeta, $plotType) + private function chartDataSeries(SimpleXMLElement $chartDetail, string $plotType): DataSeries { $multiSeriesType = null; $smoothLine = false; - $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = []; + $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = $seriesBubbles = []; - $seriesDetailSet = $chartDetail->children($namespacesChartMeta['c']); + $seriesDetailSet = $chartDetail->children($this->cNamespace); foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) { switch ($seriesDetailKey) { case 'grouping': @@ -293,6 +317,12 @@ class Chart case 'ser': $marker = null; $seriesIndex = ''; + $srgbClr = null; + $lineWidth = null; + $pointSize = null; + $noFill = false; + $schemeClr = ''; + $bubble3D = false; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -305,11 +335,32 @@ class Chart break; case 'tx': - $seriesLabel[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + $seriesLabel[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail); + + break; + case 'spPr': + $children = $seriesDetail->children($this->aNamespace); + $ln = $children->ln; + $lineWidth = self::getAttribute($ln, 'w', 'string'); + if (is_countable($ln->noFill) && count($ln->noFill) === 1) { + $noFill = true; + } + $sf = $children->solidFill->schemeClr; + if ($sf) { + $schemeClr = self::getAttribute($sf, 'val', 'string'); + } break; case 'marker': $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); + $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string'); + $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; + if (count($seriesDetail->spPr) === 1) { + $ln = $seriesDetail->spPr->children($this->aNamespace); + if (count($ln->solidFill) === 1) { + $srgbClr = self::getAttribute($ln->solidFill->srgbClr, 'val', 'string'); + } + } break; case 'smooth': @@ -317,37 +368,95 @@ class Chart break; case 'cat': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail); break; case 'val': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); break; case 'xVal': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); break; case 'yVal': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + + break; + case 'bubbleSize': + $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + + break; + case 'bubble3D': + $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean'); break; } } + if ($noFill) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setScatterLines(false); + } + } + if (is_numeric($lineWidth)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setLineWidth((int) $lineWidth); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setLineWidth((int) $lineWidth); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setLineWidth((int) $lineWidth); + } + } + if ($schemeClr) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setSchemeClr($schemeClr); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setSchemeClr($schemeClr); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setSchemeClr($schemeClr); + } + } + if ($bubble3D) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setBubble3D($bubble3D); + } + } } } + /** @phpstan-ignore-next-line */ + $series = new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); + $series->setPlotBubbleSizes($seriesBubbles); - return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); + return $series; } - private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker = null) + /** + * @return mixed + */ + private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?string $srgbClr = null, ?string $pointSize = null) { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->strRef->strCache)) { - $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -356,9 +465,9 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->numRef->numCache)) { - $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); + $seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace)); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -367,10 +476,10 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -379,10 +488,10 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -391,10 +500,20 @@ class Chart return $seriesValues; } + if (isset($seriesDetail->v)) { + return new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + null, + null, + 1, + [(string) $seriesDetail->v] + ); + } + return null; } - private static function chartDataSeriesValues($seriesValueSet, $dataType = 'n') + private function chartDataSeriesValues(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array { $seriesVal = []; $formatCode = ''; @@ -414,7 +533,7 @@ class Chart $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); if ($dataType == 's') { $seriesVal[$pointVal] = (string) $seriesValue->v; - } elseif ($seriesValue->v === ExcelError::NA()) { + } elseif ((string) $seriesValue->v === ExcelError::NA()) { $seriesVal[$pointVal] = null; } else { $seriesVal[$pointVal] = (float) $seriesValue->v; @@ -431,7 +550,7 @@ class Chart ]; } - private static function chartDataSeriesValuesMultiLevel($seriesValueSet, $dataType = 'n') + private function chartDataSeriesValuesMultiLevel(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array { $seriesVal = []; $formatCode = ''; @@ -452,7 +571,7 @@ class Chart $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); if ($dataType == 's') { $seriesVal[$pointVal][] = (string) $seriesValue->v; - } elseif ($seriesValue->v === ExcelError::NA()) { + } elseif ((string) $seriesValue->v === ExcelError::NA()) { $seriesVal[$pointVal] = null; } else { $seriesVal[$pointVal][] = (float) $seriesValue->v; @@ -470,74 +589,227 @@ class Chart ]; } - private static function parseRichText(SimpleXMLElement $titleDetailPart) + private function parseRichText(SimpleXMLElement $titleDetailPart): RichText { $value = new RichText(); $objText = null; - foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { - if (isset($titleDetailElement->t)) { - $objText = $value->createTextRun((string) $titleDetailElement->t); + $defaultFontSize = null; + $defaultBold = null; + $defaultItalic = null; + $defaultUnderscore = null; + $defaultStrikethrough = null; + $defaultBaseline = null; + $defaultFontName = null; + $defaultLatin = null; + $defaultEastAsian = null; + $defaultComplexScript = null; + $defaultColor = null; + if (isset($titleDetailPart->pPr->defRPr)) { + /** @var ?int */ + $defaultFontSize = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer'); + /** @var ?bool */ + $defaultBold = self::getAttribute($titleDetailPart->pPr->defRPr, 'b', 'boolean'); + /** @var ?bool */ + $defaultItalic = self::getAttribute($titleDetailPart->pPr->defRPr, 'i', 'boolean'); + /** @var ?string */ + $defaultUnderscore = self::getAttribute($titleDetailPart->pPr->defRPr, 'u', 'string'); + /** @var ?string */ + $defaultStrikethrough = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string'); + /** @var ?int */ + $defaultBaseline = self::getAttribute($titleDetailPart->pPr->defRPr, 'baseline', 'integer'); + if (isset($titleDetailPart->defRPr->rFont['val'])) { + $defaultFontName = (string) $titleDetailPart->defRPr->rFont['val']; } + if (isset($titleDetailPart->pPr->defRPr->latin)) { + /** @var ?string */ + $defaultLatin = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->ea)) { + /** @var ?string */ + $defaultEastAsian = self::getAttribute($titleDetailPart->pPr->defRPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->cs)) { + /** @var ?string */ + $defaultComplexScript = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->solidFill->srgbClr)) { + /** @var ?string */ + $defaultColor = self::getAttribute($titleDetailPart->pPr->defRPr->solidFill->srgbClr, 'val', 'string'); + } + } + foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { + if ( + (string) $titleDetailElementKey !== 'r' + || !isset($titleDetailElement->t) + ) { + continue; + } + $objText = $value->createTextRun((string) $titleDetailElement->t); + if ($objText->getFont() === null) { + // @codeCoverageIgnoreStart + continue; + // @codeCoverageIgnoreEnd + } + $fontSize = null; + $bold = null; + $italic = null; + $underscore = null; + $strikethrough = null; + $baseline = null; + $fontName = null; + $latinName = null; + $eastAsian = null; + $complexScript = null; + $fontColor = null; + $uSchemeClr = null; if (isset($titleDetailElement->rPr)) { + // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { - $objText->getFont()->setName((string) $titleDetailElement->rPr->rFont['val']); - } - - $fontSize = (self::getAttribute($titleDetailElement->rPr, 'sz', 'integer')); - if (is_int($fontSize)) { - $objText->getFont()->setSize(floor($fontSize / 100)); - } - - $fontColor = (self::getAttribute($titleDetailElement->rPr, 'color', 'string')); - if ($fontColor !== null) { - $objText->getFont()->setColor(new Color(self::readColor($fontColor))); + // @codeCoverageIgnoreStart + $fontName = (string) $titleDetailElement->rPr->rFont['val']; + // @codeCoverageIgnoreEnd + } + if (isset($titleDetailElement->rPr->latin)) { + /** @var ?string */ + $latinName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->ea)) { + /** @var ?string */ + $eastAsian = self::getAttribute($titleDetailElement->rPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->cs)) { + /** @var ?string */ + $complexScript = self::getAttribute($titleDetailElement->rPr->cs, 'typeface', 'string'); + } + /** @var ?int */ + $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); + + // not used now, not sure it ever was, grandfathering + /** @var ?string */ + $fontColor = self::getAttribute($titleDetailElement->rPr, 'color', 'string'); + if (isset($titleDetailElement->rPr->solidFill->srgbClr)) { + /** @var ?string */ + $fontColor = self::getAttribute($titleDetailElement->rPr->solidFill->srgbClr, 'val', 'string'); } + /** @var ?bool */ $bold = self::getAttribute($titleDetailElement->rPr, 'b', 'boolean'); - if ($bold !== null) { - $objText->getFont()->setBold($bold); - } + /** @var ?bool */ $italic = self::getAttribute($titleDetailElement->rPr, 'i', 'boolean'); - if ($italic !== null) { - $objText->getFont()->setItalic($italic); - } + /** @var ?int */ $baseline = self::getAttribute($titleDetailElement->rPr, 'baseline', 'integer'); - if ($baseline !== null) { - if ($baseline > 0) { - $objText->getFont()->setSuperscript(true); - } elseif ($baseline < 0) { - $objText->getFont()->setSubscript(true); - } + + /** @var ?string */ + $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); + if (isset($titleDetailElement->rPr->uFill->solidFill->schemeClr)) { + /** @var ?string */ + $uSchemeClr = self::getAttribute($titleDetailElement->rPr->uFill->solidFill->schemeClr, 'val', 'string'); } - $underscore = (self::getAttribute($titleDetailElement->rPr, 'u', 'string')); - if ($underscore !== null) { - if ($underscore == 'sng') { - $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); - } elseif ($underscore == 'dbl') { - $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); - } else { - $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); - } - } + /** @var ?string */ + $strikethrough = self::getAttribute($titleDetailElement->rPr, 'strike', 'string'); + } - $strikethrough = (self::getAttribute($titleDetailElement->rPr, 's', 'string')); - if ($strikethrough !== null) { - if ($strikethrough == 'noStrike') { - $objText->getFont()->setStrikethrough(false); - } else { - $objText->getFont()->setStrikethrough(true); - } + $fontFound = false; + $latinName = $latinName ?? $defaultLatin; + if ($latinName !== null) { + $objText->getFont()->setLatin($latinName); + $fontFound = true; + } + $eastAsian = $eastAsian ?? $defaultEastAsian; + if ($eastAsian !== null) { + $objText->getFont()->setEastAsian($eastAsian); + $fontFound = true; + } + $complexScript = $complexScript ?? $defaultComplexScript; + if ($complexScript !== null) { + $objText->getFont()->setComplexScript($complexScript); + $fontFound = true; + } + $fontName = $fontName ?? $defaultFontName; + if ($fontName !== null) { + // @codeCoverageIgnoreStart + $objText->getFont()->setName($fontName); + $fontFound = true; + // @codeCoverageIgnoreEnd + } + + $fontSize = $fontSize ?? $defaultFontSize; + if (is_int($fontSize)) { + $objText->getFont()->setSize(floor($fontSize / 100)); + $fontFound = true; + } + + $fontColor = $fontColor ?? $defaultColor; + if ($fontColor !== null) { + $objText->getFont()->setColor(new Color($fontColor)); + $fontFound = true; + } + + $bold = $bold ?? $defaultBold; + if ($bold !== null) { + $objText->getFont()->setBold($bold); + $fontFound = true; + } + + $italic = $italic ?? $defaultItalic; + if ($italic !== null) { + $objText->getFont()->setItalic($italic); + $fontFound = true; + } + + $baseline = $baseline ?? $defaultBaseline; + if ($baseline !== null) { + $objText->getFont()->setBaseLine($baseline); + if ($baseline > 0) { + $objText->getFont()->setSuperscript(true); + } elseif ($baseline < 0) { + $objText->getFont()->setSubscript(true); } + $fontFound = true; + } + + $underscore = $underscore ?? $defaultUnderscore; + if ($underscore !== null) { + if ($underscore == 'sng') { + $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + } elseif ($underscore == 'dbl') { + $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } elseif ($underscore !== '') { + $objText->getFont()->setUnderline($underscore); + } else { + $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); + } + $fontFound = true; + if ($uSchemeClr) { + $objText->getFont()->setUSchemeClr($uSchemeClr); + } + } + + $strikethrough = $strikethrough ?? $defaultStrikethrough; + if ($strikethrough !== null) { + $objText->getFont()->setStrikeType($strikethrough); + if ($strikethrough == 'noStrike') { + $objText->getFont()->setStrikethrough(false); + } else { + $objText->getFont()->setStrikethrough(true); + } + $fontFound = true; + } + if ($fontFound === false) { + $objText->setFont(null); } } return $value; } - private static function readChartAttributes($chartDetail) + /** + * @param null|Layout|SimpleXMLElement $chartDetail + */ + private function readChartAttributes($chartDetail): array { $plotAttributes = []; if (isset($chartDetail->dLbls)) { @@ -570,7 +842,7 @@ class Chart /** * @param mixed $plotAttributes */ - private static function setChartAttributes(Layout $plotArea, $plotAttributes): void + private function setChartAttributes(Layout $plotArea, $plotAttributes): void { foreach ($plotAttributes as $plotAttributeKey => $plotAttributeValue) { switch ($plotAttributeKey) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index c0713ae4..57a88bb0 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -50,6 +50,8 @@ class Namespaces const WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; + const CHARTSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet'; + const SCHEMA_MICROSOFT = 'http://schemas.microsoft.com/office/2006/relationships'; const EXTENSIBILITY = 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility'; @@ -74,5 +76,7 @@ class Namespaces const PURL_DRAWING = 'http://purl.oclc.org/ooxml/drawingml/main'; + const PURL_CHART = 'http://purl.oclc.org/ooxml/drawingml/chart'; + const PURL_WORKSHEET = 'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet'; } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index bac40961..0b5e0966 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -90,9 +90,9 @@ class Xml extends BaseReader // Retrieve charset encoding if (preg_match('//m', $data, $matches)) { $charSet = strtoupper($matches[1]); - if (1 == preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet)) { + if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) { $data = StringHelper::convertEncoding($data, 'UTF-8', $charSet); - $data = preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); + $data = (string) preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); } } $this->fileContents = $data; diff --git a/src/PhpSpreadsheet/Reader/Xml/Properties.php b/src/PhpSpreadsheet/Reader/Xml/Properties.php index 1c3e421a..9e10526e 100644 --- a/src/PhpSpreadsheet/Reader/Xml/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xml/Properties.php @@ -44,7 +44,7 @@ class Properties foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { $propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']); - $propertyName = preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); + $propertyName = (string) preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); $this->processCustomProperty($docProps, $propertyName, $propertyValue, $propertyAttributes); } diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 4a6ee039..046c5894 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -687,7 +687,7 @@ class ReferenceHelper ksort($cellTokens); ksort($newCellTokens); } // Update cell references in the formula - $formulaBlock = str_replace('\\', '', preg_replace($cellTokens, $newCellTokens, $formulaBlock)); + $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock)); } } } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php index 7d520120..e0dc0efb 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php @@ -219,7 +219,7 @@ class CellMatcher foreach ($splitCondition as &$value) { // Only count/replace in alternating array entries (ie. not in quoted strings) if ($i = !$i) { - $value = preg_replace_callback( + $value = (string) preg_replace_callback( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', [$this, 'conditionCellAdjustment'], $value @@ -287,7 +287,7 @@ class CellMatcher $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); $expression = array_pop($conditions); - $expression = preg_replace( + $expression = (string) preg_replace( '/\b' . $this->referenceCell . '\b/i', (string) $this->wrapCellValue(), $expression diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php index df9daab3..3eb7d54e 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php @@ -133,7 +133,7 @@ abstract class WizardAbstract foreach ($splitCondition as &$value) { // Only count/replace in alternating array entries (ie. not in quoted strings) if ($i = !$i) { - $value = preg_replace_callback( + $value = (string) preg_replace_callback( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', function ($matches) use ($referenceColumnIndex, $referenceRow) { return self::reverseCellAdjustment($matches, $referenceColumnIndex, $referenceRow); @@ -174,7 +174,7 @@ abstract class WizardAbstract foreach ($splitCondition as &$value) { // Only count/replace in alternating array entries (ie. not in quoted strings) if ($i = !$i) { - $value = preg_replace_callback( + $value = (string) preg_replace_callback( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', [$this, 'conditionCellAdjustment'], $value diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 13fe2b67..e5b056c9 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -18,6 +18,29 @@ class Font extends Supervisor */ protected $name = 'Calibri'; + /** + * The following 6 are used only for chart titles, I think. + * + *@var string + */ + private $latin = ''; + + /** @var string */ + private $eastAsian = ''; + + /** @var string */ + private $complexScript = ''; + + /** @var int */ + private $baseLine = 0; + + /** @var string */ + private $strikeType = ''; + + /** @var string */ + private $uSchemeClr = ''; + // end of chart title items + /** * Font Size. * @@ -170,6 +193,15 @@ class Font extends Supervisor if (isset($styleArray['name'])) { $this->setName($styleArray['name']); } + if (isset($styleArray['latin'])) { + $this->setLatin($styleArray['latin']); + } + if (isset($styleArray['eastAsian'])) { + $this->setEastAsian($styleArray['eastAsian']); + } + if (isset($styleArray['complexScript'])) { + $this->setComplexScript($styleArray['complexScript']); + } if (isset($styleArray['bold'])) { $this->setBold($styleArray['bold']); } @@ -213,6 +245,33 @@ class Font extends Supervisor return $this->name; } + public function getLatin(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getLatin(); + } + + return $this->latin; + } + + public function getEastAsian(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getEastAsian(); + } + + return $this->eastAsian; + } + + public function getComplexScript(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getComplexScript(); + } + + return $this->complexScript; + } + /** * Set Name. * @@ -235,6 +294,51 @@ class Font extends Supervisor return $this; } + public function setLatin(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['latin' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->latin = $fontname; + } + + return $this; + } + + public function setEastAsian(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['eastAsian' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->eastAsian = $fontname; + } + + return $this; + } + + public function setComplexScript(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['complexScript' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->complexScript = $fontname; + } + + return $this; + } + /** * Get Size. * @@ -418,6 +522,69 @@ class Font extends Supervisor return $this; } + public function getBaseLine(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBaseLine(); + } + + return $this->baseLine; + } + + public function setBaseLine(int $baseLine): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['baseLine' => $baseLine]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->baseLine = $baseLine; + } + + return $this; + } + + public function getStrikeType(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikeType(); + } + + return $this->strikeType; + } + + public function setStrikeType(string $strikeType): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['strikeType' => $strikeType]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->strikeType = $strikeType; + } + + return $this; + } + + public function getUSchemeClr(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUSchemeClr(); + } + + return $this->uSchemeClr; + } + + public function setUSchemeClr(string $uSchemeClr): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['uSchemeClr' => $uSchemeClr]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->uSchemeClr = $uSchemeClr; + } + + return $this; + } + /** * Get Underline. * @@ -546,6 +713,15 @@ class Font extends Supervisor $this->underline . ($this->strikethrough ? 't' : 'f') . $this->color->getHashCode() . + '*' . + $this->latin . + '*' . + $this->eastAsian . + '*' . + $this->complexScript . + $this->strikeType . + $this->uSchemeClr . + (string) $this->baseLine . __CLASS__ ); } @@ -553,12 +729,17 @@ class Font extends Supervisor protected function exportArray1(): array { $exportedArray = []; + $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); $this->exportArray2($exportedArray, 'color', $this->getColor()); + $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); + $this->exportArray2($exportedArray, 'eastAsian', $this->getEastAsian()); $this->exportArray2($exportedArray, 'italic', $this->getItalic()); + $this->exportArray2($exportedArray, 'latin', $this->getLatin()); $this->exportArray2($exportedArray, 'name', $this->getName()); $this->exportArray2($exportedArray, 'size', $this->getSize()); $this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough()); + $this->exportArray2($exportedArray, 'strikeType', $this->getStrikeType()); $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); diff --git a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php index b8e14b8b..6c4d9d6b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php @@ -129,11 +129,10 @@ class DateFormatter // but we don't want to change any quoted strings /** @var callable */ $callable = [self::class, 'setLowercaseCallback']; - $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format); + $format = (string) preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format); // Only process the non-quoted blocks for date format characters - /** @phpstan-ignore-next-line */ $blocks = explode('"', $format); foreach ($blocks as $key => &$block) { if ($key % 2 == 0) { diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 01407e64..3e4bdc46 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -51,12 +51,12 @@ class Formatter for ($idx = 0; $idx < $cnt; ++$idx) { if (preg_match($color_regex, $sections[$idx], $matches)) { $colors[$idx] = $matches[0]; - $sections[$idx] = preg_replace($color_regex, '', $sections[$idx]); + $sections[$idx] = (string) preg_replace($color_regex, '', $sections[$idx]); } if (preg_match($cond_regex, $sections[$idx], $matches)) { $condops[$idx] = $matches[1]; $condvals[$idx] = $matches[2]; - $sections[$idx] = preg_replace($cond_regex, '', $sections[$idx]); + $sections[$idx] = (string) preg_replace($cond_regex, '', $sections[$idx]); } } $color = $colors[0]; @@ -112,7 +112,7 @@ class Formatter return $value; } - $format = preg_replace_callback( + $format = (string) preg_replace_callback( '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', function ($matches) { return str_replace('.', chr(0x00), $matches[0]); @@ -121,7 +121,7 @@ class Formatter ); // Convert any other escaped characters to quoted strings, e.g. (\T to "T") - $format = preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); + $format = (string) preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal) $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format); @@ -130,7 +130,7 @@ class Formatter // In Excel formats, "_" is used to add spacing, // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space - $format = preg_replace('/_.?/ui', ' ', $format); + $format = (string) preg_replace('/_.?/ui', ' ', $format); // Let's begin inspecting the format and converting the value to a formatted string diff --git a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php index 334c40df..f4d3412b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -35,7 +35,7 @@ class PercentageFormatter extends BaseFormatter $wholePartSize += $decimalPartSize; $replacement = "{$wholePartSize}.{$decimalPartSize}"; - $mask = preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); + $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); /** @var float */ $valueFloat = $value; diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index dd33d5d5..05b2e9a0 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -145,7 +145,7 @@ class AutoFilter $this->evaluated = false; if ($this->workSheet !== null) { $thisrange = $this->range; - $range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? ''; + $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 66839d41..ffdbf9a7 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -212,7 +212,7 @@ class Table { if ($this->workSheet !== null) { $thisrange = $this->range; - $range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? ''; + $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index cd76d2a4..a56dda4c 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -42,10 +42,9 @@ class Validations public static function validateCellOrCellRange($cellRange): string { if (is_string($cellRange) || is_numeric($cellRange)) { - // Convert a single column reference like 'A' to 'A:A' - $cellRange = (string) preg_replace('/^([A-Z]+)$/', '${1}:${1}', (string) $cellRange); - // Convert a single row reference like '1' to '1:1' - $cellRange = (string) preg_replace('/^(\d+)$/', '${1}:${1}', $cellRange); + // Convert a single column reference like 'A' to 'A:A', + // a single row reference like '1' to '1:1' + $cellRange = (string) preg_replace('/^([A-Z]+|\d+)$/', '${1}:${1}', (string) $cellRange); } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { $cellRange = new CellRange($cellRange, $cellRange); } @@ -66,9 +65,12 @@ class Validations [$worksheet, $addressRange] = Worksheet::extractSheetTitle($cellRange, true); // Convert Column ranges like 'A:C' to 'A1:C1048576' - $addressRange = (string) preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); - // Convert Row ranges like '1:3' to 'A1:XFD3' - $addressRange = (string) preg_replace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); + // or Row ranges like '1:3' to 'A1:XFD3' + $addressRange = (string) preg_replace( + ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'], + ['${1}1:${2}1048576', 'A${1}:XFD${2}'], + $addressRange + ); return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 45d4ce85..3ef65928 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -662,13 +662,13 @@ class Html extends BaseWriter $filename = $drawing->getPath(); // Strip off eventual '.' - $filename = preg_replace('/^[.]/', '', $filename); + $filename = (string) preg_replace('/^[.]/', '', $filename); // Prepend images root $filename = $this->getImagesRoot() . $filename; // Strip off eventual '.' if followed by non-/ - $filename = preg_replace('@^[.]([^/])@', '$1', $filename); + $filename = (string) preg_replace('@^[.]([^/])@', '$1', $filename); // Convert UTF8 data to PCDATA $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); @@ -1326,7 +1326,7 @@ class Html extends BaseWriter // Converts the cell content so that spaces occuring at beginning of each new line are replaced by   // Example: " Hello\n to the world" is converted to "  Hello\n to the world" - $cellData = preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); + $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); // convert newline "\n" to '
' $cellData = nl2br($cellData); diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index 66194468..1bf2c463 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -10,12 +10,14 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Style { public const CELL_STYLE_PREFIX = 'ce'; public const COLUMN_STYLE_PREFIX = 'co'; public const ROW_STYLE_PREFIX = 'ro'; + public const TABLE_STYLE_PREFIX = 'ta'; private $writer; @@ -221,6 +223,26 @@ class Style $this->writer->endElement(); // Close style:style } + public function writeTableStyle(Worksheet $worksheet, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId) + ); + + $this->writer->startElement('style:table-properties'); + + $this->writer->writeAttribute( + 'table:display', + $worksheet->getSheetState() === Worksheet::SHEETSTATE_VISIBLE ? 'true' : 'false' + ); + + $this->writer->endElement(); // Close style:table-properties + $this->writer->endElement(); // Close style:style + } + public function write(CellStyle $style): void { $this->writer->startElement('style:style'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index a8da1019..00ab0643 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -123,6 +123,7 @@ class Content extends WriterPart for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) { $objWriter->startElement('table:table'); $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); + $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1)); $objWriter->writeElement('office:forms'); foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { $objWriter->startElement('table:table-column'); @@ -289,6 +290,8 @@ class Content extends WriterPart $sheetCount = $spreadsheet->getSheetCount(); for ($i = 0; $i < $sheetCount; ++$i) { $worksheet = $spreadsheet->getSheet($i); + $styleWriter->writeTableStyle($worksheet, $i + 1); + $worksheet->calculateColumnWidths(); foreach ($worksheet->getColumnDimensions() as $columnDimension) { if ($columnDimension->getWidth() !== -1.0) { diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 4033fd53..2f75f908 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -778,8 +778,7 @@ class Parser */ private function getRefIndex($ext_ref) { - $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. - $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. + $ext_ref = (string) preg_replace(["/^'/", "/'$/"], ['', ''], $ext_ref); // Remove leading and trailing ' if any. $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' // Check if there is a sheet range eg., Sheet1:Sheet2. diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 74e145cc..37865518 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -1039,7 +1039,7 @@ class Worksheet extends BIFFwriter $record = 0x01B8; // Record identifier // Strip URL type - $url = preg_replace('/^internal:/', '', $url); + $url = (string) preg_replace('/^internal:/', '', $url); // Pack the undocumented parts of the hyperlink stream $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); @@ -1095,8 +1095,7 @@ class Worksheet extends BIFFwriter // Strip URL type and change Unix dir separator to Dos style (if needed) // - $url = preg_replace('/^external:/', '', $url); - $url = preg_replace('/\//', '\\', $url); + $url = (string) preg_replace(['/^external:/', '/\//'], ['', '\\'], $url); // Determine if the link is relative or absolute: // relative if link contains no dir separator, "somefile.xls" @@ -1125,7 +1124,7 @@ class Worksheet extends BIFFwriter $up_count = pack('v', $up_count); // Store the short dos dir name (null terminated) - $dir_short = preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; + $dir_short = (string) preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; // Store the long dir name as a wchar string (non-null terminated) $dir_long = $dir_long . "\0"; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index ba7a6545..08d578e6 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -10,7 +10,6 @@ use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -75,6 +74,33 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', 0); $objWriter->endElement(); + $objWriter->startElement('c:view3D'); + $rotX = $chart->getRotX(); + if (is_int($rotX)) { + $objWriter->startElement('c:rotX'); + $objWriter->writeAttribute('val', "$rotX"); + $objWriter->endElement(); + } + $rotY = $chart->getRotY(); + if (is_int($rotY)) { + $objWriter->startElement('c:rotY'); + $objWriter->writeAttribute('val', "$rotY"); + $objWriter->endElement(); + } + $rAngAx = $chart->getRAngAx(); + if (is_int($rAngAx)) { + $objWriter->startElement('c:rAngAx'); + $objWriter->writeAttribute('val', "$rAngAx"); + $objWriter->endElement(); + } + $perspective = $chart->getPerspective(); + if (is_int($perspective)) { + $objWriter->startElement('c:perspective'); + $objWriter->writeAttribute('val', "$perspective"); + $objWriter->endElement(); + } + $objWriter->endElement(); // view3D + $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY(), $chart->getMajorGridlines(), $chart->getMinorGridlines()); $this->writeLegend($objWriter, $chart->getLegend()); @@ -121,6 +147,10 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $title->getCaption(); if ((is_array($caption)) && (count($caption) > 0)) { @@ -196,7 +226,7 @@ class Chart extends WriterPart return; } - $id1 = $id2 = 0; + $id1 = $id2 = $id3 = '0'; $this->seriesIndex = 0; $objWriter->startElement('c:plotArea'); @@ -226,6 +256,10 @@ class Chart extends WriterPart $objWriter->startElement('c:scatterStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); + } elseif ($groupType === DataSeries::TYPE_SURFACECHART_3D || $groupType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:wireframe'); + $objWriter->writeAttribute('val', $plotStyle ? '1' : '0'); + $objWriter->endElement(); } $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType); @@ -250,9 +284,12 @@ class Chart extends WriterPart $objWriter->endElement(); } } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) { - $objWriter->startElement('c:bubbleScale'); - $objWriter->writeAttribute('val', 25); - $objWriter->endElement(); + $scale = ($plotGroup === null) ? '' : (string) $plotGroup->getPlotStyle(); + if ($scale !== '') { + $objWriter->startElement('c:bubbleScale'); + $objWriter->writeAttribute('val', $scale); + $objWriter->endElement(); + } $objWriter->startElement('c:showNegBubbles'); $objWriter->writeAttribute('val', 0); @@ -276,9 +313,10 @@ class Chart extends WriterPart $objWriter->endElement(); } - // Generate 2 unique numbers to use for axId values - $id1 = '75091328'; - $id2 = '75089408'; + // Generate 3 unique numbers to use for axId values + $id1 = '110438656'; + $id2 = '110444544'; + $id3 = '110365312'; // used in Surface Chart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { $objWriter->startElement('c:axId'); @@ -287,6 +325,11 @@ class Chart extends WriterPart $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); + } } else { $objWriter->startElement('c:firstSliceAng'); $objWriter->writeAttribute('val', 0); @@ -304,12 +347,15 @@ class Chart extends WriterPart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { if ($chartType === DataSeries::TYPE_BUBBLECHART) { - $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); } else { $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $this->writeSerAxis($objWriter, $id2, $id3); + } } $objWriter->endElement(); @@ -369,9 +415,15 @@ class Chart extends WriterPart */ private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void { - $objWriter->startElement('c:catAx'); + // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc + // In that case, xAxis is NOT a category. + if ($yAxis->getAxisIsNumericFormat()) { + $objWriter->startElement('c:valAx'); + } else { + $objWriter->startElement('c:catAx'); + } - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id1); $objWriter->endElement(); @@ -403,20 +455,20 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $xAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); - $objWriter->endElement(); $layout = $xAxisLabel->getLayout(); $this->writeLayout($objWriter, $layout); @@ -445,7 +497,7 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); $objWriter->endElement(); - if ($id2 > 0) { + if ($id2 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -487,7 +539,7 @@ class Chart extends WriterPart { $objWriter->startElement('c:valAx'); - if ($id2 > 0) { + if ($id2 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -746,18 +798,17 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $yAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); - - $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); @@ -913,9 +964,9 @@ class Chart extends WriterPart $objWriter->endElement(); //effectList $objWriter->endElement(); //end spPr - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:crossAx'); - $objWriter->writeAttribute('val', $id2); + $objWriter->writeAttribute('val', $id1); $objWriter->endElement(); if ($xAxis->getAxisOptionsProperty('horizontal_crosses_value') !== null) { @@ -956,6 +1007,54 @@ class Chart extends WriterPart $objWriter->endElement(); } + /** + * Write Ser Axis, for Surface chart. + */ + private function writeSerAxis(XMLWriter $objWriter, string $id2, string $id3): void + { + $objWriter->startElement('c:serAx'); + + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); // axId + + $objWriter->startElement('c:scaling'); + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', 'minMax'); + $objWriter->endElement(); // orientation + $objWriter->endElement(); // scaling + + $objWriter->startElement('c:delete'); + $objWriter->writeAttribute('val', '0'); + $objWriter->endElement(); // delete + + $objWriter->startElement('c:axPos'); + $objWriter->writeAttribute('val', 'b'); + $objWriter->endElement(); // axPos + + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', 'out'); + $objWriter->endElement(); // majorTickMark + + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', 'none'); + $objWriter->endElement(); // minorTickMark + + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', 'nextTo'); + $objWriter->endElement(); // tickLblPos + + $objWriter->startElement('c:crossAx'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); // crossAx + + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', 'autoZero'); + $objWriter->endElement(); // crosses + + $objWriter->endElement(); //serAx + } + /** * Get the data series type(s) for a chart plot series. * @@ -1016,7 +1115,7 @@ class Chart extends WriterPart * @param bool $valIsMultiLevelSeries Is value set a multi-series set * @param string $plotGroupingType Type of grouping for multi-series values */ - private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void + private function writePlotGroup(?DataSeries $plotGroup, string $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void { if ($plotGroup === null) { return; @@ -1104,16 +1203,29 @@ class Chart extends WriterPart } // Formatting for the points - if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART)) { + if ( + $groupType == DataSeries::TYPE_LINECHART + || $groupType == DataSeries::TYPE_STOCKCHART + || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()) + || ($plotSeriesValues !== false && $plotSeriesValues->getSchemeClr()) + ) { $plotLineWidth = 12700; if ($plotSeriesValues) { $plotLineWidth = $plotSeriesValues->getLineWidth(); } $objWriter->startElement('c:spPr'); + $schemeClr = $plotLabel ? $plotLabel->getSchemeClr() : null; + if ($schemeClr) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', $schemeClr); + $objWriter->endElement(); + $objWriter->endElement(); + } $objWriter->startElement('a:ln'); $objWriter->writeAttribute('w', $plotLineWidth); - if ($groupType == DataSeries::TYPE_STOCKCHART) { + if ($groupType == DataSeries::TYPE_STOCKCHART || $groupType === DataSeries::TYPE_SCATTERCHART) { $objWriter->startElement('a:noFill'); $objWriter->endElement(); } elseif ($plotLabel) { @@ -1142,6 +1254,16 @@ class Chart extends WriterPart $objWriter->startElement('c:size'); $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize()); $objWriter->endElement(); + $fillColor = $plotSeriesValues->getFillColor(); + if (is_string($fillColor) && $fillColor !== '') { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $fillColor); + $objWriter->endElement(); // srgbClr + $objWriter->endElement(); // solidFill + $objWriter->endElement(); // spPr + } } $objWriter->endElement(); @@ -1176,7 +1298,14 @@ class Chart extends WriterPart $objWriter->startElement('c:cat'); } - $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + // xVals (Categories) are not always 'str' + // Test X-axis Label's Datatype to decide 'str' vs 'num' + $CategoryDatatype = $plotSeriesCategory->getDataType(); + if ($CategoryDatatype == DataSeriesValues::DATASERIES_TYPE_NUMBER) { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'num'); + } else { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + } $objWriter->endElement(); } @@ -1192,10 +1321,31 @@ class Chart extends WriterPart $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num'); $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') { + $objWriter->startElement('c:smooth'); + $objWriter->writeAttribute('val', '1'); + $objWriter->endElement(); + } } if ($groupType === DataSeries::TYPE_BUBBLECHART) { - $this->writeBubbles($plotSeriesValues, $objWriter); + if (!empty($plotGroup->getPlotBubbleSizes()[$plotSeriesIdx])) { + $objWriter->startElement('c:bubbleSize'); + $this->writePlotSeriesValues( + $plotGroup->getPlotBubbleSizes()[$plotSeriesIdx], + $objWriter, + $groupType, + 'num' + ); + $objWriter->endElement(); + if ($plotSeriesValues !== false) { + $objWriter->startElement('c:bubble3D'); + $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); + $objWriter->endElement(); + } + } else { + $this->writeBubbles($plotSeriesValues, $objWriter); + } } $objWriter->endElement(); @@ -1289,38 +1439,43 @@ class Chart extends WriterPart $objWriter->writeRawData($plotSeriesValues->getDataSource()); $objWriter->endElement(); - $objWriter->startElement('c:' . $dataType . 'Cache'); + $count = $plotSeriesValues->getPointCount(); + $source = $plotSeriesValues->getDataSource(); + $values = $plotSeriesValues->getDataValues(); + if ($count > 1 || ($count === 1 && "=$source" !== (string) $values[0])) { + $objWriter->startElement('c:' . $dataType . 'Cache'); - if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) { - if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) { - $objWriter->startElement('c:formatCode'); - $objWriter->writeRawData($plotSeriesValues->getFormatCode()); - $objWriter->endElement(); - } - } - - $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); - $objWriter->endElement(); - - $dataValues = $plotSeriesValues->getDataValues(); - if (!empty($dataValues)) { - if (is_array($dataValues)) { - foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { - $objWriter->startElement('c:pt'); - $objWriter->writeAttribute('idx', $plotSeriesKey); - - $objWriter->startElement('c:v'); - $objWriter->writeRawData($plotSeriesValue); - $objWriter->endElement(); + if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) { + if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) { + $objWriter->startElement('c:formatCode'); + $objWriter->writeRawData($plotSeriesValues->getFormatCode()); $objWriter->endElement(); } } + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->endElement(); + + $dataValues = $plotSeriesValues->getDataValues(); + if (!empty($dataValues)) { + if (is_array($dataValues)) { + foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', $plotSeriesKey); + + $objWriter->startElement('c:v'); + $objWriter->writeRawData($plotSeriesValue); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + } + + $objWriter->endElement(); // *Cache } - $objWriter->endElement(); - - $objWriter->endElement(); + $objWriter->endElement(); // *Ref } } @@ -1362,7 +1517,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); $objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 816bb9d4..7693c72c 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -89,22 +89,49 @@ class Drawing extends WriterPart $tl = $chart->getTopLeftPosition(); $tlColRow = Coordinate::indexesFromString($tl['cell']); $br = $chart->getBottomRightPosition(); - $brColRow = Coordinate::indexesFromString($br['cell']); - $objWriter->startElement('xdr:twoCellAnchor'); + $isTwoCellAnchor = $br['cell'] !== ''; + if ($isTwoCellAnchor) { + $brColRow = Coordinate::indexesFromString($br['cell']); - $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); - $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); - $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); - $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); - $objWriter->endElement(); - $objWriter->startElement('xdr:to'); - $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1)); - $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset'])); - $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); - $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); - $objWriter->endElement(); + $objWriter->startElement('xdr:twoCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:to'); + $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } elseif ($chart->getOneCellAnchor()) { + $objWriter->startElement('xdr:oneCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset'])); + $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } else { + $objWriter->startElement('xdr:absoluteAnchor'); + $objWriter->startElement('xdr:pos'); + $objWriter->writeAttribute('x', '0'); + $objWriter->writeAttribute('y', '0'); + $objWriter->endElement(); + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset'])); + $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } $objWriter->startElement('xdr:graphicFrame'); $objWriter->writeAttribute('macro', ''); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index a64e0d68..da7d825b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -203,7 +203,8 @@ class StringTable extends WriterPart if (!$richText instanceof RichText) { $textRun = $richText; $richText = new RichText(); - $richText->createTextRun($textRun); + $run = $richText->createTextRun($textRun); + $run->setFont(null); } if ($prefix !== null) { @@ -215,36 +216,75 @@ class StringTable extends WriterPart foreach ($elements as $element) { // r $objWriter->startElement($prefix . 'r'); + if ($element->getFont() !== null) { + // rPr + $objWriter->startElement($prefix . 'rPr'); + $size = $element->getFont()->getSize(); + if (is_numeric($size)) { + $objWriter->writeAttribute('sz', (string) (int) ($size * 100)); + } - // rPr - $objWriter->startElement($prefix . 'rPr'); + // Bold + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); + // Italic + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); + // Underline + $underlineType = $element->getFont()->getUnderline(); + switch ($underlineType) { + case 'single': + $underlineType = 'sng'; - // Bold - $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); - // Italic - $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); - // Underline - $underlineType = $element->getFont()->getUnderline(); - switch ($underlineType) { - case 'single': - $underlineType = 'sng'; + break; + case 'double': + $underlineType = 'dbl'; - break; - case 'double': - $underlineType = 'dbl'; + break; + } + $objWriter->writeAttribute('u', $underlineType); + // Strikethrough + $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike')); + // Superscript/subscript + if ($element->getFont()->getBaseLine()) { + $objWriter->writeAttribute('baseline', (string) $element->getFont()->getBaseLine()); + } - break; + // Color + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . 'srgbClr'); + $objWriter->writeAttribute('val', $element->getFont()->getColor()->getRGB()); + $objWriter->endElement(); // srgbClr + $objWriter->endElement(); // solidFill + + // Underscore Color + if ($element->getFont()->getUSchemeClr()) { + $objWriter->startElement($prefix . 'uFill'); + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . 'schemeClr'); + $objWriter->writeAttribute('val', $element->getFont()->getUSchemeClr()); + $objWriter->endElement(); // schemeClr + $objWriter->endElement(); // solidFill + $objWriter->endElement(); // uFill + } + + // fontName + if ($element->getFont()->getLatin()) { + $objWriter->startElement($prefix . 'latin'); + $objWriter->writeAttribute('typeface', $element->getFont()->getLatin()); + $objWriter->endElement(); + } + if ($element->getFont()->getEastAsian()) { + $objWriter->startElement($prefix . 'ea'); + $objWriter->writeAttribute('typeface', $element->getFont()->getEastAsian()); + $objWriter->endElement(); + } + if ($element->getFont()->getComplexScript()) { + $objWriter->startElement($prefix . 'cs'); + $objWriter->writeAttribute('typeface', $element->getFont()->getComplexScript()); + $objWriter->endElement(); + } + + $objWriter->endElement(); } - $objWriter->writeAttribute('u', $underlineType); - // Strikethrough - $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); - - // rFont - $objWriter->startElement($prefix . 'latin'); - $objWriter->writeAttribute('typeface', $element->getFont()->getName()); - $objWriter->endElement(); - - $objWriter->endElement(); // t $objWriter->startElement($prefix . 't'); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php index c88ef245..6fc0c66a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -152,7 +152,7 @@ class Xlfn */ public static function addXlfn(string $funcstring): string { - return preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring); + return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring); } /** diff --git a/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php b/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php index da821532..60351d71 100644 --- a/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php +++ b/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php @@ -19,10 +19,13 @@ abstract class AbstractFunctional extends TestCase * * @return Spreadsheet */ - protected function writeAndReload(Spreadsheet $spreadsheet, $format, ?callable $readerCustomizer = null) + protected function writeAndReload(Spreadsheet $spreadsheet, $format, ?callable $readerCustomizer = null, ?callable $writerCustomizer = null) { $filename = File::temporaryFilename(); $writer = IOFactory::createWriter($spreadsheet, $format); + if ($writerCustomizer) { + $writerCustomizer($writer); + } $writer->save($filename); $reader = IOFactory::createReader($format); diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2840Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2840Test.php new file mode 100644 index 00000000..34d0a864 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2840Test.php @@ -0,0 +1,45 @@ +getPreserveNullString()); + $inputData = <<loadSpreadsheetFromString($inputData); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame($expected, $sheet->toArray()); + $spreadsheet->disconnectWorksheets(); + } + + public function testNullStringLoad(): void + { + $reader = new Csv(); + $reader->setPreserveNullString(true); + $inputData = <<loadSpreadsheetFromString($inputData); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame($expected, $sheet->toArray()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/HiddenWorksheetTest.php new file mode 100644 index 00000000..ffa3b88d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/HiddenWorksheetTest.php @@ -0,0 +1,56 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php new file mode 100644 index 00000000..c02c8771 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php @@ -0,0 +1,56 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenWorksheetTest.php new file mode 100644 index 00000000..82fd6e12 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenWorksheetTest.php @@ -0,0 +1,56 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php new file mode 100644 index 00000000..0f1605ff --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php @@ -0,0 +1,35 @@ +setIncludeCharts(true); + $spreadsheet = $reader->load($filename); + + self::assertCount(2, $spreadsheet->getAllSheets()); + $chartSheet = $spreadsheet->getSheetByName('Chart1'); + self::assertInstanceOf(Worksheet::class, $chartSheet); + self::assertSame(1, $chartSheet->getChartCount()); + } + + public function testLoadChartSheetWithoutCharts(): void + { + $filename = 'tests/data/Reader/XLSX/ChartSheet.xlsx'; + $reader = new Xlsx(); + $reader->setIncludeCharts(false); + $spreadsheet = $reader->load($filename); + + self::assertCount(1, $spreadsheet->getAllSheets()); + $chartSheet = $spreadsheet->getSheetByName('Chart1'); + self::assertNull($chartSheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartsOpenpyxlTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartsOpenpyxlTest.php new file mode 100644 index 00000000..a7343af5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartsOpenpyxlTest.php @@ -0,0 +1,115 @@ +setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + + self::assertSame('Sheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertEmpty($chart->getTitle()); + self::assertTrue($chart->getOneCellAnchor()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $labels = $dataSeries->getPlotLabels(); + self::assertCount(2, $labels); + self::assertSame(['2013'], $labels[0]->getDataValues()); + self::assertSame(['2014'], $labels[1]->getDataValues()); + + $plotCategories = $dataSeries->getPlotCategories(); + self::assertCount(2, $plotCategories); + $categories = $plotCategories[0]; + self::assertSame('Number', $categories->getDataType()); + self::assertSame('\'Sheet\'!$A$2:$A$5', $categories->getDataSource()); + self::assertFalse($categories->getBubble3D()); + $categories = $plotCategories[1]; + self::assertCount(2, $plotCategories); + self::assertSame('Number', $categories->getDataType()); + self::assertSame('\'Sheet\'!$A$7:$A$10', $categories->getDataSource()); + self::assertFalse($categories->getBubble3D()); + + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(2, $plotValues); + $values = $plotValues[0]; + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$B$2:$B$5', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + $values = $plotValues[1]; + self::assertCount(2, $plotValues); + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$B$7:$B$10', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + + $plotValues = $dataSeries->getPlotBubbleSizes(); + self::assertCount(2, $plotValues); + $values = $plotValues[0]; + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$C$2:$C$5', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + $values = $plotValues[1]; + self::assertCount(2, $plotValues); + self::assertSame('Number', $values->getDataType()); + self::assertSame('\'Sheet\'!$C$7:$C$10', $values->getDataSource()); + self::assertFalse($values->getBubble3D()); + + $spreadsheet->disconnectWorksheets(); + } + + public function testXml(): void + { + $infile = self::DIRECTORY . '32readwriteBubbleChart2.xlsx'; + $file = 'zip://'; + $file .= $infile; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(0, substr_count($data, 'c:'), 'unusual choice of prefix'); + self::assertSame(0, substr_count($data, 'bubbleScale')); + self::assertSame(1, substr_count($data, '2013'), 'v tag for 2013'); + self::assertSame(1, substr_count($data, '2014'), 'v tag for 2014'); + self::assertSame(0, substr_count($data, 'numCache'), 'no cached values'); + } + $file = 'zip://'; + $file .= $infile; + $file .= '#xl/drawings/_rels/drawing1.xml.rels'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(1, substr_count($data, 'Target="/xl/charts/chart1.xml"'), 'Unusual absolute address in drawing rels file'); + } + $file = 'zip://'; + $file .= $infile; + $file .= '#xl/worksheets/_rels/sheet1.xml.rels'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(1, substr_count($data, 'Target="/xl/drawings/drawing1.xml"'), 'Unusual absolute address in worksheet rels file'); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenWorksheetTest.php new file mode 100644 index 00000000..130d76a6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenWorksheetTest.php @@ -0,0 +1,56 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php index 3b515090..e7f99010 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php @@ -26,7 +26,7 @@ class URLImageTest extends TestCase self::assertInstanceOf(Drawing::class, $drawing); // Check if the source is a URL or a file path self::assertTrue($drawing->getIsURL()); - self::assertSame('https://www.globalipmanager.com/DataFiles/Pics/20/Berniaga.comahp2.jpg', $drawing->getPath()); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/topics/images/01-03-filter-icon-1.png', $drawing->getPath()); $imageContents = file_get_contents($drawing->getPath()); self::assertNotFalse($imageContents); $filePath = tempnam(sys_get_temp_dir(), 'Drawing'); @@ -36,7 +36,7 @@ class URLImageTest extends TestCase unlink($filePath); self::assertNotFalse($mimeType); $extension = File::mime2ext($mimeType); - self::assertSame('jpeg', $extension); + self::assertSame('png', $extension); } } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php index ed01db25..cc2269b2 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/WorksheetInfoNamesTest.php @@ -77,4 +77,29 @@ class WorksheetInfoNamesTest extends TestCase self::assertEquals($expected, $actual); } + + public function testListWorksheetNamesChartSheet(): void + { + $filename = 'tests/data/Reader/XLSX/ChartSheet.xlsx'; + $reader = new Xlsx(); + $actual = $reader->listWorksheetNames($filename); + + $expected = ['Sheet1', 'Chart1']; + + self::assertEquals($expected, $actual); + } + + public function testListWorksheetInfoChartSheet(): void + { + $filename = 'tests/data/Reader/XLSX/ChartSheet.xlsx'; + $reader = new Xlsx(); + $actual = $reader->listWorksheetInfo($filename); + + $chartSheetInfo = $actual[1]; + + self::assertSame('Chart1', $chartSheetInfo['worksheetName']); + self::assertSame(-1, $chartSheetInfo['lastColumnIndex']); + self::assertSame(0, $chartSheetInfo['totalRows']); + self::assertSame(0, $chartSheetInfo['totalColumns']); + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php index 917a0410..81c244a7 100644 --- a/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php @@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Writer\Ods\Content; use PHPUnit\Framework\TestCase; @@ -106,4 +107,26 @@ class ContentTest extends TestCase self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-with-data.xml', $xml); } + + public function testWriteWithHiddenWorksheet(): void + { + $workbook = new Spreadsheet(); + + // Worksheet 1 + $worksheet1 = $workbook->getActiveSheet(); + $worksheet1->setCellValue('A1', 1); + + // Worksheet 2 + $worksheet2 = $workbook->createSheet(); + $worksheet2->setTitle('New Worksheet'); + $worksheet2->setCellValue('A1', 2); + + $worksheet2->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + + // Write + $content = new Content(new Ods($workbook)); + $xml = $content->write(); + + self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-hidden-worksheet.xml', $xml); + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php new file mode 100644 index 00000000..af33baa1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php @@ -0,0 +1,171 @@ +outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + /** + * @dataProvider providerCatAxValAx + */ + public function test1CatAx1ValAx(?bool $numeric): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + // changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date + $worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] + ); + $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); + $worksheet->getColumnDimension('A')->setAutoSize(true); + $worksheet->setSelectedCells('A1'); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 + ]; + // Set the X-Axis Labels + // changed from STRING to NUMBER + // added 2 additional x-axis values associated with each of the 3 metrics + // added FORMATE_CODE_NUMBER + $xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + // added FORMAT_CODE_NUMBER + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), + ]; + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers + $xAxis = new Axis(); + //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, $numeric); + } else { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + } + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + //DataSeries::STYLE_LINEMARKER // plotStyle + DataSeries::STYLE_MARKER // plotStyle + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test Scatter Trend Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('P20'); + // Add the chart to the worksheet + $worksheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } elseif ($numeric === true) { + self::assertSame(0, substr_count($data, 'setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testStock5(): void + { + $file = self::DIRECTORY . '32readwriteStockChart5.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $xAxisLabel = $chart->getXAxisLabel(); + $captionArray = $xAxisLabel->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('X-Axis Title in Green', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('00B050', $font->getColor()->getRGB()); + + $yAxisLabel = $chart->getYAxisLabel(); + $captionArray = $yAxisLabel->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Y-Axis Title in Red', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('FF0000', $font->getColor()->getRGB()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php new file mode 100644 index 00000000..76389727 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php @@ -0,0 +1,333 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testScatter1(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - No Join and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getLatin()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter6(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart6.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - Rich Text Title No Join and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(3, $elements); + + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getLatin()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $run = $elements[1]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Courier New', $font->getLatin()); + self::assertEquals(10, $font->getSize()); + self::assertFalse($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('single', $font->getUnderline()); + self::assertSame('00B0F0', $font->getColor()->getRGB()); + + $run = $elements[2]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getLatin()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter3(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart3.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - Join Straight Lines and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getLatin()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter7(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart7.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Latin/EA/CS Title ABCאבגDEFァ', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertGreaterThan(0, count($elements)); + foreach ($elements as $run) { + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Times New Roman', $font->getLatin()); + self::assertSame('Malgun Gothic', $font->getEastAsian()); + self::assertSame('Courier New', $font->getComplexScript()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + } + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php new file mode 100644 index 00000000..9b32518a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php @@ -0,0 +1,147 @@ +outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + /** + * @dataProvider providerScatterCharts + */ + public function testBezierCount(int $expectedCount, string $inputFile): void + { + $file = self::DIRECTORY . $inputFile; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart2.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(1, substr_count($data, '')); + self::assertSame($expectedCount, substr_count($data, '')); + } + } + + public function providerScatterCharts(): array + { + return [ + 'no line' => [0, '32readwriteScatterChart1.xlsx'], + 'smooth line (Bezier)' => [3, '32readwriteScatterChart2.xlsx'], + 'straight line' => [0, '32readwriteScatterChart3.xlsx'], + ]; + } + + public function testAreaPercentageNoCat(): void + { + $file = self::DIRECTORY . '32readwriteAreaPercentageChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(0, substr_count($data, '')); + } + } + + /** + * @dataProvider providerCatAxValAx + */ + public function testCatAxValAx(?bool $numeric): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $xAxis->getAxisNumberFormat()); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, true); + } + $yAxis = $chart->getChartAxisY(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $yAxis->getAxisNumberFormat()); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); + $yAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); + } + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart2.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } elseif ($numeric === true) { + self::assertSame(0, substr_count($data, '')); + self::assertSame(2, substr_count($data, '')); + } else { + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + } + } + + public function providerCatAxValAx(): array + { + return [ + [true], + [false], + [null], + ]; + } +} diff --git a/tests/data/Calculation/MathTrig/PRODUCT.php b/tests/data/Calculation/MathTrig/PRODUCT.php index 0e7c4080..657c713c 100644 --- a/tests/data/Calculation/MathTrig/PRODUCT.php +++ b/tests/data/Calculation/MathTrig/PRODUCT.php @@ -48,6 +48,30 @@ return [ -6.7800000000000002, -2, ], + [ + 31.25, + 12.5, + null, + 2.5, + ], + [ + 31.25, + 12.5, + 2.5, + null, + ], + [ + 12.5, + 12.5, + null, + null, + ], + [ + 0.0, + null, + null, + null, + ], ['#VALUE!', 1, 'y', 3], [6, 1, '2', 3], ]; diff --git a/tests/data/Reader/Gnumeric/HiddenSheet.gnumeric b/tests/data/Reader/Gnumeric/HiddenSheet.gnumeric new file mode 100644 index 00000000..4f503c28 Binary files /dev/null and b/tests/data/Reader/Gnumeric/HiddenSheet.gnumeric differ diff --git a/tests/data/Reader/Ods/HiddenSheet.ods b/tests/data/Reader/Ods/HiddenSheet.ods new file mode 100644 index 00000000..9b1d01a6 Binary files /dev/null and b/tests/data/Reader/Ods/HiddenSheet.ods differ diff --git a/tests/data/Reader/XLS/HiddenSheet.xls b/tests/data/Reader/XLS/HiddenSheet.xls new file mode 100644 index 00000000..39ced197 Binary files /dev/null and b/tests/data/Reader/XLS/HiddenSheet.xls differ diff --git a/tests/data/Reader/XLSX/ChartSheet.xlsx b/tests/data/Reader/XLSX/ChartSheet.xlsx new file mode 100644 index 00000000..861bdeeb Binary files /dev/null and b/tests/data/Reader/XLSX/ChartSheet.xlsx differ diff --git a/tests/data/Reader/XLSX/HiddenSheet.xlsx b/tests/data/Reader/XLSX/HiddenSheet.xlsx new file mode 100644 index 00000000..6d1d56a2 Binary files /dev/null and b/tests/data/Reader/XLSX/HiddenSheet.xlsx differ diff --git a/tests/data/Reader/XLSX/urlImage.xlsx b/tests/data/Reader/XLSX/urlImage.xlsx index 01e8e24d..42295822 100644 Binary files a/tests/data/Reader/XLSX/urlImage.xlsx and b/tests/data/Reader/XLSX/urlImage.xlsx differ diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index 867cfa3e..84f4c239 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -3,6 +3,9 @@ + + + @@ -12,7 +15,7 @@ - + diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml new file mode 100644 index 00000000..88a53257 --- /dev/null +++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + 2 + + + + + + + + \ No newline at end of file diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index fff47f65..12140fa9 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -3,6 +3,12 @@ + + + + + + @@ -62,7 +68,7 @@ - + @@ -107,7 +113,7 @@ - +