diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a5e6d357d..8fb4f8197 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -211,7 +211,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c9899aa..fd987eae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## TBD - 3.4.0 +## TBD - 3.5.0 ### Added @@ -15,17 +15,57 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Nothing yet. +### Moved + +- Nothing yet. + ### Deprecated - Nothing yet. +### Fixed + +- Add support for `` tag when converting HTML to RichText. [Issue #4223](https://github.com/PHPOffice/PhpSpreadsheet/issues/4223) [PR #4224](https://github.com/PHPOffice/PhpSpreadsheet/pull/4224) + +## 2024-11-10 - 3.4.0 + +### Security Fix + +- Several security patches. + +### Added + +- Add Dynamic valueBinder Property to Spreadsheet and Readers. [Issue #1395](https://github.com/PHPOffice/PhpSpreadsheet/issues/1395) [PR #4185](https://github.com/PHPOffice/PhpSpreadsheet/pull/4185) +- Allow Omitting Chart Border. [Issue #562](https://github.com/PHPOffice/PhpSpreadsheet/issues/562) [PR #4188](https://github.com/PHPOffice/PhpSpreadsheet/pull/4188) +- Method to Test Whether Csv Will Be Affected by Php9. [PR #4189](https://github.com/PHPOffice/PhpSpreadsheet/pull/4189) + +### Changed + +- Refactor Xls Reader. [PR #4118](https://github.com/PHPOffice/PhpSpreadsheet/pull/4118) + +### Deprecated + +- IReader::SKIP_EMPTY_CELLS - use its alias IGNORE_EMPTY_CELLS instead. +- Worksheet::getProtectedCells was deprecated in release 2, but was not properly documented, and not removed in release 3. Use getProtectedCellRanges instead. +- Writer/Html::isMpdf property was deprecated in release 2, but was not properly documented, and not removed in release 3. Use instanceof Mpdf instead. + ### Moved - Nothing yet. ### Fixed -- Nothing yet. +- Xls Writer Condtional Rules Applied to Whole Rows or Columns. [Issue #3185](https://github.com/PHPOffice/PhpSpreadsheet/issues/3185) [PR #4152](https://github.com/PHPOffice/PhpSpreadsheet/pull/4152) +- Xlsx Writer Duplicate ContentTypes Entry for Background Image. [Issue #4179](https://github.com/PHPOffice/PhpSpreadsheet/issues/4179) [PR #4180](https://github.com/PHPOffice/PhpSpreadsheet/pull/4180) +- Check strictNullComparison outside of loops. [PR #3347](https://github.com/PHPOffice/PhpSpreadsheet/pull/3347) +- SUMIFS Does Not Require xlfn. [Issue #4182](https://github.com/PHPOffice/PhpSpreadsheet/issues/4182) [PR #4186](https://github.com/PHPOffice/PhpSpreadsheet/pull/4186) +- Image Transparency/Opacity with Html Reader Changes. [Discussion #4117](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4117) [PR #4142](https://github.com/PHPOffice/PhpSpreadsheet/pull/4142) +- Option to Write Hyperlink Rather Than Label to Csv. [Issue #1412](https://github.com/PHPOffice/PhpSpreadsheet/issues/1412) [PR #4151](https://github.com/PHPOffice/PhpSpreadsheet/pull/4151) +- Invalid Html Due to Cached Filesize. [Issue #1107](https://github.com/PHPOffice/PhpSpreadsheet/issues/1107) [PR #4184](https://github.com/PHPOffice/PhpSpreadsheet/pull/4184) +- Excel 2003 Allows Html Entities. [Issue #2157](https://github.com/PHPOffice/PhpSpreadsheet/issues/2157) [PR #4187](https://github.com/PHPOffice/PhpSpreadsheet/pull/4187) +- Changes to ROUNDDOWN/ROUNDUP/TRUNC. [Issue #4213](https://github.com/PHPOffice/PhpSpreadsheet/issues/4213) [PR #4214](https://github.com/PHPOffice/PhpSpreadsheet/pull/4214) +- Writer Xlsx ignoredErrors Before Drawings. [Issue #4200](https://github.com/PHPOffice/PhpSpreadsheet/issues/4200) [Issue #4145](https://github.com/PHPOffice/PhpSpreadsheet/issues/4145) [PR #4212](https://github.com/PHPOffice/PhpSpreadsheet/pull/4212) +- Allow ANCHORARRAY as Data Validation list. [Issue #4197](https://github.com/PHPOffice/PhpSpreadsheet/issues/4197) [PR #4203](https://github.com/PHPOffice/PhpSpreadsheet/pull/4203) ## 2024-09-29 - 3.3.0 (no 3.0.\*, 3.1.\*, 3.2.\*) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e89e99ec5..209a9199e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,10 +4,11 @@ If you would like to contribute, here are some notes and guidelines: - All new development should be on feature/fix branches, which are then merged to the `master` branch once stable and approved; so the `master` branch is always the most up-to-date, working code - If you are going to submit a pull request, please fork from `master`, and submit your pull request back as a fix/feature branch referencing the GitHub issue number + - Install (development) dependencies by running `composer install` inside your PhpSpreadsheet clone. - The code must work with all PHP versions that we support. - You can call `composer versions` to test version compatibility. - Code style should be maintained. - - `composer style` will identify any issues with Coding Style`. + - `composer style` will identify any issues with Coding Style. - `composer fix` will fix most issues with Coding Style. - All code changes must be validated by `composer check`. - Please include Unit Tests to verify that a bug exists, and that this PR fixes it. @@ -39,7 +40,10 @@ This makes it easier to see exactly what is being tested when reviewing the PR. 2. Tag subject must be the version number, eg: `1.2.3` 3. Tag body must be a copy-paste of the changelog entries. 3. Push the tag with `git push --tags`, GitHub Actions will create a GitHub release automatically, and the release details will automatically be sent to packagist. -4. Github seems to remove markdown headings in the Release Notes, so you should edit to restore these. +4. By default, Github remove markdown headings in the Release Notes. You can either edit to restore these, or, probably preferably, change the default comment character on your system - `git config core.commentChar ';'`. -> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.) +> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.) However, there are 3 branches which have been updated to apply security patches, and those may be tagged if future security updates are needed. +- release1291 +- release210 +- release222 diff --git a/composer.json b/composer.json index a823d2378..6acb8333c 100644 --- a/composer.json +++ b/composer.json @@ -45,12 +45,12 @@ ], "scripts": { "check": [ - "./bin/check-phpdoc-types", + "php ./bin/check-phpdoc-types", "phpcs samples/ src/ tests/ --report=checkstyle", - "phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- -n", + "phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- --exclude=PHPCompatibility.Variables.ForbiddenThisUseContexts -n", "php-cs-fixer fix --ansi --dry-run --diff", - "phpunit --color=always", - "phpstan analyse --ansi --memory-limit=2048M" + "phpstan analyse --ansi --memory-limit=2048M", + "phpunit --color=always" ], "style": [ "phpcs samples/ src/ tests/ --report=checkstyle", @@ -61,7 +61,7 @@ "php-cs-fixer fix" ], "versions": [ - "phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- -n" + "phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- --exclude=PHPCompatibility.Variables.ForbiddenThisUseContexts -n" ] }, "require": { diff --git a/composer.lock b/composer.lock index 1446a2da2..aa0082c22 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "maennchen/zipstream-php", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1" + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/b8174494eda667f7d13876b4a7bfef0f62a7c0d1", - "reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9", "shasum": "" }, "require": { @@ -73,19 +73,15 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.0" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1" }, "funding": [ { "url": "https://github.com/maennchen", "type": "github" - }, - { - "url": "https://opencollective.com/zipstream", - "type": "open_collective" } ], - "time": "2023-06-21T14:59:35+00:00" + "time": "2024-10-10T12:33:01+00:00" }, { "name": "markbaker/complex", @@ -1793,16 +1789,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.8", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec" + "reference": "384af967d35b2162f69526c7276acadce534d0e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec", - "reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/384af967d35b2162f69526c7276acadce534d0e1", + "reference": "384af967d35b2162f69526c7276acadce534d0e1", "shasum": "" }, "require": { @@ -1847,7 +1843,7 @@ "type": "github" } ], - "time": "2024-07-24T07:01:22+00:00" + "time": "2024-08-27T09:18:05+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -4973,20 +4969,20 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -5033,7 +5029,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -5049,7 +5045,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php81", @@ -5129,16 +5125,16 @@ }, { "name": "symfony/process", - "version": "v6.4.8", + "version": "v6.4.14", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" + "reference": "25214adbb0996d18112548de20c281be9f27279f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "url": "https://api.github.com/repos/symfony/process/zipball/25214adbb0996d18112548de20c281be9f27279f", + "reference": "25214adbb0996d18112548de20c281be9f27279f", "shasum": "" }, "require": { @@ -5170,7 +5166,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.8" + "source": "https://github.com/symfony/process/tree/v6.4.14" }, "funding": [ { @@ -5186,7 +5182,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-11-06T09:25:01+00:00" }, { "name": "symfony/service-contracts", @@ -5421,16 +5417,16 @@ }, { "name": "tecnickcom/tcpdf", - "version": "6.7.5", + "version": "6.7.7", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "951eabf0338ec2522bd0d5d9c79b08a3a3d36b36" + "reference": "cfbc0028cc23f057f2baf9e73bdc238153c22086" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/951eabf0338ec2522bd0d5d9c79b08a3a3d36b36", - "reference": "951eabf0338ec2522bd0d5d9c79b08a3a3d36b36", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/cfbc0028cc23f057f2baf9e73bdc238153c22086", + "reference": "cfbc0028cc23f057f2baf9e73bdc238153c22086", "shasum": "" }, "require": { @@ -5481,7 +5477,7 @@ ], "support": { "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.7.5" + "source": "https://github.com/tecnickcom/TCPDF/tree/6.7.7" }, "funding": [ { @@ -5489,7 +5485,7 @@ "type": "custom" } ], - "time": "2024-04-20T17:25:10+00:00" + "time": "2024-10-26T12:15:02+00:00" }, { "name": "theseer/tokenizer", diff --git a/docs/topics/Behind the Mask.md b/docs/topics/Behind the Mask.md index 9648f52df..ba77fcc4e 100644 --- a/docs/topics/Behind the Mask.md +++ b/docs/topics/Behind the Mask.md @@ -117,7 +117,10 @@ If you wish to emulate the MS Excel behaviour, and automatically convert string You can do this by changing the Value Binder, which will then apply every time you set a Cell value. ```php +// Old method using static property Cell::setValueBinder(new AdvancedValueBinder()); +// Preferred method using dynamic property since 3.4.0 +$spreadsheet->setValueBinder(new AdvancedValueBinder()); // Set Cell C21 using a formatted string value $worksheet->getCell('C20')->setValue('€ -12345.6789'); diff --git a/docs/topics/The Dating Game.md b/docs/topics/The Dating Game.md index 75726614d..5b0c2812f 100644 --- a/docs/topics/The Dating Game.md +++ b/docs/topics/The Dating Game.md @@ -257,7 +257,10 @@ $spreadsheet = new Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); // Use the Advanced Value Binder so that our string date/time values will be automatically converted // to Excel serialized date/timestamps +// Old method using static property Cell::setValueBinder(new AdvancedValueBinder()); +// Preferred method using dynamic property since 3.4.0 +$spreadsheet->setValueBinder(new AdvancedValueBinder()); // Write our data to the worksheet $worksheet->fromArray($projectHeading); diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md index d377da899..a9fe3695d 100644 --- a/docs/topics/accessing-cells.md +++ b/docs/topics/accessing-cells.md @@ -518,15 +518,15 @@ style information. The following example demonstrates how to set the value binder in PhpSpreadsheet: ```php -/** PhpSpreadsheet */ -require_once 'src/Boostrap.php'; - -// Set value binder +// Older method using static property \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); - // Create new Spreadsheet object $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); +// Preferred method using dynamic property since 3.4.0 +$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); +$spreadsheet->setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); + // ... // Add some data, resembling some different data types $spreadsheet->getActiveSheet()->setCellValue('A4', 'Percentage value:'); @@ -555,13 +555,20 @@ $stringValueBinder->setNumericConversion(false) ->setBooleanConversion(false) ->setNullConversion(false) ->setFormulaConversion(false); +// Older method using static property \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( $stringValueBinder ); +// Preferred method using dynamic property since 3.4.0 +$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); +$spreadsheet->setValueBinder( $stringValueBinder ); ``` You can override the current binder when setting individual cell values by specifying a different Binder to use in the Cell's `setValue()` or the Worksheet's `setCellValue()` methods. ```php $spreadsheet = new Spreadsheet(); +// Old method using static property Cell::setValueBinder(new AdvancedValueBinder()); +// Preferred method using dynamic property since 3.4.0 +$spreadsheet->setValueBinder(new AdvancedValueBinder()); $value = '12.5%'; diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index aa6298df9..e1d5229cd 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -1113,19 +1113,19 @@ Flags that are available that can be passed to the Reader in this way include: - $reader::LOAD_WITH_CHARTS - $reader::READ_DATA_ONLY - - $reader::IGNORE_EMPTY_CELLS - - $reader::SKIP_EMPTY_CELLS (synonym for IGNORE_EMPTY_CELLS) + - $reader::IGNORE_EMPTY_CELLS + - $reader::IGNORE_ROWS_WITH_NO_CELLS -| Readers | LOAD_WITH_CHARTS | READ_DATA_ONLY | IGNORE_EMPTY_CELLS | -|----------|------------------|----------------|--------------------| -| Xlsx | YES | YES | YES | -| Xls | NO | YES | YES | -| Xml | NO | NO | NO | -| Ods | NO | YES | NO | -| Gnumeric | NO | YES | NO | -| Html | N/A | N/A | N/A | -| Slk | N/A | NO | NO | -| Csv | N/A | NO | NO | +| Readers | LOAD_WITH_CHARTS | READ_DATA_ONLY | IGNORE_EMPTY_CELLS | IGNORE_ROWS_WITH_NO_CELLS | +|----------|------------------|----------------|--------------------|---------------------------| +| Xlsx | YES | YES | YES | YES | +| Xls | NO | YES | YES | NO | +| Xml | NO | NO | NO | NO | +| Ods | NO | YES | NO | NO | +| Gnumeric | NO | YES | NO | NO | +| Html | N/A | N/A | N/A | N/A | +| Slk | N/A | NO | NO | NO | +| Csv | N/A | NO | NO | NO | Likewise, when saving a file using a Writer, loaded charts will not be saved unless you explicitly tell the Writer to include them: @@ -1162,5 +1162,5 @@ Two or more flags can be passed together using PHP's `|` operator. ```php $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile("myExampleFile.xlsx"); -$reader->load("spreadsheetWithCharts.xlsx", $reader::READ_DATA_ONLY | $reader::SKIP_EMPTY_CELLS); +$reader->load("spreadsheetWithCharts.xlsx", $reader::READ_DATA_ONLY | $reader::IGNORE_EMPTY_CELLS); ``` diff --git a/docs/topics/reading-files.md b/docs/topics/reading-files.md index 8f74978d3..4aba019ff 100644 --- a/docs/topics/reading-files.md +++ b/docs/topics/reading-files.md @@ -755,14 +755,18 @@ So using a Value Binder allows a great deal more flexibility in the loader logic when reading unformatted text files. ```php -/** Tell PhpSpreadsheet that we want to use the Advanced Value Binder **/ -\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); - $inputFileType = 'Csv'; $inputFileName = './sampleData/example1.tsv'; $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType); $reader->setDelimiter("\t"); + +/** Tell PhpSpreadsheet that we want to use the Advanced Value Binder **/ +// Old method using static property +\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); +// Preferred method using dynamic property since 3.4.0 +$reader::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); + $spreadsheet = $reader->load($inputFileName); ``` @@ -774,7 +778,7 @@ Loading using a Value Binder applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N ----------|:---:|--------|:---:|--------------|:---: Xlsx | NO | Xls | NO | Xml | NO -Ods | NO | SYLK | NO | Gnumeric | NO +Ods | NO | SYLK | YES | Gnumeric | NO CSV | YES | HTML | YES Note that you can also use the Binder to determine how PhpSpreadsheet identified datatypes for values when you set a cell value without explicitly setting a datatype. diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 670714c84..b5a392703 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -233,7 +233,10 @@ method that suits you the best. Here are some examples: ```php // MySQL-like timestamp '2008-12-31' or date string +// Old method using static property \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); +// Preferred method using dynamic property since 3.4.0 +$spreadsheet->setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); $spreadsheet->getActiveSheet() ->setCellValue('D1', '2008-12-31'); @@ -599,7 +602,10 @@ when it sees a newline character in a string that you are inserting in a cell. Just like Microsoft Office Excel. Try this: ```php +// Old method using static property \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); +// Preferred method using dynamic property since 3.4.0 +$spreadsheet->setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() ); $spreadsheet->getActiveSheet()->getCell('A1')->setValue("hello\nworld"); ``` diff --git a/samples/Basic4/53_ImageOpacity.php b/samples/Basic4/53_ImageOpacity.php new file mode 100644 index 000000000..75d22bae9 --- /dev/null +++ b/samples/Basic4/53_ImageOpacity.php @@ -0,0 +1,69 @@ +getProperties()->setTitle('53_ImageOpacity'); + +$helper->log('Add image to spreadsheet 6 times with different opacities'); +$sheet = $spreadsheet->getActiveSheet(); +$sheet->setTitle('Squares different opacities'); +$sheet->setShowGridLines(false); + +$drawing = new Drawing(); +$drawing->setName('Blue Square opacity not specified'); +$drawing->setPath($path); +$drawing->setCoordinates('A1'); +$drawing->setCoordinates2('B5'); +$drawing->setWorksheet($sheet); + +$drawing = new Drawing(); +$drawing->setName('Blue Square opacity 80%'); +$drawing->setPath($path); +$drawing->setCoordinates('C1'); +$drawing->setCoordinates2('D5'); +$drawing->setOpacity(80000); +$drawing->setWorksheet($sheet); + +$drawing = new Drawing(); +$drawing->setName('Blue Square opacity 60%'); +$drawing->setPath($path); +$drawing->setCoordinates('E1'); +$drawing->setCoordinates2('F5'); +$drawing->setOpacity(60000); +$drawing->setWorksheet($sheet); + +$drawing = new Drawing(); +$drawing->setName('Blue Square opacity 40%'); +$drawing->setPath($path); +$drawing->setCoordinates('A8'); +$drawing->setCoordinates2('B12'); +$drawing->setOpacity(40000); +$drawing->setWorksheet($sheet); + +$drawing = new Drawing(); +$drawing->setName('Blue Square opacity 20%'); +$drawing->setPath($path); +$drawing->setCoordinates('C8'); +$drawing->setCoordinates2('D12'); +$drawing->setOpacity(20000); +$drawing->setWorksheet($sheet); + +$drawing = new Drawing(); +$drawing->setName('Blue Square opacity 0%'); +$drawing->setPath($path); +$drawing->setCoordinates('E8'); +$drawing->setCoordinates2('F12'); +$drawing->setOpacity(0); +$drawing->setWorksheet($sheet); + +// Save +$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Html', 'Dompdf', 'Mpdf']); +$spreadsheet->disconnectWorksheets(); diff --git a/samples/Chart33a/33_Chart_create_area.php b/samples/Chart33a/33_Chart_create_area.php index c6e349aa1..4e6fd4ec4 100644 --- a/samples/Chart33a/33_Chart_create_area.php +++ b/samples/Chart33a/33_Chart_create_area.php @@ -91,6 +91,8 @@ $chart = new Chart( $chart->setTopLeftPosition('A7'); $chart->setBottomRightPosition('H20'); +$chart->setNoBorder(true); + // Add the chart to the worksheet $worksheet->addChart($chart); diff --git a/samples/images/blue_square.png b/samples/images/blue_square.png new file mode 100644 index 000000000..4528c17f0 Binary files /dev/null and b/samples/images/blue_square.png differ diff --git a/samples/templates/27template.xlsx b/samples/templates/27template.xlsx index 9651f1735..441522177 100644 Binary files a/samples/templates/27template.xlsx and b/samples/templates/27template.xlsx differ diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 6fa1dde2c..05b5ef52b 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4160,8 +4160,10 @@ class Calculation } } } elseif ($expectedArgumentCount != '*') { - preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch); - switch ($argMatch[2] ?? '') { + if (1 !== preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) { + $argMatch = ['', '', '', '']; + } + switch ($argMatch[2]) { case '+': if ($argumentCount < $argMatch[1]) { $argumentCountError = true; @@ -4234,7 +4236,7 @@ class Calculation // do we now have a function/variable/number? $expectingOperator = true; $expectingOperand = false; - $val = $match[1]; + $val = $match[1] ?? ''; $length = strlen($val); if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { @@ -4290,7 +4292,7 @@ class Calculation $rangeStartCellRef = $output[count($output) - 2]['value'] ?? ''; } preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches); - if ($rangeStartMatches[2] !== $matches[2]) { + if (isset($rangeStartMatches[2]) && $rangeStartMatches[2] !== $matches[2]) { return $this->raiseFormulaError('3D Range references are not yet supported'); } } @@ -4380,7 +4382,7 @@ class Calculation $valx = $val; $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : AddressRange::MAX_COLUMN; // Max 16,384 columns for Excel2007 $val = "{$rangeWS2}{$endRowColRef}{$val}"; - } elseif (ctype_alpha($val) && strlen($val ?? '') <= 3) { + } elseif (ctype_alpha($val) && is_string($val) && strlen($val) <= 3) { // Column range $stackItemType = 'Column Reference'; $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007 @@ -4545,6 +4547,12 @@ class Calculation return $operand; } + private static int $matchIndex8 = 8; + + private static int $matchIndex9 = 9; + + private static int $matchIndex10 = 10; + /** * @return array|false */ @@ -4908,12 +4916,17 @@ class Calculation } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token ?? '', $matches)) { $cellRef = null; - if (isset($matches[8])) { + /* Phpstan says matches[8/9/10] is never set, + and code coverage report seems to confirm. + Appease PhpStan for now; + probably delete this block later. + */ + if (isset($matches[self::$matchIndex8])) { if ($cell === null) { // We can't access the range, so return a REF error $cellValue = ExcelError::REF(); } else { - $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10]; + $cellRef = $matches[6] . $matches[7] . ':' . $matches[self::$matchIndex9] . $matches[self::$matchIndex10]; if ($matches[2] > '') { $matches[2] = trim($matches[2], "\"'"); if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) { diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 77f8317ae..9a094966a 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -164,8 +164,10 @@ class Functions return str_replace('""""', '""', '=' . $condition); } - preg_match('/(=|<[>=]?|>=?)(.*)/', $condition, $matches); - [, $operator, $operand] = $matches; + $operator = $operand = ''; + if (1 === preg_match('/(=|<[>=]?|>=?)(.*)/', $condition, $matches)) { + [, $operator, $operand] = $matches; + } $operand = self::operandSpecialHandling($operand); if (is_numeric(trim($operand, '"'))) { diff --git a/src/PhpSpreadsheet/Calculation/Information/Value.php b/src/PhpSpreadsheet/Calculation/Information/Value.php index a99c2343f..49361ef06 100644 --- a/src/PhpSpreadsheet/Calculation/Information/Value.php +++ b/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -210,10 +210,11 @@ class Value $fullCellReference = Functions::trimTrailingRange($fullCellReference); - preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $fullCellReference, $matches); - - $fullCellReference = $matches[6] . $matches[7]; - $worksheetName = str_replace("''", "'", trim($matches[2], "'")); + $worksheetName = ''; + if (1 == preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $fullCellReference, $matches)) { + $fullCellReference = $matches[6] . $matches[7]; + $worksheetName = str_replace("''", "'", trim($matches[2], "'")); + } $worksheet = (!empty($worksheetName)) ? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php b/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php index 5c7f4051c..55a2a8fff 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php @@ -20,13 +20,14 @@ class Formula return ExcelError::REF(); } - preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches); - - $cellReference = $matches[6] . $matches[7]; - $worksheetName = trim($matches[3], "'"); - $worksheet = (!empty($worksheetName)) - ? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName) - : $cell->getWorksheet(); + $worksheet = null; + if (1 === preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches)) { + $cellReference = $matches[6] . $matches[7]; + $worksheetName = trim($matches[3], "'"); + $worksheet = (!empty($worksheetName)) + ? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName) + : $cell->getWorksheet(); + } if ( $worksheet === null diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Round.php b/src/PhpSpreadsheet/Calculation/MathTrig/Round.php index d2aa1c0b1..e0e03ae96 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Round.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Round.php @@ -5,6 +5,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +// following added in Php8.4 +use RoundingMode; class Round { @@ -67,22 +69,19 @@ class Round return 0.0; } - $digitsPlus1 = $digits + 1; + if (PHP_VERSION_ID >= 80400) { + return round( + (float) (string) $number, + $digits, + RoundingMode::AwayFromZero //* @phpstan-ignore-line + ); + } + if ($number < 0.0) { - if ($digitsPlus1 < 0) { - return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); - } - $result = sprintf("%.{$digitsPlus1}F", $number - 0.5 * 0.1 ** $digits); - - return round((float) $result, $digits, PHP_ROUND_HALF_DOWN); + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); } - if ($digitsPlus1 < 0) { - return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); - } - $result = sprintf("%.{$digitsPlus1}F", $number + 0.5 * 0.1 ** $digits); - - return round((float) $result, $digits, PHP_ROUND_HALF_DOWN); + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); } /** @@ -90,8 +89,8 @@ class Round * * Rounds a number down to a specified number of decimal places * - * @param array|float $number Number to round, or can be an array of numbers - * @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers + * @param null|array|float|string $number Number to round, or can be an array of numbers + * @param array|float|int|string $digits Number of digits to which you want to round $number, or can be an array of numbers * * @return array|float|string Rounded Number, or a string containing an error * If an array of numbers is passed as the argument, then the returned result will also be an array @@ -114,23 +113,19 @@ class Round return 0.0; } - $digitsPlus1 = $digits + 1; + if (PHP_VERSION_ID >= 80400) { + return round( + (float) (string) $number, + $digits, + RoundingMode::TowardsZero //* @phpstan-ignore-line + ); + } + if ($number < 0.0) { - if ($digitsPlus1 < 0) { - return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); - } - $result = sprintf("%.{$digitsPlus1}F", $number + 0.5 * 0.1 ** $digits); - - return round((float) $result, $digits, PHP_ROUND_HALF_UP); + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); } - if ($digitsPlus1 < 0) { - return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); - } - - $result = sprintf("%.{$digitsPlus1}F", $number - 0.5 * 0.1 ** $digits); - - return round((float) $result, $digits, PHP_ROUND_HALF_UP); + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php b/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php index f36f14d70..096b27a58 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; -use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Trunc { @@ -19,47 +18,19 @@ class Trunc * (or possibly that value minus 1). * Excel is unlikely to do any better. * - * @param array|float $value Or can be an array of values - * @param array|int $digits Or can be an array of values + * @param null|array|float|string $value Or can be an array of values + * @param array|float|int|string $digits Or can be an array of values * * @return array|float|string Truncated value, or a string containing an error * If an array of numbers is passed as an argument, then the returned result will also be an array * with the same dimensions */ - public static function evaluate(array|float|string|null $value = 0, array|int|string $digits = 0): array|float|string + public static function evaluate(array|float|string|null $value = 0, array|float|int|string $digits = 0): array|float|string { if (is_array($value) || is_array($digits)) { return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $digits); } - try { - $value = Helpers::validateNumericNullBool($value); - $digits = Helpers::validateNumericNullSubstitution($digits, null); - } catch (Exception $e) { - return $e->getMessage(); - } - - if ($value == 0) { - return $value; - } - - if ($value >= 0) { - $minusSign = ''; - } else { - $minusSign = '-'; - $value = -$value; - } - $digits = (int) floor($digits); - if ($digits < 0) { - $result = (float) (substr(sprintf('%.0F', $value), 0, $digits) . str_repeat('0', -$digits)); - - return ($minusSign === '') ? $result : -$result; - } - $decimals = (floor($value) == (int) $value) ? (PHP_FLOAT_DIG - strlen((string) (int) $value)) : $digits; - $resultString = ($decimals < 0) ? sprintf('%F', $value) : sprintf('%.' . $decimals . 'F', $value); - $regExp = '/([.]\\d{' . $digits . '})\\d+$/'; - $result = $minusSign . (preg_replace($regExp, '$1', $resultString) ?? $resultString); - - return (float) $result; + return Round::down($value, $digits); } } diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index 923f9417a..61139727f 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -136,9 +136,7 @@ class AddressHelper ?int $currentRowNumber = null, ?int $currentColumnNumber = null ): string { - $validityCheck = preg_match(Coordinate::A1_COORDINATE_REGEX, $address, $cellReference); - - if ($validityCheck === 0) { + if (1 !== preg_match(Coordinate::A1_COORDINATE_REGEX, $address, $cellReference)) { throw new Exception('Invalid A1-format Cell Reference'); } diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index e8e9afa71..1e353db91 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -112,8 +112,11 @@ class Cell implements Stringable $dataType = DataType::TYPE_STRING; } $this->dataType = $dataType; - } elseif (self::getValueBinder()->bindValue($this, $value) === false) { - throw new SpreadsheetException('Value could not be bound to cell.'); + } else { + $valueBinder = $worksheet->getParent()?->getValueBinder() ?? self::getValueBinder(); + if ($valueBinder->bindValue($this, $value) === false) { + throw new SpreadsheetException('Value could not be bound to cell.'); + } } $this->ignoredErrors = new IgnoredErrors(); } @@ -191,7 +194,7 @@ class Cell implements Stringable $currentCalendar = SharedDate::getExcelCalendar(); SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar()); $formattedValue = (string) NumberFormat::toFormattedString( - $this->getCalculatedValue(), + $this->getCalculatedValueString(), (string) $this->getStyle()->getNumberFormat()->getFormatCode(true) ); SharedDate::setExcelCalendar($currentCalendar); @@ -232,7 +235,8 @@ class Cell implements Stringable */ public function setValue(mixed $value, ?IValueBinder $binder = null): self { - $binder ??= self::getValueBinder(); + // Cells?->Worksheet?->Spreadsheet + $binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder(); if (!$binder->bindValue($this, $value)) { throw new SpreadsheetException('Value could not be bound to cell.'); } @@ -495,9 +499,10 @@ class Cell implements Stringable if (isset($matches[3])) { $minCol = $matches[1]; $minRow = (int) $matches[2]; - $maxCol = $matches[4]; + // https://github.com/phpstan/phpstan/issues/11602 + $maxCol = $matches[4]; // @phpstan-ignore-line ++$maxCol; - $maxRow = (int) $matches[5]; + $maxRow = (int) $matches[5]; // @phpstan-ignore-line for ($row = $minRow; $row <= $maxRow; ++$row) { for ($col = $minCol; $col !== $maxCol; ++$col) { if ("$col$row" !== $coordinate) { @@ -934,7 +939,7 @@ class Cell implements Stringable * * @return $this */ - public function setFormulaAttributes(mixed $attributes): self + public function setFormulaAttributes(?array $attributes): self { $this->formulaAttributes = $attributes; diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 313cc3d9b..237f1cd15 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -271,8 +271,7 @@ abstract class Coordinate private static function validateReferenceAndGetData($reference): array { $data = []; - preg_match(self::FULL_REFERENCE_REGEX, $reference, $matches); - if (count($matches) === 0) { + if (1 !== preg_match(self::FULL_REFERENCE_REGEX, $reference, $matches)) { return ['type' => 'invalid']; } diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index dae43c95d..6bcae28c8 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -106,6 +106,8 @@ class Chart private bool $noFill = false; + private bool $noBorder = false; + private bool $roundedCorners = false; private GridLines $borderLines; @@ -696,6 +698,18 @@ class Chart return $this; } + public function getNoBorder(): bool + { + return $this->noBorder; + } + + public function setNoBorder(bool $noBorder): self + { + $this->noBorder = $noBorder; + + return $this; + } + public function getRoundedCorners(): bool { return $this->roundedCorners; diff --git a/src/PhpSpreadsheet/Chart/Layout.php b/src/PhpSpreadsheet/Chart/Layout.php index 570892ec1..5dc2991a4 100644 --- a/src/PhpSpreadsheet/Chart/Layout.php +++ b/src/PhpSpreadsheet/Chart/Layout.php @@ -450,6 +450,13 @@ class Layout return $this->labelFont; } + public function setLabelFont(?Font $labelFont): self + { + $this->labelFont = $labelFont; + + return $this; + } + public function getLabelEffects(): ?Properties { return $this->labelEffects; diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index e690ae652..f9fc6e3dc 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -561,6 +561,7 @@ class Html 'u' => [self::class, 'startUnderlineTag'], 'ins' => [self::class, 'startUnderlineTag'], 'del' => [self::class, 'startStrikethruTag'], + 's' => [self::class, 'startStrikethruTag'], 'sup' => [self::class, 'startSuperscriptTag'], 'sub' => [self::class, 'startSubscriptTag'], ]; @@ -575,6 +576,7 @@ class Html 'u' => [self::class, 'endUnderlineTag'], 'ins' => [self::class, 'endUnderlineTag'], 'del' => [self::class, 'endStrikethruTag'], + 's' => [self::class, 'endStrikethruTag'], 'sup' => [self::class, 'endSuperscriptTag'], 'sub' => [self::class, 'endSubscriptTag'], 'br' => [self::class, 'breakTag'], diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 3e2ed7569..136c458d4 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -120,6 +120,9 @@ class Sample // Write documents foreach ($writers as $writerType) { $path = $this->getFilename($filename, mb_strtolower($writerType)); + if (preg_match('/(mpdf|tcpdf)$/', $path)) { + $path .= '.pdf'; + } $writer = IOFactory::createWriter($spreadsheet, $writerType); $writer->setIncludeCharts($withCharts); if ($writerCallback !== null) { diff --git a/src/PhpSpreadsheet/Helper/Size.php b/src/PhpSpreadsheet/Helper/Size.php index 575ed890b..c3e4e4598 100644 --- a/src/PhpSpreadsheet/Helper/Size.php +++ b/src/PhpSpreadsheet/Helper/Size.php @@ -8,7 +8,7 @@ class Size implements Stringable { const REGEXP_SIZE_VALIDATION = '/^(?P\d*\.?\d+)(?Ppt|px|em)?$/i'; - protected bool $valid; + protected bool $valid = false; protected string $size = ''; @@ -16,8 +16,8 @@ class Size implements Stringable public function __construct(string $size) { - $this->valid = (bool) preg_match(self::REGEXP_SIZE_VALIDATION, $size, $matches); - if ($this->valid) { + if (1 === preg_match(self::REGEXP_SIZE_VALIDATION, $size, $matches)) { + $this->valid = true; $this->size = $matches['size']; $this->unit = $matches['unit'] ?? 'pt'; } diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php index 1408f7bf0..de80834d1 100644 --- a/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/BaseReader.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; @@ -19,7 +20,7 @@ abstract class BaseReader implements IReader /** * Read empty cells? - * Identifies whether the Reader should read data values for cells all cells, or should ignore cells containing + * Identifies whether the Reader should read data values for all cells, or should ignore cells containing * null value or empty string. */ protected bool $readEmptyCells = true; @@ -56,6 +57,8 @@ abstract class BaseReader implements IReader protected ?XmlScanner $securityScanner = null; + protected ?IValueBinder $valueBinder = null; + public function __construct() { $this->readFilter = new DefaultReadFilter(); @@ -166,7 +169,7 @@ abstract class BaseReader implements IReader if (((bool) ($flags & self::READ_DATA_ONLY)) === true) { $this->setReadDataOnly(true); } - if (((bool) ($flags & self::SKIP_EMPTY_CELLS) || (bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) { + if (((bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) { $this->setReadEmptyCells(false); } if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) { @@ -242,4 +245,16 @@ abstract class BaseReader implements IReader return $returnArray; } + + public function getValueBinder(): ?IValueBinder + { + return $this->valueBinder; + } + + public function setValueBinder(?IValueBinder $valueBinder): self + { + $this->valueBinder = $valueBinder; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index 762ec3191..33bee9cdf 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use Throwable; class Csv extends BaseReader { @@ -74,7 +75,7 @@ class Csv extends BaseReader * It is anticipated that it will conditionally be set * to null-string for Php9 and above. */ - private static string $defaultEscapeCharacter = '\\'; + private static string $defaultEscapeCharacter = PHP_VERSION_ID < 90000 ? '\\' : ''; /** * Callback for setting defaults in construction. @@ -262,6 +263,7 @@ class Csv extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); // Load into this instance return $this->loadIntoExisting($filename, $spreadsheet); @@ -274,6 +276,7 @@ class Csv extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); // Load into this instance return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true); @@ -286,6 +289,12 @@ class Csv extends BaseReader if (!$fhandle) { throw new ReaderException($filename . ' is an Invalid Spreadsheet file.'); } + if ($this->inputEncoding === 'UTF-8') { + $encoding = self::guessEncodingBom($filename); + if ($encoding !== '') { + $this->inputEncoding = $encoding; + } + } if ($this->inputEncoding === self::GUESS_ENCODING) { $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding); } @@ -313,7 +322,7 @@ class Csv extends BaseReader private function setAutoDetect(?string $value): ?string { $retVal = null; - if ($value !== null && $this->testAutodetect) { + if ($value !== null && $this->testAutodetect && PHP_VERSION_ID < 90000) { $retVal2 = @ini_set('auto_detect_line_endings', $value); if (is_string($retVal2)) { $retVal = $retVal2; @@ -362,6 +371,21 @@ class Csv extends BaseReader // Deprecated in Php8.1 $iniset = $this->setAutoDetect('1'); + try { + $this->loadStringOrFile2($filename, $spreadsheet, $dataUri); + $this->setAutoDetect($iniset); + } catch (Throwable $e) { + $this->setAutoDetect($iniset); + + throw $e; + } + + return $spreadsheet; + } + + private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void + { + // Open file if ($dataUri) { $this->openDataUri($filename); @@ -391,7 +415,7 @@ class Csv extends BaseReader // Loop through each line of the file in turn $delimiter = $this->delimiter ?? ''; $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); - $valueBinder = Cell::getValueBinder(); + $valueBinder = $this->valueBinder ?? Cell::getValueBinder(); $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion(); $this->getTrue = Calculation::getTRUE(); $this->getFalse = Calculation::getFALSE(); @@ -433,11 +457,6 @@ class Csv extends BaseReader // Close file fclose($fileHandle); - - $this->setAutoDetect($iniset); - - // Return - return $spreadsheet; } /** @@ -545,6 +564,10 @@ class Csv extends BaseReader */ public function setEscapeCharacter(string $escapeCharacter): self { + if (PHP_VERSION_ID >= 90000 && $escapeCharacter !== '') { + throw new ReaderException('Escape character must be null string for Php9+'); + } + $this->escapeCharacter = $escapeCharacter; return $this; @@ -622,17 +645,15 @@ class Csv extends BaseReader } } - private static function guessEncodingBom(string $filename): string + public static function guessEncodingBom(string $filename, ?string $convertString = null): string { $encoding = ''; - $first4 = file_get_contents($filename, false, null, 0, 4); - if ($first4 !== false) { - self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); - self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); - self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); - self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); - self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); - } + $first4 = $convertString ?? (string) file_get_contents($filename, false, null, 0, 4); + self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); return $encoding; } @@ -689,4 +710,39 @@ class Csv extends BaseReader return fgetcsv($stream, $length, $separator, $enclosure, $escape); } + + public static function affectedByPhp9( + string $filename, + string $inputEncoding = 'UTF-8', + ?string $delimiter = null, + string $enclosure = '"', + string $escapeCharacter = '\\' + ): bool { + if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 90000) { + throw new ReaderException('Function valid only for Php7.4 or Php8'); // @codeCoverageIgnore + } + $reader1 = new self(); + $reader1->setInputEncoding($inputEncoding) + ->setTestAutoDetect(true) + ->setEscapeCharacter($escapeCharacter) + ->setDelimiter($delimiter) + ->setEnclosure($enclosure); + $spreadsheet1 = $reader1->load($filename); + $sheet1 = $spreadsheet1->getActiveSheet(); + $array1 = $sheet1->toArray(null, false, false); + $spreadsheet1->disconnectWorksheets(); + + $reader2 = new self(); + $reader2->setInputEncoding($inputEncoding) + ->setTestAutoDetect(false) + ->setEscapeCharacter('') + ->setDelimiter($delimiter) + ->setEnclosure($enclosure); + $spreadsheet2 = $reader2->load($filename); + $sheet2 = $spreadsheet2->getActiveSheet(); + $array2 = $sheet2->toArray(null, false, false); + $spreadsheet2->disconnectWorksheets(); + + return $array1 !== $array2; + } } diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 898e32db5..d80a87ecc 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -226,6 +226,7 @@ class Gnumeric extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); $spreadsheet->removeSheetByIndex(0); // Load into this instance diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 2a2ffc410..4e00f7dd3 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -173,6 +173,7 @@ class Html extends BaseReader // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3 $filename = $meta['uri']; //@phpstan-ignore-line + clearstatcache(true, $filename); $size = (int) filesize($filename); if ($size === 0) { return ''; @@ -210,6 +211,7 @@ class Html extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); // Load into this instance return $this->loadIntoExisting($filename, $spreadsheet); @@ -315,6 +317,7 @@ class Html extends BaseReader try { $sheet->setTitle($cellContent, true, true); + $sheet->getParent()?->getProperties()?->setTitle($cellContent); } catch (SpreadsheetException) { // leave default title if too long or illegal chars } @@ -476,6 +479,11 @@ class Html extends BaseReader private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'table') { + if (isset($attributeArray['class'])) { + $classes = explode(' ', $attributeArray['class']); + $sheet->setShowGridlines(in_array('gridlines', $classes, true)); + $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true)); + } $this->currentColumn = 'A'; $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); $column = $this->setTableStartColumn($column); @@ -786,6 +794,7 @@ class Html extends BaseReader throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null); } $spreadsheet = $spreadsheet ?? new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); self::loadProperties($dom, $spreadsheet); return $this->loadDocument($dom, $spreadsheet); @@ -1038,14 +1047,21 @@ class Html extends BaseReader if (!isset($attributes['src'])) { return; } + $styleArray = self::getStyleArray($attributes); - $src = urldecode($attributes['src']); - $width = isset($attributes['width']) ? (float) $attributes['width'] : null; - $height = isset($attributes['height']) ? (float) $attributes['height'] : null; + $src = $attributes['src']; + if (substr($src, 0, 5) !== 'data:') { + $src = urldecode($src); + } + $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null); + $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null); $name = $attributes['alt'] ?? null; $drawing = new Drawing(); - $drawing->setPath($src); + $drawing->setPath($src, false); + if ($drawing->getPath() === '') { + return; + } $drawing->setWorksheet($sheet); $drawing->setCoordinates($column . $row); $drawing->setOffsetX(0); @@ -1057,10 +1073,12 @@ class Html extends BaseReader } if ($width) { - $drawing->setWidth((int) $width); - } - - if ($height) { + if ($height) { + $drawing->setWidthAndHeight((int) $width, (int) $height); + } else { + $drawing->setWidth((int) $width); + } + } elseif ($height) { $drawing->setHeight((int) $height); } @@ -1071,6 +1089,44 @@ class Html extends BaseReader $sheet->getRowDimension($row)->setRowHeight( $drawing->getHeight() * 0.9 ); + + if (isset($styleArray['opacity'])) { + $opacity = $styleArray['opacity']; + if (is_numeric($opacity)) { + $drawing->setOpacity((int) ($opacity * 100000)); + } + } + } + + private static function getStyleArray(array $attributes): array + { + $styleArray = []; + if (isset($attributes['style'])) { + $styles = explode(';', $attributes['style']); + foreach ($styles as $style) { + $value = explode(':', $style); + if (count($value) === 2) { + $arrayKey = trim($value[0]); + $arrayValue = trim($value[1]); + if ($arrayKey === 'width') { + if (substr($arrayValue, -2) === 'px') { + $arrayValue = (string) (((float) substr($arrayValue, 0, -2))); + } else { + $arrayValue = (new CssDimension($arrayValue))->width(); + } + } elseif ($arrayKey === 'height') { + if (substr($arrayValue, -2) === 'px') { + $arrayValue = substr($arrayValue, 0, -2); + } else { + $arrayValue = (new CssDimension($arrayValue))->height(); + } + } + $styleArray[$arrayKey] = $arrayValue; + } + } + } + + return $styleArray; } private const BORDER_MAPPINGS = [ diff --git a/src/PhpSpreadsheet/Reader/IReader.php b/src/PhpSpreadsheet/Reader/IReader.php index 62c2103ab..6075eef4f 100644 --- a/src/PhpSpreadsheet/Reader/IReader.php +++ b/src/PhpSpreadsheet/Reader/IReader.php @@ -6,13 +6,36 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; interface IReader { + /** + * Flag used to load the charts. + * + * This flag is supported only for some formats. + */ public const LOAD_WITH_CHARTS = 1; + /** + * Flag used to read data only, not style or structure information. + */ public const READ_DATA_ONLY = 2; - public const SKIP_EMPTY_CELLS = 4; + /** + * @deprecated 3.4.0 use IGNORE_EMPTY_CELLS instead. + */ + public const SKIP_EMPTY_CELLS = self::IGNORE_EMPTY_CELLS; + + /** + * Flag used to ignore empty cells when reading. + * + * The ignored cells will not be instantiated. + */ public const IGNORE_EMPTY_CELLS = 4; + /** + * Flag used to ignore rows without cells. + * + * This flag is supported only for some formats. + * This can heavily improve performance for some files. + */ public const IGNORE_ROWS_WITH_NO_CELLS = 8; public function __construct(); @@ -119,7 +142,7 @@ interface IReader * @param int $flags Flags that can change the behaviour of the Writer: * self::LOAD_WITH_CHARTS Load any charts that are defined (if the Reader supports Charts) * self::READ_DATA_ONLY Read only data, not style or structure information, from the file - * self::SKIP_EMPTY_CELLS Don't read empty cells (cells that contain a null value, + * self::IGNORE_EMPTY_CELLS Don't read empty cells (cells that contain a null value, * empty string, or a string containing only whitespace characters) */ public function load(string $filename, int $flags = 0): Spreadsheet; diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index da7b85cee..85fa08ecf 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -234,6 +234,7 @@ class Ods extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); $spreadsheet->removeSheetByIndex(0); // Load into this instance diff --git a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php index 362a70c12..be6498192 100644 --- a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php +++ b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -6,6 +6,9 @@ use PhpOffice\PhpSpreadsheet\Reader; class XmlScanner { + private const ENCODING_PATTERN = '/encoding\\s*=\\s*(["\'])(.+?)\\1/s'; + private const ENCODING_UTF7 = '/encoding\\s*=\\s*(["\'])UTF-7\\1/si'; + private string $pattern; /** @var ?callable */ @@ -36,13 +39,24 @@ class XmlScanner private function toUtf8(string $xml): string { $charset = $this->findCharSet($xml); + $foundUtf7 = $charset === 'UTF-7'; if ($charset !== 'UTF-8') { + $testStart = '/^.{0,4}\\s*findCharSet($xml); - if ($charset !== 'UTF-8') { - throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); + if ($startWithXml1 === 1 && preg_match($testStart, $xml) !== 1) { + throw new Reader\Exception('Double encoding not permitted'); } + $foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1); + $xml = preg_replace(self::ENCODING_PATTERN, '', $xml) ?? $xml; + } else { + $foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1); + } + if ($foundUtf7) { + throw new Reader\Exception('UTF-7 encoding not permitted'); + } + if (substr($xml, 0, Reader\Csv::UTF8_BOM_LEN) === Reader\Csv::UTF8_BOM) { + $xml = substr($xml, Reader\Csv::UTF8_BOM_LEN); } return $xml; @@ -50,15 +64,16 @@ class XmlScanner private function findCharSet(string $xml): string { - $patterns = [ - '/encoding\\s*=\\s*"([^"]*]?)"/', - "/encoding\\s*=\\s*'([^']*?)'/", - ]; - - foreach ($patterns as $pattern) { - if (preg_match($pattern, $xml, $matches)) { - return strtoupper($matches[1]); - } + if (substr($xml, 0, 4) === "\x4c\x6f\xa7\x94") { + throw new Reader\Exception('EBCDIC encoding not permitted'); + } + $encoding = Reader\Csv::guessEncodingBom('', $xml); + if ($encoding !== '') { + return $encoding; + } + $xml = str_replace("\0", '', $xml); + if (preg_match(self::ENCODING_PATTERN, $xml, $matches)) { + return strtoupper($matches[2]); } return 'UTF-8'; @@ -71,13 +86,15 @@ class XmlScanner */ public function scan($xml): string { + // Don't rely purely on libxml_disable_entity_loader() + $pattern = '/\\0*' . implode('\\0*', str_split($this->pattern)) . '\\0*/'; + $xml = "$xml"; + if (preg_match($pattern, $xml)) { + throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); + } $xml = $this->toUtf8($xml); - - // Don't rely purely on libxml_disable_entity_loader() - $pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/'; - if (preg_match($pattern, $xml)) { throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); } @@ -90,7 +107,7 @@ class XmlScanner } /** - * Scan theXML for use of setValueBinder($this->valueBinder); // Load into this instance return $this->loadIntoExisting($filename, $spreadsheet); diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 833b2550f..5883b20da 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -6,21 +6,16 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; -use PhpOffice\PhpSpreadsheet\NamedRange; -use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting; use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont; use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\CodePage; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Escher; -use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer; -use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\OLE; use PhpOffice\PhpSpreadsheet\Shared\OLERead; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; -use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; @@ -31,7 +26,6 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; use PhpOffice\PhpSpreadsheet\Style\Style; -use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -66,297 +60,175 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; // Patch code for user-defined named cells supports single cells only. // NOTE: this patch only works for BIFF8 as BIFF5-7 use a different // external sheet reference structure -class Xls extends BaseReader +class Xls extends XlsBase { - private const HIGH_ORDER_BIT = 0x80 << 24; - private const FC000000 = 0xFC << 24; - private const FE000000 = 0xFE << 24; - - // ParseXL definitions - const XLS_BIFF8 = 0x0600; - const XLS_BIFF7 = 0x0500; - const XLS_WORKBOOKGLOBALS = 0x0005; - const XLS_WORKSHEET = 0x0010; - - // record identifiers - const XLS_TYPE_FORMULA = 0x0006; - const XLS_TYPE_EOF = 0x000A; - const XLS_TYPE_PROTECT = 0x0012; - const XLS_TYPE_OBJECTPROTECT = 0x0063; - const XLS_TYPE_SCENPROTECT = 0x00DD; - const XLS_TYPE_PASSWORD = 0x0013; - const XLS_TYPE_HEADER = 0x0014; - const XLS_TYPE_FOOTER = 0x0015; - const XLS_TYPE_EXTERNSHEET = 0x0017; - const XLS_TYPE_DEFINEDNAME = 0x0018; - const XLS_TYPE_VERTICALPAGEBREAKS = 0x001A; - const XLS_TYPE_HORIZONTALPAGEBREAKS = 0x001B; - const XLS_TYPE_NOTE = 0x001C; - const XLS_TYPE_SELECTION = 0x001D; - const XLS_TYPE_DATEMODE = 0x0022; - const XLS_TYPE_EXTERNNAME = 0x0023; - const XLS_TYPE_LEFTMARGIN = 0x0026; - const XLS_TYPE_RIGHTMARGIN = 0x0027; - const XLS_TYPE_TOPMARGIN = 0x0028; - const XLS_TYPE_BOTTOMMARGIN = 0x0029; - const XLS_TYPE_PRINTGRIDLINES = 0x002B; - const XLS_TYPE_FILEPASS = 0x002F; - const XLS_TYPE_FONT = 0x0031; - const XLS_TYPE_CONTINUE = 0x003C; - const XLS_TYPE_PANE = 0x0041; - const XLS_TYPE_CODEPAGE = 0x0042; - const XLS_TYPE_DEFCOLWIDTH = 0x0055; - const XLS_TYPE_OBJ = 0x005D; - const XLS_TYPE_COLINFO = 0x007D; - const XLS_TYPE_IMDATA = 0x007F; - const XLS_TYPE_SHEETPR = 0x0081; - const XLS_TYPE_HCENTER = 0x0083; - const XLS_TYPE_VCENTER = 0x0084; - const XLS_TYPE_SHEET = 0x0085; - const XLS_TYPE_PALETTE = 0x0092; - const XLS_TYPE_SCL = 0x00A0; - const XLS_TYPE_PAGESETUP = 0x00A1; - const XLS_TYPE_MULRK = 0x00BD; - const XLS_TYPE_MULBLANK = 0x00BE; - const XLS_TYPE_DBCELL = 0x00D7; - const XLS_TYPE_XF = 0x00E0; - const XLS_TYPE_MERGEDCELLS = 0x00E5; - const XLS_TYPE_MSODRAWINGGROUP = 0x00EB; - const XLS_TYPE_MSODRAWING = 0x00EC; - const XLS_TYPE_SST = 0x00FC; - const XLS_TYPE_LABELSST = 0x00FD; - const XLS_TYPE_EXTSST = 0x00FF; - const XLS_TYPE_EXTERNALBOOK = 0x01AE; - const XLS_TYPE_DATAVALIDATIONS = 0x01B2; - const XLS_TYPE_TXO = 0x01B6; - const XLS_TYPE_HYPERLINK = 0x01B8; - const XLS_TYPE_DATAVALIDATION = 0x01BE; - const XLS_TYPE_DIMENSION = 0x0200; - const XLS_TYPE_BLANK = 0x0201; - const XLS_TYPE_NUMBER = 0x0203; - const XLS_TYPE_LABEL = 0x0204; - const XLS_TYPE_BOOLERR = 0x0205; - const XLS_TYPE_STRING = 0x0207; - const XLS_TYPE_ROW = 0x0208; - const XLS_TYPE_INDEX = 0x020B; - const XLS_TYPE_ARRAY = 0x0221; - const XLS_TYPE_DEFAULTROWHEIGHT = 0x0225; - const XLS_TYPE_WINDOW2 = 0x023E; - const XLS_TYPE_RK = 0x027E; - const XLS_TYPE_STYLE = 0x0293; - const XLS_TYPE_FORMAT = 0x041E; - const XLS_TYPE_SHAREDFMLA = 0x04BC; - const XLS_TYPE_BOF = 0x0809; - const XLS_TYPE_SHEETPROTECTION = 0x0867; - const XLS_TYPE_RANGEPROTECTION = 0x0868; - const XLS_TYPE_SHEETLAYOUT = 0x0862; - const XLS_TYPE_XFEXT = 0x087D; - const XLS_TYPE_PAGELAYOUTVIEW = 0x088B; - const XLS_TYPE_CFHEADER = 0x01B0; - const XLS_TYPE_CFRULE = 0x01B1; - const XLS_TYPE_UNKNOWN = 0xFFFF; - - // Encryption type - const MS_BIFF_CRYPTO_NONE = 0; - const MS_BIFF_CRYPTO_XOR = 1; - const MS_BIFF_CRYPTO_RC4 = 2; - - // Size of stream blocks when using RC4 encryption - const REKEY_BLOCK = 0x400; - - // should be consistent with Writer\Xls\Style\CellBorder - const BORDER_STYLE_MAP = [ - Border::BORDER_NONE, // => 0x00, - Border::BORDER_THIN, // => 0x01, - Border::BORDER_MEDIUM, // => 0x02, - Border::BORDER_DASHED, // => 0x03, - Border::BORDER_DOTTED, // => 0x04, - Border::BORDER_THICK, // => 0x05, - Border::BORDER_DOUBLE, // => 0x06, - Border::BORDER_HAIR, // => 0x07, - Border::BORDER_MEDIUMDASHED, // => 0x08, - Border::BORDER_DASHDOT, // => 0x09, - Border::BORDER_MEDIUMDASHDOT, // => 0x0A, - Border::BORDER_DASHDOTDOT, // => 0x0B, - Border::BORDER_MEDIUMDASHDOTDOT, // => 0x0C, - Border::BORDER_SLANTDASHDOT, // => 0x0D, - Border::BORDER_OMIT, // => 0x0E, - Border::BORDER_OMIT, // => 0x0F, - ]; - /** * Summary Information stream data. */ - private ?string $summaryInformation = null; + protected ?string $summaryInformation = null; /** * Extended Summary Information stream data. */ - private ?string $documentSummaryInformation = null; + protected ?string $documentSummaryInformation = null; /** * Workbook stream data. (Includes workbook globals substream as well as sheet substreams). */ - private string $data; + protected string $data; /** * Size in bytes of $this->data. */ - private int $dataSize; + protected int $dataSize; /** * Current position in stream. */ - private int $pos; + protected int $pos; /** * Workbook to be returned by the reader. */ - private Spreadsheet $spreadsheet; + protected Spreadsheet $spreadsheet; /** * Worksheet that is currently being built by the reader. */ - private Worksheet $phpSheet; + protected Worksheet $phpSheet; /** * BIFF version. */ - private int $version = 0; - - /** - * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95) - * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'. - */ - private string $codepage = ''; + protected int $version = 0; /** * Shared formats. */ - private array $formats; + protected array $formats; /** * Shared fonts. * * @var Font[] */ - private array $objFonts; + protected array $objFonts; /** * Color palette. */ - private array $palette; + protected array $palette; /** * Worksheets. */ - private array $sheets; + protected array $sheets; /** * External books. */ - private array $externalBooks; + protected array $externalBooks; /** * REF structures. Only applies to BIFF8. */ - private array $ref; + protected array $ref; /** * External names. */ - private array $externalNames; + protected array $externalNames; /** * Defined names. */ - private array $definedname; + protected array $definedname; /** * Shared strings. Only applies to BIFF8. */ - private array $sst; + protected array $sst; /** * Panes are frozen? (in sheet currently being read). See WINDOW2 record. */ - private bool $frozen; + protected bool $frozen; /** * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record. */ - private bool $isFitToPages; + protected bool $isFitToPages; /** * Objects. One OBJ record contributes with one entry. */ - private array $objs; + protected array $objs; /** * Text Objects. One TXO record corresponds with one entry. */ - private array $textObjects; + protected array $textObjects; /** * Cell Annotations (BIFF8). */ - private array $cellNotes; + protected array $cellNotes; /** * The combined MSODRAWINGGROUP data. */ - private string $drawingGroupData; + protected string $drawingGroupData; /** * The combined MSODRAWING data (per sheet). */ - private string $drawingData; + protected string $drawingData; /** * Keep track of XF index. */ - private int $xfIndex; + protected int $xfIndex; /** * Mapping of XF index (that is a cell XF) to final index in cellXf collection. */ - private array $mapCellXfIndex; + protected array $mapCellXfIndex; /** * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection. */ - private array $mapCellStyleXfIndex; + protected array $mapCellStyleXfIndex; /** * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value. */ - private array $sharedFormulas; + protected array $sharedFormulas; /** * The shared formula parts in a sheet. One FORMULA record contributes with one value if it * refers to a shared formula. */ - private array $sharedFormulaParts; + protected array $sharedFormulaParts; /** * The type of encryption in use. */ - private int $encryption = 0; + protected int $encryption = 0; /** * The position in the stream after which contents are encrypted. */ - private int $encryptionStartPos = 0; + protected int $encryptionStartPos = 0; /** * The current RC4 decryption object. */ - private ?Xls\RC4 $rc4Key = null; + protected ?Xls\RC4 $rc4Key = null; /** * The position in the stream that the RC4 decryption object was left at. */ - private int $rc4Pos = 0; + protected int $rc4Pos = 0; /** * The current MD5 context state. @@ -364,104 +236,18 @@ class Xls extends BaseReader */ private string $md5Ctxt; // @phpstan-ignore-line - private int $textObjRef; + protected int $textObjRef; - private string $baseCell; + protected string $baseCell; - private bool $activeSheetSet = false; - - /** - * Create a new Xls Reader instance. - */ - public function __construct() - { - parent::__construct(); - } - - /** - * Can the current IReader read the file? - */ - public function canRead(string $filename): bool - { - if (File::testFileNoThrow($filename) === false) { - return false; - } - - try { - // Use ParseXL for the hard work. - $ole = new OLERead(); - - // get excel data - $ole->read($filename); - if ($ole->wrkbook === null) { - throw new Exception('The filename ' . $filename . ' is not recognised as a Spreadsheet file'); - } - - return true; - } catch (PhpSpreadsheetException) { - return false; - } - } - - public function setCodepage(string $codepage): void - { - if (CodePage::validate($codepage) === false) { - throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage); - } - - $this->codepage = $codepage; - } - - public function getCodepage(): string - { - return $this->codepage; - } + protected bool $activeSheetSet = false; /** * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object. */ public function listWorksheetNames(string $filename): array { - File::assertFile($filename); - - $worksheetNames = []; - - // Read the OLE file - $this->loadOLE($filename); - - // total byte size of Excel data (workbook global substream + sheet substreams) - $this->dataSize = strlen($this->data); - - $this->pos = 0; - $this->sheets = []; - - // Parse Workbook Global Substream - while ($this->pos < $this->dataSize) { - $code = self::getUInt2d($this->data, $this->pos); - - match ($code) { - self::XLS_TYPE_BOF => $this->readBof(), - self::XLS_TYPE_SHEET => $this->readSheet(), - self::XLS_TYPE_EOF => $this->readDefault(), - self::XLS_TYPE_CODEPAGE => $this->readCodepage(), - default => $this->readDefault(), - }; - - if ($code === self::XLS_TYPE_EOF) { - break; - } - } - - foreach ($this->sheets as $sheet) { - if ($sheet['sheetType'] != 0x00) { - // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module - continue; - } - - $worksheetNames[] = $sheet['name']; - } - - return $worksheetNames; + return (new Xls\ListFunctions())->listWorksheetNames2($filename, $this); } /** @@ -469,100 +255,7 @@ class Xls extends BaseReader */ public function listWorksheetInfo(string $filename): array { - File::assertFile($filename); - - $worksheetInfo = []; - - // Read the OLE file - $this->loadOLE($filename); - - // total byte size of Excel data (workbook global substream + sheet substreams) - $this->dataSize = strlen($this->data); - - // initialize - $this->pos = 0; - $this->sheets = []; - - // Parse Workbook Global Substream - while ($this->pos < $this->dataSize) { - $code = self::getUInt2d($this->data, $this->pos); - - match ($code) { - self::XLS_TYPE_BOF => $this->readBof(), - self::XLS_TYPE_SHEET => $this->readSheet(), - self::XLS_TYPE_EOF => $this->readDefault(), - self::XLS_TYPE_CODEPAGE => $this->readCodepage(), - default => $this->readDefault(), - }; - - if ($code === self::XLS_TYPE_EOF) { - break; - } - } - - // Parse the individual sheets - foreach ($this->sheets as $sheet) { - if ($sheet['sheetType'] != 0x00) { - // 0x00: Worksheet - // 0x02: Chart - // 0x06: Visual Basic module - continue; - } - - $tmpInfo = []; - $tmpInfo['worksheetName'] = $sheet['name']; - $tmpInfo['lastColumnLetter'] = 'A'; - $tmpInfo['lastColumnIndex'] = 0; - $tmpInfo['totalRows'] = 0; - $tmpInfo['totalColumns'] = 0; - - $this->pos = $sheet['offset']; - - while ($this->pos <= $this->dataSize - 4) { - $code = self::getUInt2d($this->data, $this->pos); - - switch ($code) { - case self::XLS_TYPE_RK: - case self::XLS_TYPE_LABELSST: - case self::XLS_TYPE_NUMBER: - case self::XLS_TYPE_FORMULA: - case self::XLS_TYPE_BOOLERR: - case self::XLS_TYPE_LABEL: - $length = self::getUInt2d($this->data, $this->pos + 2); - $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); - - // move stream pointer to next record - $this->pos += 4 + $length; - - $rowIndex = self::getUInt2d($recordData, 0) + 1; - $columnIndex = self::getUInt2d($recordData, 2); - - $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); - $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); - - break; - case self::XLS_TYPE_BOF: - $this->readBof(); - - break; - case self::XLS_TYPE_EOF: - $this->readDefault(); - - break 2; - default: - $this->readDefault(); - - break; - } - } - - $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); - $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; - - $worksheetInfo[] = $tmpInfo; - } - - return $worksheetInfo; + return (new Xls\ListFunctions())->listWorksheetInfo2($filename, $this); } /** @@ -570,650 +263,7 @@ class Xls extends BaseReader */ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { - // Read the OLE file - $this->loadOLE($filename); - - // Initialisations - $this->spreadsheet = new Spreadsheet(); - $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet - if (!$this->readDataOnly) { - $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style - $this->spreadsheet->removeCellXfByIndex(0); // remove the default style - } - - // Read the summary information stream (containing meta data) - $this->readSummaryInformation(); - - // Read the Additional document summary information stream (containing application-specific meta data) - $this->readDocumentSummaryInformation(); - - // total byte size of Excel data (workbook global substream + sheet substreams) - $this->dataSize = strlen($this->data); - - // initialize - $this->pos = 0; - $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE; - $this->formats = []; - $this->objFonts = []; - $this->palette = []; - $this->sheets = []; - $this->externalBooks = []; - $this->ref = []; - $this->definedname = []; - $this->sst = []; - $this->drawingGroupData = ''; - $this->xfIndex = 0; - $this->mapCellXfIndex = []; - $this->mapCellStyleXfIndex = []; - - // Parse Workbook Global Substream - while ($this->pos < $this->dataSize) { - $code = self::getUInt2d($this->data, $this->pos); - - match ($code) { - self::XLS_TYPE_BOF => $this->readBof(), - self::XLS_TYPE_FILEPASS => $this->readFilepass(), - self::XLS_TYPE_CODEPAGE => $this->readCodepage(), - self::XLS_TYPE_DATEMODE => $this->readDateMode(), - self::XLS_TYPE_FONT => $this->readFont(), - self::XLS_TYPE_FORMAT => $this->readFormat(), - self::XLS_TYPE_XF => $this->readXf(), - self::XLS_TYPE_XFEXT => $this->readXfExt(), - self::XLS_TYPE_STYLE => $this->readStyle(), - self::XLS_TYPE_PALETTE => $this->readPalette(), - self::XLS_TYPE_SHEET => $this->readSheet(), - self::XLS_TYPE_EXTERNALBOOK => $this->readExternalBook(), - self::XLS_TYPE_EXTERNNAME => $this->readExternName(), - self::XLS_TYPE_EXTERNSHEET => $this->readExternSheet(), - self::XLS_TYPE_DEFINEDNAME => $this->readDefinedName(), - self::XLS_TYPE_MSODRAWINGGROUP => $this->readMsoDrawingGroup(), - self::XLS_TYPE_SST => $this->readSst(), - self::XLS_TYPE_EOF => $this->readDefault(), - default => $this->readDefault(), - }; - - if ($code === self::XLS_TYPE_EOF) { - break; - } - } - - // Resolve indexed colors for font, fill, and border colors - // Cannot be resolved already in XF record, because PALETTE record comes afterwards - if (!$this->readDataOnly) { - foreach ($this->objFonts as $objFont) { - if (isset($objFont->colorIndex)) { - $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version); - $objFont->getColor()->setRGB($color['rgb']); - } - } - - foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) { - // fill start and end color - $fill = $objStyle->getFill(); - - if (isset($fill->startcolorIndex)) { - $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version); - $fill->getStartColor()->setRGB($startColor['rgb']); - } - if (isset($fill->endcolorIndex)) { - $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version); - $fill->getEndColor()->setRGB($endColor['rgb']); - } - - // border colors - $top = $objStyle->getBorders()->getTop(); - $right = $objStyle->getBorders()->getRight(); - $bottom = $objStyle->getBorders()->getBottom(); - $left = $objStyle->getBorders()->getLeft(); - $diagonal = $objStyle->getBorders()->getDiagonal(); - - if (isset($top->colorIndex)) { - $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version); - $top->getColor()->setRGB($borderTopColor['rgb']); - } - if (isset($right->colorIndex)) { - $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version); - $right->getColor()->setRGB($borderRightColor['rgb']); - } - if (isset($bottom->colorIndex)) { - $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version); - $bottom->getColor()->setRGB($borderBottomColor['rgb']); - } - if (isset($left->colorIndex)) { - $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version); - $left->getColor()->setRGB($borderLeftColor['rgb']); - } - if (isset($diagonal->colorIndex)) { - $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version); - $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']); - } - } - } - - // treat MSODRAWINGGROUP records, workbook-level Escher - $escherWorkbook = null; - if (!$this->readDataOnly && $this->drawingGroupData) { - $escher = new Escher(); - $reader = new Xls\Escher($escher); - $escherWorkbook = $reader->load($this->drawingGroupData); - } - - // Parse the individual sheets - $this->activeSheetSet = false; - foreach ($this->sheets as $sheet) { - $selectedCells = ''; - if ($sheet['sheetType'] != 0x00) { - // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module - continue; - } - - // check if sheet should be skipped - if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) { - continue; - } - - // add sheet to PhpSpreadsheet object - $this->phpSheet = $this->spreadsheet->createSheet(); - // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula - // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet - // name in line with the formula, not the reverse - $this->phpSheet->setTitle($sheet['name'], false, false); - $this->phpSheet->setSheetState($sheet['sheetState']); - - $this->pos = $sheet['offset']; - - // Initialize isFitToPages. May change after reading SHEETPR record. - $this->isFitToPages = false; - - // Initialize drawingData - $this->drawingData = ''; - - // Initialize objs - $this->objs = []; - - // Initialize shared formula parts - $this->sharedFormulaParts = []; - - // Initialize shared formulas - $this->sharedFormulas = []; - - // Initialize text objs - $this->textObjects = []; - - // Initialize cell annotations - $this->cellNotes = []; - $this->textObjRef = -1; - - while ($this->pos <= $this->dataSize - 4) { - $code = self::getUInt2d($this->data, $this->pos); - - switch ($code) { - case self::XLS_TYPE_BOF: - $this->readBof(); - - break; - case self::XLS_TYPE_PRINTGRIDLINES: - $this->readPrintGridlines(); - - break; - case self::XLS_TYPE_DEFAULTROWHEIGHT: - $this->readDefaultRowHeight(); - - break; - case self::XLS_TYPE_SHEETPR: - $this->readSheetPr(); - - break; - case self::XLS_TYPE_HORIZONTALPAGEBREAKS: - $this->readHorizontalPageBreaks(); - - break; - case self::XLS_TYPE_VERTICALPAGEBREAKS: - $this->readVerticalPageBreaks(); - - break; - case self::XLS_TYPE_HEADER: - $this->readHeader(); - - break; - case self::XLS_TYPE_FOOTER: - $this->readFooter(); - - break; - case self::XLS_TYPE_HCENTER: - $this->readHcenter(); - - break; - case self::XLS_TYPE_VCENTER: - $this->readVcenter(); - - break; - case self::XLS_TYPE_LEFTMARGIN: - $this->readLeftMargin(); - - break; - case self::XLS_TYPE_RIGHTMARGIN: - $this->readRightMargin(); - - break; - case self::XLS_TYPE_TOPMARGIN: - $this->readTopMargin(); - - break; - case self::XLS_TYPE_BOTTOMMARGIN: - $this->readBottomMargin(); - - break; - case self::XLS_TYPE_PAGESETUP: - $this->readPageSetup(); - - break; - case self::XLS_TYPE_PROTECT: - $this->readProtect(); - - break; - case self::XLS_TYPE_SCENPROTECT: - $this->readScenProtect(); - - break; - case self::XLS_TYPE_OBJECTPROTECT: - $this->readObjectProtect(); - - break; - case self::XLS_TYPE_PASSWORD: - $this->readPassword(); - - break; - case self::XLS_TYPE_DEFCOLWIDTH: - $this->readDefColWidth(); - - break; - case self::XLS_TYPE_COLINFO: - $this->readColInfo(); - - break; - case self::XLS_TYPE_DIMENSION: - $this->readDefault(); - - break; - case self::XLS_TYPE_ROW: - $this->readRow(); - - break; - case self::XLS_TYPE_DBCELL: - $this->readDefault(); - - break; - case self::XLS_TYPE_RK: - $this->readRk(); - - break; - case self::XLS_TYPE_LABELSST: - $this->readLabelSst(); - - break; - case self::XLS_TYPE_MULRK: - $this->readMulRk(); - - break; - case self::XLS_TYPE_NUMBER: - $this->readNumber(); - - break; - case self::XLS_TYPE_FORMULA: - $this->readFormula(); - - break; - case self::XLS_TYPE_SHAREDFMLA: - $this->readSharedFmla(); - - break; - case self::XLS_TYPE_BOOLERR: - $this->readBoolErr(); - - break; - case self::XLS_TYPE_MULBLANK: - $this->readMulBlank(); - - break; - case self::XLS_TYPE_LABEL: - $this->readLabel(); - - break; - case self::XLS_TYPE_BLANK: - $this->readBlank(); - - break; - case self::XLS_TYPE_MSODRAWING: - $this->readMsoDrawing(); - - break; - case self::XLS_TYPE_OBJ: - $this->readObj(); - - break; - case self::XLS_TYPE_WINDOW2: - $this->readWindow2(); - - break; - case self::XLS_TYPE_PAGELAYOUTVIEW: - $this->readPageLayoutView(); - - break; - case self::XLS_TYPE_SCL: - $this->readScl(); - - break; - case self::XLS_TYPE_PANE: - $this->readPane(); - - break; - case self::XLS_TYPE_SELECTION: - $selectedCells = $this->readSelection(); - - break; - case self::XLS_TYPE_MERGEDCELLS: - $this->readMergedCells(); - - break; - case self::XLS_TYPE_HYPERLINK: - $this->readHyperLink(); - - break; - case self::XLS_TYPE_DATAVALIDATIONS: - $this->readDataValidations(); - - break; - case self::XLS_TYPE_DATAVALIDATION: - $this->readDataValidation(); - - break; - case self::XLS_TYPE_CFHEADER: - $cellRangeAddresses = $this->readCFHeader(); - - break; - case self::XLS_TYPE_CFRULE: - $this->readCFRule($cellRangeAddresses ?? []); - - break; - case self::XLS_TYPE_SHEETLAYOUT: - $this->readSheetLayout(); - - break; - case self::XLS_TYPE_SHEETPROTECTION: - $this->readSheetProtection(); - - break; - case self::XLS_TYPE_RANGEPROTECTION: - $this->readRangeProtection(); - - break; - case self::XLS_TYPE_NOTE: - $this->readNote(); - - break; - case self::XLS_TYPE_TXO: - $this->readTextObject(); - - break; - case self::XLS_TYPE_CONTINUE: - $this->readContinue(); - - break; - case self::XLS_TYPE_EOF: - $this->readDefault(); - - break 2; - default: - $this->readDefault(); - - break; - } - } - - // treat MSODRAWING records, sheet-level Escher - if (!$this->readDataOnly && $this->drawingData) { - $escherWorksheet = new Escher(); - $reader = new Xls\Escher($escherWorksheet); - $escherWorksheet = $reader->load($this->drawingData); - - // get all spContainers in one long array, so they can be mapped to OBJ records - /** @var SpContainer[] $allSpContainers */ - $allSpContainers = method_exists($escherWorksheet, 'getDgContainer') ? $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers() : []; - } - - // treat OBJ records - foreach ($this->objs as $n => $obj) { - // the first shape container never has a corresponding OBJ record, hence $n + 1 - if (isset($allSpContainers[$n + 1])) { - $spContainer = $allSpContainers[$n + 1]; - - // we skip all spContainers that are a part of a group shape since we cannot yet handle those - if ($spContainer->getNestingLevel() > 1) { - continue; - } - - // calculate the width and height of the shape - /** @var int $startRow */ - [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates()); - /** @var int $endRow */ - [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates()); - - $startOffsetX = $spContainer->getStartOffsetX(); - $startOffsetY = $spContainer->getStartOffsetY(); - $endOffsetX = $spContainer->getEndOffsetX(); - $endOffsetY = $spContainer->getEndOffsetY(); - - $width = SharedXls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX); - $height = SharedXls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY); - - // calculate offsetX and offsetY of the shape - $offsetX = (int) ($startOffsetX * SharedXls::sizeCol($this->phpSheet, $startColumn) / 1024); - $offsetY = (int) ($startOffsetY * SharedXls::sizeRow($this->phpSheet, $startRow) / 256); - - switch ($obj['otObjType']) { - case 0x19: - // Note - if (isset($this->cellNotes[$obj['idObjID']])) { - //$cellNote = $this->cellNotes[$obj['idObjID']]; - - if (isset($this->textObjects[$obj['idObjID']])) { - $textObject = $this->textObjects[$obj['idObjID']]; - $this->cellNotes[$obj['idObjID']]['objTextData'] = $textObject; - } - } - - break; - case 0x08: - // picture - // get index to BSE entry (1-based) - $BSEindex = $spContainer->getOPT(0x0104); - - // If there is no BSE Index, we will fail here and other fields are not read. - // Fix by checking here. - // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field? - // More likely : a uncompatible picture - if (!$BSEindex) { - continue 2; - } - - if ($escherWorkbook) { - $BSECollection = method_exists($escherWorkbook, 'getDggContainer') ? $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection() : []; - $BSE = $BSECollection[$BSEindex - 1]; - $blipType = $BSE->getBlipType(); - - // need check because some blip types are not supported by Escher reader such as EMF - if ($blip = $BSE->getBlip()) { - $ih = imagecreatefromstring($blip->getData()); - if ($ih !== false) { - $drawing = new MemoryDrawing(); - $drawing->setImageResource($ih); - - // width, height, offsetX, offsetY - $drawing->setResizeProportional(false); - $drawing->setWidth($width); - $drawing->setHeight($height); - $drawing->setOffsetX($offsetX); - $drawing->setOffsetY($offsetY); - - switch ($blipType) { - case BSE::BLIPTYPE_JPEG: - $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG); - $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG); - - break; - case BSE::BLIPTYPE_PNG: - imagealphablending($ih, false); - imagesavealpha($ih, true); - $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG); - $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG); - - break; - } - - $drawing->setWorksheet($this->phpSheet); - $drawing->setCoordinates($spContainer->getStartCoordinates()); - } - } - } - - break; - default: - // other object type - break; - } - } - } - - // treat SHAREDFMLA records - if ($this->version == self::XLS_BIFF8) { - foreach ($this->sharedFormulaParts as $cell => $baseCell) { - /** @var int $row */ - [$column, $row] = Coordinate::coordinateFromString($cell); - if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) { - $formula = $this->getFormulaFromStructure($this->sharedFormulas[$baseCell], $cell); - $this->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA); - } - } - } - - if (!empty($this->cellNotes)) { - foreach ($this->cellNotes as $note => $noteDetails) { - if (!isset($noteDetails['objTextData'])) { - if (isset($this->textObjects[$note])) { - $textObject = $this->textObjects[$note]; - $noteDetails['objTextData'] = $textObject; - } else { - $noteDetails['objTextData']['text'] = ''; - } - } - $cellAddress = str_replace('$', '', $noteDetails['cellRef']); - $this->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($this->parseRichText($noteDetails['objTextData']['text'])); - } - } - if ($selectedCells !== '') { - $this->phpSheet->setSelectedCells($selectedCells); - } - } - if ($this->activeSheetSet === false) { - $this->spreadsheet->setActiveSheetIndex(0); - } - - // add the named ranges (defined names) - foreach ($this->definedname as $definedName) { - if ($definedName['isBuiltInName']) { - switch ($definedName['name']) { - case pack('C', 0x06): - // print area - // in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2 - $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma? - - $extractedRanges = []; - $sheetName = ''; - /** @var non-empty-string $range */ - foreach ($ranges as $range) { - // $range should look like one of these - // Foo!$C$7:$J$66 - // Bar!$A$1:$IV$2 - $explodes = Worksheet::extractSheetTitle($range, true); - $sheetName = trim($explodes[0], "'"); - if (!str_contains($explodes[1], ':')) { - $explodes[1] = $explodes[1] . ':' . $explodes[1]; - } - $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66 - } - if ($docSheet = $this->spreadsheet->getSheetByName($sheetName)) { - $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2 - } - - break; - case pack('C', 0x07): - // print titles (repeating rows) - // Assuming BIFF8, there are 3 cases - // 1. repeating rows - // formula looks like this: Sheet!$A$1:$IV$2 - // rows 1-2 repeat - // 2. repeating columns - // formula looks like this: Sheet!$A$1:$B$65536 - // columns A-B repeat - // 3. both repeating rows and repeating columns - // formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2 - $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma? - foreach ($ranges as $range) { - // $range should look like this one of these - // Sheet!$A$1:$B$65536 - // Sheet!$A$1:$IV$2 - if (str_contains($range, '!')) { - $explodes = Worksheet::extractSheetTitle($range, true); - if ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) { - $extractedRange = $explodes[1]; - $extractedRange = str_replace('$', '', $extractedRange); - - $coordinateStrings = explode(':', $extractedRange); - if (count($coordinateStrings) == 2) { - [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]); - [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]); - - if ($firstColumn == 'A' && $lastColumn == 'IV') { - // then we have repeating rows - $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]); - } elseif ($firstRow == 1 && $lastRow == 65536) { - // then we have repeating columns - $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]); - } - } - } - } - } - - break; - } - } else { - // Extract range - /** @var non-empty-string $formula */ - $formula = $definedName['formula']; - if (str_contains($formula, '!')) { - $explodes = Worksheet::extractSheetTitle($formula, true); - if ( - ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) - || ($docSheet = $this->spreadsheet->getSheetByName(trim($explodes[0], "'"))) - ) { - $extractedRange = $explodes[1]; - - $localOnly = ($definedName['scope'] === 0) ? false : true; - - $scope = ($definedName['scope'] === 0) ? null : $this->spreadsheet->getSheetByName($this->sheets[$definedName['scope'] - 1]['name']); - - $this->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope)); - } - } - // Named Value - // TODO Provide support for named values - } - } - $this->data = ''; - - return $this->spreadsheet; + return (new Xls\LoadSpreadsheet())->loadSpreadsheetFromFile2($filename, $this); } /** @@ -1225,7 +275,7 @@ class Xls extends BaseReader * * @return string Record data */ - private function readRecordData(string $data, int $pos, int $len): string + protected function readRecordData(string $data, int $pos, int $len): string { $data = substr($data, $pos, $len); @@ -1275,7 +325,7 @@ class Xls extends BaseReader /** * Use OLE reader to extract the relevant data streams from the OLE file. */ - private function loadOLE(string $filename): void + protected function loadOLE(string $filename): void { // OLE reader $ole = new OLERead(); @@ -1292,7 +342,7 @@ class Xls extends BaseReader /** * Read summary information. */ - private function readSummaryInformation(): void + protected function readSummaryInformation(): void { if (!isset($this->summaryInformation)) { return; @@ -1439,7 +489,7 @@ class Xls extends BaseReader /** * Read additional document summary information. */ - private function readDocumentSummaryInformation(): void + protected function readDocumentSummaryInformation(): void { if (!isset($this->documentSummaryInformation)) { return; @@ -1577,7 +627,7 @@ class Xls extends BaseReader /** * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record. */ - private function readDefault(): void + protected function readDefault(): void { $length = self::getUInt2d($this->data, $this->pos + 2); @@ -1589,7 +639,7 @@ class Xls extends BaseReader * The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions, * this record stores a note (cell note). This feature was significantly enhanced in Excel 97. */ - private function readNote(): void + protected function readNote(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -1601,7 +651,7 @@ class Xls extends BaseReader return; } - $cellAddress = $this->readBIFF8CellAddress(substr($recordData, 0, 4)); + $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($recordData, 0, 4)); if ($this->version == self::XLS_BIFF8) { $noteObjID = self::getUInt2d($recordData, 6); $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8)); @@ -1643,7 +693,7 @@ class Xls extends BaseReader /** * The TEXT Object record contains the text associated with a cell annotation. */ - private function readTextObject(): void + protected function readTextObject(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -1690,7 +740,7 @@ class Xls extends BaseReader /** * Read BOF. */ - private function readBof(): void + protected function readBof(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = substr($this->data, $this->pos + 4, $length); @@ -1741,7 +791,7 @@ class Xls extends BaseReader * are based on the source of Spreadsheet-ParseExcel: * https://metacpan.org/release/Spreadsheet-ParseExcel */ - private function readFilepass(): void + protected function readFilepass(): void { $length = self::getUInt2d($this->data, $this->pos + 2); @@ -1889,7 +939,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readCodepage(): void + protected function readCodepage(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -1915,7 +965,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readDateMode(): void + protected function readDateMode(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -1935,7 +985,7 @@ class Xls extends BaseReader /** * Read a FONT record. */ - private function readFont(): void + protected function readFont(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2012,7 +1062,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readFormat(): void + protected function readFormat(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2053,7 +1103,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readXf(): void + protected function readXf(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2301,7 +1351,7 @@ class Xls extends BaseReader } } - private function readXfExt(): void + protected function readXfExt(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2477,7 +1527,7 @@ class Xls extends BaseReader /** * Read STYLE record. */ - private function readStyle(): void + protected function readStyle(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2514,7 +1564,7 @@ class Xls extends BaseReader /** * Read PALETTE record. */ - private function readPalette(): void + protected function readPalette(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2546,7 +1596,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readSheet(): void + protected function readSheet(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2590,7 +1640,7 @@ class Xls extends BaseReader /** * Read EXTERNALBOOK record. */ - private function readExternalBook(): void + protected function readExternalBook(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2652,7 +1702,7 @@ class Xls extends BaseReader /** * Read EXTERNNAME record. */ - private function readExternName(): void + protected function readExternName(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2686,7 +1736,7 @@ class Xls extends BaseReader /** * Read EXTERNSHEET record. */ - private function readExternSheet(): void + protected function readExternSheet(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2722,7 +1772,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readDefinedName(): void + protected function readDefinedName(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -2777,7 +1827,7 @@ class Xls extends BaseReader /** * Read MSODRAWINGGROUP record. */ - private function readMsoDrawingGroup(): void + protected function readMsoDrawingGroup(): void { //$length = self::getUInt2d($this->data, $this->pos + 2); @@ -2799,7 +1849,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readSst(): void + protected function readSst(): void { // offset within (spliced) record data $pos = 0; @@ -2995,7 +2045,7 @@ class Xls extends BaseReader /** * Read PRINTGRIDLINES record. */ - private function readPrintGridlines(): void + protected function readPrintGridlines(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3013,7 +2063,7 @@ class Xls extends BaseReader /** * Read DEFAULTROWHEIGHT record. */ - private function readDefaultRowHeight(): void + protected function readDefaultRowHeight(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3030,7 +2080,7 @@ class Xls extends BaseReader /** * Read SHEETPR record. */ - private function readSheetPr(): void + protected function readSheetPr(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3056,7 +2106,7 @@ class Xls extends BaseReader /** * Read HORIZONTALPAGEBREAKS record. */ - private function readHorizontalPageBreaks(): void + protected function readHorizontalPageBreaks(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3083,7 +2133,7 @@ class Xls extends BaseReader /** * Read VERTICALPAGEBREAKS record. */ - private function readVerticalPageBreaks(): void + protected function readVerticalPageBreaks(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3110,7 +2160,7 @@ class Xls extends BaseReader /** * Read HEADER record. */ - private function readHeader(): void + protected function readHeader(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3137,7 +2187,7 @@ class Xls extends BaseReader /** * Read FOOTER record. */ - private function readFooter(): void + protected function readFooter(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3163,7 +2213,7 @@ class Xls extends BaseReader /** * Read HCENTER record. */ - private function readHcenter(): void + protected function readHcenter(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3182,7 +2232,7 @@ class Xls extends BaseReader /** * Read VCENTER record. */ - private function readVcenter(): void + protected function readVcenter(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3201,7 +2251,7 @@ class Xls extends BaseReader /** * Read LEFTMARGIN record. */ - private function readLeftMargin(): void + protected function readLeftMargin(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3218,7 +2268,7 @@ class Xls extends BaseReader /** * Read RIGHTMARGIN record. */ - private function readRightMargin(): void + protected function readRightMargin(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3235,7 +2285,7 @@ class Xls extends BaseReader /** * Read TOPMARGIN record. */ - private function readTopMargin(): void + protected function readTopMargin(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3252,7 +2302,7 @@ class Xls extends BaseReader /** * Read BOTTOMMARGIN record. */ - private function readBottomMargin(): void + protected function readBottomMargin(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3269,7 +2319,7 @@ class Xls extends BaseReader /** * Read PAGESETUP record. */ - private function readPageSetup(): void + protected function readPageSetup(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3327,7 +2377,7 @@ class Xls extends BaseReader * PROTECT - Sheet protection (BIFF2 through BIFF8) * if this record is omitted, then it also means no sheet protection. */ - private function readProtect(): void + protected function readProtect(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3349,7 +2399,7 @@ class Xls extends BaseReader /** * SCENPROTECT. */ - private function readScenProtect(): void + protected function readScenProtect(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3372,7 +2422,7 @@ class Xls extends BaseReader /** * OBJECTPROTECT. */ - private function readObjectProtect(): void + protected function readObjectProtect(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3395,7 +2445,7 @@ class Xls extends BaseReader /** * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8). */ - private function readPassword(): void + protected function readPassword(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3413,7 +2463,7 @@ class Xls extends BaseReader /** * Read DEFCOLWIDTH record. */ - private function readDefColWidth(): void + protected function readDefColWidth(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3431,7 +2481,7 @@ class Xls extends BaseReader /** * Read COLINFO record. */ - private function readColInfo(): void + protected function readColInfo(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3491,7 +2541,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readRow(): void + protected function readRow(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3560,7 +2610,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readRk(): void + protected function readRk(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3604,7 +2654,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readLabelSst(): void + protected function readLabelSst(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3689,7 +2739,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readMulRk(): void + protected function readMulRk(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3742,7 +2792,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readNumber(): void + protected function readNumber(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3783,7 +2833,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readFormula(): void + protected function readFormula(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3915,7 +2965,7 @@ class Xls extends BaseReader * which usually contains relative references. * These will be used to construct the formula in each shared formula part after the sheet is read. */ - private function readSharedFmla(): void + protected function readSharedFmla(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3925,7 +2975,7 @@ class Xls extends BaseReader // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything //$cellRange = substr($recordData, 0, 6); - //$cellRange = $this->readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax + //$cellRange = Xls\Biff5::readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax // offset: 6, size: 1; not used @@ -3946,7 +2996,7 @@ class Xls extends BaseReader * * @return string The string contents as UTF-8 */ - private function readString(): string + protected function readString(): string { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -3973,7 +3023,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readBoolErr(): void + protected function readBoolErr(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4032,7 +3082,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readMulBlank(): void + protected function readMulBlank(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4075,7 +3125,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readLabel(): void + protected function readLabel(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4119,7 +3169,7 @@ class Xls extends BaseReader /** * Read BLANK record. */ - private function readBlank(): void + protected function readBlank(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4149,7 +3199,7 @@ class Xls extends BaseReader /** * Read MSODRAWING record. */ - private function readMsoDrawing(): void + protected function readMsoDrawing(): void { //$length = self::getUInt2d($this->data, $this->pos + 2); @@ -4163,7 +3213,7 @@ class Xls extends BaseReader /** * Read OBJ record. */ - private function readObj(): void + protected function readObj(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4203,7 +3253,7 @@ class Xls extends BaseReader /** * Read WINDOW2 record. */ - private function readWindow2(): void + protected function readWindow2(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4288,7 +3338,7 @@ class Xls extends BaseReader /** * Read PLV Record(Created by Excel2007 or upper). */ - private function readPageLayoutView(): void + protected function readPageLayoutView(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4325,7 +3375,7 @@ class Xls extends BaseReader /** * Read SCL record. */ - private function readScl(): void + protected function readScl(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4346,7 +3396,7 @@ class Xls extends BaseReader /** * Read PANE record. */ - private function readPane(): void + protected function readPane(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4380,7 +3430,7 @@ class Xls extends BaseReader /** * Read SELECTION record. There is one such record for each pane in the sheet. */ - private function readSelection(): string + protected function readSelection(): string { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4405,7 +3455,7 @@ class Xls extends BaseReader // offset: 7; size: var; cell range address list containing all selected cell ranges $data = substr($recordData, 7); - $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax + $cellRangeAddressList = Xls\Biff5::readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0]; @@ -4460,7 +3510,7 @@ class Xls extends BaseReader * -- "OpenOffice.org's Documentation of the Microsoft * Excel File Format" */ - private function readMergedCells(): void + protected function readMergedCells(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4469,7 +3519,7 @@ class Xls extends BaseReader $this->pos += 4 + $length; if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) { - $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($recordData); + $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($recordData); foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) { if ( (str_contains($cellRangeAddress, ':')) @@ -4484,7 +3534,7 @@ class Xls extends BaseReader /** * Read HYPERLINK record. */ - private function readHyperLink(): void + protected function readHyperLink(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4495,7 +3545,7 @@ class Xls extends BaseReader if (!$this->readDataOnly) { // offset: 0; size: 8; cell range address of all cells containing this hyperlink try { - $cellRange = $this->readBIFF8CellRangeAddressFixed($recordData); + $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed($recordData); } catch (PhpSpreadsheetException) { return; } @@ -4658,7 +3708,7 @@ class Xls extends BaseReader /** * Read DATAVALIDATIONS record. */ - private function readDataValidations(): void + protected function readDataValidations(): void { $length = self::getUInt2d($this->data, $this->pos + 2); //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4670,144 +3720,15 @@ class Xls extends BaseReader /** * Read DATAVALIDATION record. */ - private function readDataValidation(): void + protected function readDataValidation(): void { - $length = self::getUInt2d($this->data, $this->pos + 2); - $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); - - // move stream pointer forward to next record - $this->pos += 4 + $length; - - if ($this->readDataOnly) { - return; - } - - // offset: 0; size: 4; Options - $options = self::getInt4d($recordData, 0); - - // bit: 0-3; mask: 0x0000000F; type - $type = (0x0000000F & $options) >> 0; - $type = Xls\DataValidationHelper::type($type); - - // bit: 4-6; mask: 0x00000070; error type - $errorStyle = (0x00000070 & $options) >> 4; - $errorStyle = Xls\DataValidationHelper::errorStyle($errorStyle); - - // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list) - // I have only seen cases where this is 1 - //$explicitFormula = (0x00000080 & $options) >> 7; - - // bit: 8; mask: 0x00000100; 1= empty cells allowed - $allowBlank = (0x00000100 & $options) >> 8; - - // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity - $suppressDropDown = (0x00000200 & $options) >> 9; - - // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected - $showInputMessage = (0x00040000 & $options) >> 18; - - // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered - $showErrorMessage = (0x00080000 & $options) >> 19; - - // bit: 20-23; mask: 0x00F00000; condition operator - $operator = (0x00F00000 & $options) >> 20; - $operator = Xls\DataValidationHelper::operator($operator); - - if ($type === null || $errorStyle === null || $operator === null) { - return; - } - - // offset: 4; size: var; title of the prompt box - $offset = 4; - $string = self::readUnicodeStringLong(substr($recordData, $offset)); - $promptTitle = $string['value'] !== chr(0) ? $string['value'] : ''; - $offset += $string['size']; - - // offset: var; size: var; title of the error box - $string = self::readUnicodeStringLong(substr($recordData, $offset)); - $errorTitle = $string['value'] !== chr(0) ? $string['value'] : ''; - $offset += $string['size']; - - // offset: var; size: var; text of the prompt box - $string = self::readUnicodeStringLong(substr($recordData, $offset)); - $prompt = $string['value'] !== chr(0) ? $string['value'] : ''; - $offset += $string['size']; - - // offset: var; size: var; text of the error box - $string = self::readUnicodeStringLong(substr($recordData, $offset)); - $error = $string['value'] !== chr(0) ? $string['value'] : ''; - $offset += $string['size']; - - // offset: var; size: 2; size of the formula data for the first condition - $sz1 = self::getUInt2d($recordData, $offset); - $offset += 2; - - // offset: var; size: 2; not used - $offset += 2; - - // offset: var; size: $sz1; formula data for first condition (without size field) - $formula1 = substr($recordData, $offset, $sz1); - $formula1 = pack('v', $sz1) . $formula1; // prepend the length - - try { - $formula1 = $this->getFormulaFromStructure($formula1); - - // in list type validity, null characters are used as item separators - if ($type == DataValidation::TYPE_LIST) { - $formula1 = str_replace(chr(0), ',', $formula1); - } - } catch (PhpSpreadsheetException $e) { - return; - } - $offset += $sz1; - - // offset: var; size: 2; size of the formula data for the first condition - $sz2 = self::getUInt2d($recordData, $offset); - $offset += 2; - - // offset: var; size: 2; not used - $offset += 2; - - // offset: var; size: $sz2; formula data for second condition (without size field) - $formula2 = substr($recordData, $offset, $sz2); - $formula2 = pack('v', $sz2) . $formula2; // prepend the length - - try { - $formula2 = $this->getFormulaFromStructure($formula2); - } catch (PhpSpreadsheetException) { - return; - } - $offset += $sz2; - - // offset: var; size: var; cell range address list with - $cellRangeAddressList = $this->readBIFF8CellRangeAddressList(substr($recordData, $offset)); - $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; - - foreach ($cellRangeAddresses as $cellRange) { - $stRange = $this->phpSheet->shrinkRangeToFit($cellRange); - foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) { - $objValidation = $this->phpSheet->getCell($coordinate)->getDataValidation(); - $objValidation->setType($type); - $objValidation->setErrorStyle($errorStyle); - $objValidation->setAllowBlank((bool) $allowBlank); - $objValidation->setShowInputMessage((bool) $showInputMessage); - $objValidation->setShowErrorMessage((bool) $showErrorMessage); - $objValidation->setShowDropDown(!$suppressDropDown); - $objValidation->setOperator($operator); - $objValidation->setErrorTitle($errorTitle); - $objValidation->setError($error); - $objValidation->setPromptTitle($promptTitle); - $objValidation->setPrompt($prompt); - $objValidation->setFormula1($formula1); - $objValidation->setFormula2($formula2); - } - } + (new Xls\DataValidationHelper())->readDataValidation2($this); } /** * Read SHEETLAYOUT record. Stores sheet tab color information. */ - private function readSheetLayout(): void + protected function readSheetLayout(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4842,7 +3763,7 @@ class Xls extends BaseReader /** * Read SHEETPROTECTION record (FEATHEADR). */ - private function readSheetProtection(): void + protected function readSheetProtection(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4946,7 +3867,7 @@ class Xls extends BaseReader * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification, * where it is referred to as FEAT record. */ - private function readRangeProtection(): void + protected function readRangeProtection(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -4980,7 +3901,7 @@ class Xls extends BaseReader $cellRanges = []; for ($i = 0; $i < $cref; ++$i) { try { - $cellRange = $this->readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8)); + $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8)); } catch (PhpSpreadsheetException) { return; } @@ -5008,7 +3929,7 @@ class Xls extends BaseReader * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented. * In this case, we must treat the CONTINUE record as a MSODRAWING record. */ - private function readContinue(): void + protected function readContinue(): void { $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -5094,7 +4015,7 @@ class Xls extends BaseReader * * @return string Human readable formula */ - private function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string + protected function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string { // offset: 0; size: 2; size of the following formula data $sz = self::getUInt2d($formulaStructure, 0); @@ -5269,7 +4190,7 @@ class Xls extends BaseReader break; case 'tArray': // array constant - $constantArray = self::readBIFF8ConstantArray($additionalData); + $constantArray = Xls\Biff8::readBIFF8ConstantArray($additionalData); $formulaStrings[] = $space1 . $space0 . $constantArray['value']; $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data unset($space0, $space1); @@ -5277,7 +4198,7 @@ class Xls extends BaseReader break; case 'tMemArea': // bite off chunk of additional data - $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($additionalData); + $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($additionalData); $additionalData = substr($additionalData, $cellRangeAddressList['size']); $formulaStrings[] = "$space1$space0{$token['data']}"; unset($space0, $space1); @@ -5557,811 +4478,11 @@ class Xls extends BaseReader $name = 'tFunc'; $size = 3; // offset: 1; size: 2; index to built-in sheet function - switch (self::getUInt2d($formulaData, 1)) { - case 2: - $function = 'ISNA'; - $args = 1; - - break; - case 3: - $function = 'ISERROR'; - $args = 1; - - break; - case 10: - $function = 'NA'; - $args = 0; - - break; - case 15: - $function = 'SIN'; - $args = 1; - - break; - case 16: - $function = 'COS'; - $args = 1; - - break; - case 17: - $function = 'TAN'; - $args = 1; - - break; - case 18: - $function = 'ATAN'; - $args = 1; - - break; - case 19: - $function = 'PI'; - $args = 0; - - break; - case 20: - $function = 'SQRT'; - $args = 1; - - break; - case 21: - $function = 'EXP'; - $args = 1; - - break; - case 22: - $function = 'LN'; - $args = 1; - - break; - case 23: - $function = 'LOG10'; - $args = 1; - - break; - case 24: - $function = 'ABS'; - $args = 1; - - break; - case 25: - $function = 'INT'; - $args = 1; - - break; - case 26: - $function = 'SIGN'; - $args = 1; - - break; - case 27: - $function = 'ROUND'; - $args = 2; - - break; - case 30: - $function = 'REPT'; - $args = 2; - - break; - case 31: - $function = 'MID'; - $args = 3; - - break; - case 32: - $function = 'LEN'; - $args = 1; - - break; - case 33: - $function = 'VALUE'; - $args = 1; - - break; - case 34: - $function = 'TRUE'; - $args = 0; - - break; - case 35: - $function = 'FALSE'; - $args = 0; - - break; - case 38: - $function = 'NOT'; - $args = 1; - - break; - case 39: - $function = 'MOD'; - $args = 2; - - break; - case 40: - $function = 'DCOUNT'; - $args = 3; - - break; - case 41: - $function = 'DSUM'; - $args = 3; - - break; - case 42: - $function = 'DAVERAGE'; - $args = 3; - - break; - case 43: - $function = 'DMIN'; - $args = 3; - - break; - case 44: - $function = 'DMAX'; - $args = 3; - - break; - case 45: - $function = 'DSTDEV'; - $args = 3; - - break; - case 48: - $function = 'TEXT'; - $args = 2; - - break; - case 61: - $function = 'MIRR'; - $args = 3; - - break; - case 63: - $function = 'RAND'; - $args = 0; - - break; - case 65: - $function = 'DATE'; - $args = 3; - - break; - case 66: - $function = 'TIME'; - $args = 3; - - break; - case 67: - $function = 'DAY'; - $args = 1; - - break; - case 68: - $function = 'MONTH'; - $args = 1; - - break; - case 69: - $function = 'YEAR'; - $args = 1; - - break; - case 71: - $function = 'HOUR'; - $args = 1; - - break; - case 72: - $function = 'MINUTE'; - $args = 1; - - break; - case 73: - $function = 'SECOND'; - $args = 1; - - break; - case 74: - $function = 'NOW'; - $args = 0; - - break; - case 75: - $function = 'AREAS'; - $args = 1; - - break; - case 76: - $function = 'ROWS'; - $args = 1; - - break; - case 77: - $function = 'COLUMNS'; - $args = 1; - - break; - case 83: - $function = 'TRANSPOSE'; - $args = 1; - - break; - case 86: - $function = 'TYPE'; - $args = 1; - - break; - case 97: - $function = 'ATAN2'; - $args = 2; - - break; - case 98: - $function = 'ASIN'; - $args = 1; - - break; - case 99: - $function = 'ACOS'; - $args = 1; - - break; - case 105: - $function = 'ISREF'; - $args = 1; - - break; - case 111: - $function = 'CHAR'; - $args = 1; - - break; - case 112: - $function = 'LOWER'; - $args = 1; - - break; - case 113: - $function = 'UPPER'; - $args = 1; - - break; - case 114: - $function = 'PROPER'; - $args = 1; - - break; - case 117: - $function = 'EXACT'; - $args = 2; - - break; - case 118: - $function = 'TRIM'; - $args = 1; - - break; - case 119: - $function = 'REPLACE'; - $args = 4; - - break; - case 121: - $function = 'CODE'; - $args = 1; - - break; - case 126: - $function = 'ISERR'; - $args = 1; - - break; - case 127: - $function = 'ISTEXT'; - $args = 1; - - break; - case 128: - $function = 'ISNUMBER'; - $args = 1; - - break; - case 129: - $function = 'ISBLANK'; - $args = 1; - - break; - case 130: - $function = 'T'; - $args = 1; - - break; - case 131: - $function = 'N'; - $args = 1; - - break; - case 140: - $function = 'DATEVALUE'; - $args = 1; - - break; - case 141: - $function = 'TIMEVALUE'; - $args = 1; - - break; - case 142: - $function = 'SLN'; - $args = 3; - - break; - case 143: - $function = 'SYD'; - $args = 4; - - break; - case 162: - $function = 'CLEAN'; - $args = 1; - - break; - case 163: - $function = 'MDETERM'; - $args = 1; - - break; - case 164: - $function = 'MINVERSE'; - $args = 1; - - break; - case 165: - $function = 'MMULT'; - $args = 2; - - break; - case 184: - $function = 'FACT'; - $args = 1; - - break; - case 189: - $function = 'DPRODUCT'; - $args = 3; - - break; - case 190: - $function = 'ISNONTEXT'; - $args = 1; - - break; - case 195: - $function = 'DSTDEVP'; - $args = 3; - - break; - case 196: - $function = 'DVARP'; - $args = 3; - - break; - case 198: - $function = 'ISLOGICAL'; - $args = 1; - - break; - case 199: - $function = 'DCOUNTA'; - $args = 3; - - break; - case 207: - $function = 'REPLACEB'; - $args = 4; - - break; - case 210: - $function = 'MIDB'; - $args = 3; - - break; - case 211: - $function = 'LENB'; - $args = 1; - - break; - case 212: - $function = 'ROUNDUP'; - $args = 2; - - break; - case 213: - $function = 'ROUNDDOWN'; - $args = 2; - - break; - case 214: - $function = 'ASC'; - $args = 1; - - break; - case 215: - $function = 'DBCS'; - $args = 1; - - break; - case 221: - $function = 'TODAY'; - $args = 0; - - break; - case 229: - $function = 'SINH'; - $args = 1; - - break; - case 230: - $function = 'COSH'; - $args = 1; - - break; - case 231: - $function = 'TANH'; - $args = 1; - - break; - case 232: - $function = 'ASINH'; - $args = 1; - - break; - case 233: - $function = 'ACOSH'; - $args = 1; - - break; - case 234: - $function = 'ATANH'; - $args = 1; - - break; - case 235: - $function = 'DGET'; - $args = 3; - - break; - case 244: - $function = 'INFO'; - $args = 1; - - break; - case 252: - $function = 'FREQUENCY'; - $args = 2; - - break; - case 261: - $function = 'ERROR.TYPE'; - $args = 1; - - break; - case 271: - $function = 'GAMMALN'; - $args = 1; - - break; - case 273: - $function = 'BINOMDIST'; - $args = 4; - - break; - case 274: - $function = 'CHIDIST'; - $args = 2; - - break; - case 275: - $function = 'CHIINV'; - $args = 2; - - break; - case 276: - $function = 'COMBIN'; - $args = 2; - - break; - case 277: - $function = 'CONFIDENCE'; - $args = 3; - - break; - case 278: - $function = 'CRITBINOM'; - $args = 3; - - break; - case 279: - $function = 'EVEN'; - $args = 1; - - break; - case 280: - $function = 'EXPONDIST'; - $args = 3; - - break; - case 281: - $function = 'FDIST'; - $args = 3; - - break; - case 282: - $function = 'FINV'; - $args = 3; - - break; - case 283: - $function = 'FISHER'; - $args = 1; - - break; - case 284: - $function = 'FISHERINV'; - $args = 1; - - break; - case 285: - $function = 'FLOOR'; - $args = 2; - - break; - case 286: - $function = 'GAMMADIST'; - $args = 4; - - break; - case 287: - $function = 'GAMMAINV'; - $args = 3; - - break; - case 288: - $function = 'CEILING'; - $args = 2; - - break; - case 289: - $function = 'HYPGEOMDIST'; - $args = 4; - - break; - case 290: - $function = 'LOGNORMDIST'; - $args = 3; - - break; - case 291: - $function = 'LOGINV'; - $args = 3; - - break; - case 292: - $function = 'NEGBINOMDIST'; - $args = 3; - - break; - case 293: - $function = 'NORMDIST'; - $args = 4; - - break; - case 294: - $function = 'NORMSDIST'; - $args = 1; - - break; - case 295: - $function = 'NORMINV'; - $args = 3; - - break; - case 296: - $function = 'NORMSINV'; - $args = 1; - - break; - case 297: - $function = 'STANDARDIZE'; - $args = 3; - - break; - case 298: - $function = 'ODD'; - $args = 1; - - break; - case 299: - $function = 'PERMUT'; - $args = 2; - - break; - case 300: - $function = 'POISSON'; - $args = 3; - - break; - case 301: - $function = 'TDIST'; - $args = 3; - - break; - case 302: - $function = 'WEIBULL'; - $args = 4; - - break; - case 303: - $function = 'SUMXMY2'; - $args = 2; - - break; - case 304: - $function = 'SUMX2MY2'; - $args = 2; - - break; - case 305: - $function = 'SUMX2PY2'; - $args = 2; - - break; - case 306: - $function = 'CHITEST'; - $args = 2; - - break; - case 307: - $function = 'CORREL'; - $args = 2; - - break; - case 308: - $function = 'COVAR'; - $args = 2; - - break; - case 309: - $function = 'FORECAST'; - $args = 3; - - break; - case 310: - $function = 'FTEST'; - $args = 2; - - break; - case 311: - $function = 'INTERCEPT'; - $args = 2; - - break; - case 312: - $function = 'PEARSON'; - $args = 2; - - break; - case 313: - $function = 'RSQ'; - $args = 2; - - break; - case 314: - $function = 'STEYX'; - $args = 2; - - break; - case 315: - $function = 'SLOPE'; - $args = 2; - - break; - case 316: - $function = 'TTEST'; - $args = 4; - - break; - case 325: - $function = 'LARGE'; - $args = 2; - - break; - case 326: - $function = 'SMALL'; - $args = 2; - - break; - case 327: - $function = 'QUARTILE'; - $args = 2; - - break; - case 328: - $function = 'PERCENTILE'; - $args = 2; - - break; - case 331: - $function = 'TRIMMEAN'; - $args = 2; - - break; - case 332: - $function = 'TINV'; - $args = 2; - - break; - case 337: - $function = 'POWER'; - $args = 2; - - break; - case 342: - $function = 'RADIANS'; - $args = 1; - - break; - case 343: - $function = 'DEGREES'; - $args = 1; - - break; - case 346: - $function = 'COUNTIF'; - $args = 2; - - break; - case 347: - $function = 'COUNTBLANK'; - $args = 1; - - break; - case 350: - $function = 'ISPMT'; - $args = 4; - - break; - case 351: - $function = 'DATEDIF'; - $args = 3; - - break; - case 352: - $function = 'DATESTRING'; - $args = 1; - - break; - case 353: - $function = 'NUMBERSTRING'; - $args = 2; - - break; - case 360: - $function = 'PHONETIC'; - $args = 1; - - break; - case 368: - $function = 'BAHTTEXT'; - $args = 1; - - break; - default: - throw new Exception('Unrecognized function in formula'); + $mapping = Xls\Mappings::TFUNC_MAPPINGS[self::getUInt2d($formulaData, 1)] ?? null; + if ($mapping === null) { + throw new Exception('Unrecognized function in formula'); } - $data = ['function' => $function, 'args' => $args]; + $data = ['function' => $mapping[0], 'args' => $mapping[1]]; break; case 0x22: // function with variable number of arguments @@ -6373,97 +4494,10 @@ class Xls extends BaseReader $args = ord($formulaData[1]); // offset: 2: size: 2; index to built-in sheet function $index = self::getUInt2d($formulaData, 2); - $function = match ($index) { - 0 => 'COUNT', - 1 => 'IF', - 4 => 'SUM', - 5 => 'AVERAGE', - 6 => 'MIN', - 7 => 'MAX', - 8 => 'ROW', - 9 => 'COLUMN', - 11 => 'NPV', - 12 => 'STDEV', - 13 => 'DOLLAR', - 14 => 'FIXED', - 28 => 'LOOKUP', - 29 => 'INDEX', - 36 => 'AND', - 37 => 'OR', - 46 => 'VAR', - 49 => 'LINEST', - 50 => 'TREND', - 51 => 'LOGEST', - 52 => 'GROWTH', - 56 => 'PV', - 57 => 'FV', - 58 => 'NPER', - 59 => 'PMT', - 60 => 'RATE', - 62 => 'IRR', - 64 => 'MATCH', - 70 => 'WEEKDAY', - 78 => 'OFFSET', - 82 => 'SEARCH', - 100 => 'CHOOSE', - 101 => 'HLOOKUP', - 102 => 'VLOOKUP', - 109 => 'LOG', - 115 => 'LEFT', - 116 => 'RIGHT', - 120 => 'SUBSTITUTE', - 124 => 'FIND', - 125 => 'CELL', - 144 => 'DDB', - 148 => 'INDIRECT', - 167 => 'IPMT', - 168 => 'PPMT', - 169 => 'COUNTA', - 183 => 'PRODUCT', - 193 => 'STDEVP', - 194 => 'VARP', - 197 => 'TRUNC', - 204 => 'USDOLLAR', - 205 => 'FINDB', - 206 => 'SEARCHB', - 208 => 'LEFTB', - 209 => 'RIGHTB', - 216 => 'RANK', - 219 => 'ADDRESS', - 220 => 'DAYS360', - 222 => 'VDB', - 227 => 'MEDIAN', - 228 => 'SUMPRODUCT', - 247 => 'DB', - 255 => '', - 269 => 'AVEDEV', - 270 => 'BETADIST', - 272 => 'BETAINV', - 317 => 'PROB', - 318 => 'DEVSQ', - 319 => 'GEOMEAN', - 320 => 'HARMEAN', - 321 => 'SUMSQ', - 322 => 'KURT', - 323 => 'SKEW', - 324 => 'ZTEST', - 329 => 'PERCENTRANK', - 330 => 'MODE', - 336 => 'CONCATENATE', - 344 => 'SUBTOTAL', - 345 => 'SUMIF', - 354 => 'ROMAN', - 358 => 'GETPIVOTDATA', - 359 => 'HYPERLINK', - 361 => 'AVERAGEA', - 362 => 'MAXA', - 363 => 'MINA', - 364 => 'STDEVPA', - 365 => 'VARPA', - 366 => 'STDEVA', - 367 => 'VARA', - default => throw new Exception('Unrecognized function in formula'), - }; + $function = Xls\Mappings::TFUNCV_MAPPINGS[$index] ?? null; + if ($function === null) { + throw new Exception('Unrecognized function in formula'); + } $data = ['function' => $function, 'args' => $args]; break; @@ -6483,7 +4517,7 @@ class Xls extends BaseReader case 0x64: $name = 'tRef'; $size = 5; - $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4)); + $data = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 1, 4)); break; case 0x25: // cell range reference to cells in the same sheet (2d) @@ -6491,7 +4525,7 @@ class Xls extends BaseReader case 0x65: $name = 'tArea'; $size = 9; - $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8)); + $data = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 1, 8)); break; case 0x26: // Constant reference sub-expression @@ -6531,7 +4565,7 @@ class Xls extends BaseReader case 0x6C: $name = 'tRefN'; $size = 5; - $data = $this->readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell); + $data = Xls\Biff8::readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell); break; case 0x2D: // Relative 2d range reference @@ -6539,7 +4573,7 @@ class Xls extends BaseReader case 0x6D: $name = 'tAreaN'; $size = 9; - $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell); + $data = Xls\Biff8::readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell); break; case 0x39: // External name @@ -6565,7 +4599,7 @@ class Xls extends BaseReader // offset: 1; size: 2; index to REF entry $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1)); // offset: 3; size: 4; cell address - $cellAddress = $this->readBIFF8CellAddress(substr($formulaData, 3, 4)); + $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 3, 4)); $data = "$sheetRange!$cellAddress"; } catch (PhpSpreadsheetException) { @@ -6584,7 +4618,7 @@ class Xls extends BaseReader // offset: 1; size: 2; index to REF entry $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1)); // offset: 3; size: 8; cell address - $cellRangeAddress = $this->readBIFF8CellRangeAddress(substr($formulaData, 3, 8)); + $cellRangeAddress = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 3, 8)); $data = "$sheetRange!$cellRangeAddress"; } catch (PhpSpreadsheetException) { @@ -6606,337 +4640,13 @@ class Xls extends BaseReader ]; } - /** - * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2' - * section 3.3.4. - */ - private function readBIFF8CellAddress(string $cellAddressStructure): string - { - // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767)) - $row = self::getUInt2d($cellAddressStructure, 0) + 1; - - // offset: 2; size: 2; index to column or column offset + relative flags - // bit: 7-0; mask 0x00FF; column index - $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1); - - // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) - if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) { - $column = '$' . $column; - } - // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) - if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) { - $row = '$' . $row; - } - - return $column . $row; - } - - /** - * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column - * to indicate offsets from a base cell - * section 3.3.4. - * - * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas - */ - private function readBIFF8CellAddressB(string $cellAddressStructure, string $baseCell = 'A1'): string - { - [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell); - $baseCol = Coordinate::columnIndexFromString($baseCol) - 1; - $baseRow = (int) $baseRow; - - // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767)) - $rowIndex = self::getUInt2d($cellAddressStructure, 0); - $row = self::getUInt2d($cellAddressStructure, 0) + 1; - - // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) - if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) { - // offset: 2; size: 2; index to column or column offset + relative flags - // bit: 7-0; mask 0x00FF; column index - $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2); - - $column = Coordinate::stringFromColumnIndex($colIndex + 1); - $column = '$' . $column; - } else { - // offset: 2; size: 2; index to column or column offset + relative flags - // bit: 7-0; mask 0x00FF; column index - $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2); - $colIndex = $baseCol + $relativeColIndex; - $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256; - $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256; - $column = Coordinate::stringFromColumnIndex($colIndex + 1); - } - - // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) - if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) { - $row = '$' . $row; - } else { - $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536; - $row = $baseRow + $rowIndex; - } - - return $column . $row; - } - - /** - * Reads a cell range address in BIFF5 e.g. 'A2:B6' or 'A1' - * always fixed range - * section 2.5.14. - */ - private function readBIFF5CellRangeAddressFixed(string $subData): string - { - // offset: 0; size: 2; index to first row - $fr = self::getUInt2d($subData, 0) + 1; - - // offset: 2; size: 2; index to last row - $lr = self::getUInt2d($subData, 2) + 1; - - // offset: 4; size: 1; index to first column - $fc = ord($subData[4]); - - // offset: 5; size: 1; index to last column - $lc = ord($subData[5]); - - // check values - if ($fr > $lr || $fc > $lc) { - throw new Exception('Not a cell range address'); - } - - // column index to letter - $fc = Coordinate::stringFromColumnIndex($fc + 1); - $lc = Coordinate::stringFromColumnIndex($lc + 1); - - if ($fr == $lr && $fc == $lc) { - return "$fc$fr"; - } - - return "$fc$fr:$lc$lr"; - } - - /** - * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1' - * always fixed range - * section 2.5.14. - */ - private function readBIFF8CellRangeAddressFixed(string $subData): string - { - // offset: 0; size: 2; index to first row - $fr = self::getUInt2d($subData, 0) + 1; - - // offset: 2; size: 2; index to last row - $lr = self::getUInt2d($subData, 2) + 1; - - // offset: 4; size: 2; index to first column - $fc = self::getUInt2d($subData, 4); - - // offset: 6; size: 2; index to last column - $lc = self::getUInt2d($subData, 6); - - // check values - if ($fr > $lr || $fc > $lc) { - throw new Exception('Not a cell range address'); - } - - // column index to letter - $fc = Coordinate::stringFromColumnIndex($fc + 1); - $lc = Coordinate::stringFromColumnIndex($lc + 1); - - if ($fr == $lr && $fc == $lc) { - return "$fc$fr"; - } - - return "$fc$fr:$lc$lr"; - } - - /** - * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6' - * there are flags indicating whether column/row index is relative - * section 3.3.4. - */ - private function readBIFF8CellRangeAddress(string $subData): string - { - // todo: if cell range is just a single cell, should this funciton - // not just return e.g. 'A1' and not 'A1:A1' ? - - // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767)) - $fr = self::getUInt2d($subData, 0) + 1; - - // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767)) - $lr = self::getUInt2d($subData, 2) + 1; - - // offset: 4; size: 2; index to first column or column offset + relative flags - - // bit: 7-0; mask 0x00FF; column index - $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1); - - // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) - if (!(0x4000 & self::getUInt2d($subData, 4))) { - $fc = '$' . $fc; - } - - // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) - if (!(0x8000 & self::getUInt2d($subData, 4))) { - $fr = '$' . $fr; - } - - // offset: 6; size: 2; index to last column or column offset + relative flags - - // bit: 7-0; mask 0x00FF; column index - $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1); - - // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) - if (!(0x4000 & self::getUInt2d($subData, 6))) { - $lc = '$' . $lc; - } - - // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) - if (!(0x8000 & self::getUInt2d($subData, 6))) { - $lr = '$' . $lr; - } - - return "$fc$fr:$lc$lr"; - } - - /** - * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column - * to indicate offsets from a base cell - * section 3.3.4. - * - * @param string $baseCell Base cell - * - * @return string Cell range address - */ - private function readBIFF8CellRangeAddressB(string $subData, string $baseCell = 'A1'): string - { - [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell); - $baseCol = $baseCol - 1; - - // TODO: if cell range is just a single cell, should this funciton - // not just return e.g. 'A1' and not 'A1:A1' ? - - // offset: 0; size: 2; first row - $frIndex = self::getUInt2d($subData, 0); // adjust below - - // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767) - $lrIndex = self::getUInt2d($subData, 2); // adjust below - - // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) - if (!(0x4000 & self::getUInt2d($subData, 4))) { - // absolute column index - // offset: 4; size: 2; first column with relative/absolute flags - // bit: 7-0; mask 0x00FF; column index - $fcIndex = 0x00FF & self::getUInt2d($subData, 4); - $fc = Coordinate::stringFromColumnIndex($fcIndex + 1); - $fc = '$' . $fc; - } else { - // column offset - // offset: 4; size: 2; first column with relative/absolute flags - // bit: 7-0; mask 0x00FF; column index - $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4); - $fcIndex = $baseCol + $relativeFcIndex; - $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256; - $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256; - $fc = Coordinate::stringFromColumnIndex($fcIndex + 1); - } - - // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) - if (!(0x8000 & self::getUInt2d($subData, 4))) { - // absolute row index - $fr = $frIndex + 1; - $fr = '$' . $fr; - } else { - // row offset - $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536; - $fr = $baseRow + $frIndex; - } - - // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) - if (!(0x4000 & self::getUInt2d($subData, 6))) { - // absolute column index - // offset: 6; size: 2; last column with relative/absolute flags - // bit: 7-0; mask 0x00FF; column index - $lcIndex = 0x00FF & self::getUInt2d($subData, 6); - $lc = Coordinate::stringFromColumnIndex($lcIndex + 1); - $lc = '$' . $lc; - } else { - // column offset - // offset: 6; size: 2; last column with relative/absolute flags - // bit: 7-0; mask 0x00FF; column index - $relativeLcIndex = 0x00FF & self::getInt2d($subData, 6); - $lcIndex = $baseCol + $relativeLcIndex; - $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256; - $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256; - $lc = Coordinate::stringFromColumnIndex($lcIndex + 1); - } - - // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) - if (!(0x8000 & self::getUInt2d($subData, 6))) { - // absolute row index - $lr = $lrIndex + 1; - $lr = '$' . $lr; - } else { - // row offset - $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536; - $lr = $baseRow + $lrIndex; - } - - return "$fc$fr:$lc$lr"; - } - - /** - * Read BIFF8 cell range address list - * section 2.5.15. - */ - private function readBIFF8CellRangeAddressList(string $subData): array - { - $cellRangeAddresses = []; - - // offset: 0; size: 2; number of the following cell range addresses - $nm = self::getUInt2d($subData, 0); - - $offset = 2; - // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses - for ($i = 0; $i < $nm; ++$i) { - $cellRangeAddresses[] = $this->readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8)); - $offset += 8; - } - - return [ - 'size' => 2 + 8 * $nm, - 'cellRangeAddresses' => $cellRangeAddresses, - ]; - } - - /** - * Read BIFF5 cell range address list - * section 2.5.15. - */ - private function readBIFF5CellRangeAddressList(string $subData): array - { - $cellRangeAddresses = []; - - // offset: 0; size: 2; number of the following cell range addresses - $nm = self::getUInt2d($subData, 0); - - $offset = 2; - // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses - for ($i = 0; $i < $nm; ++$i) { - $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6)); - $offset += 6; - } - - return [ - 'size' => 2 + 6 * $nm, - 'cellRangeAddresses' => $cellRangeAddresses, - ]; - } - /** * Get a sheet range like Sheet1:Sheet3 from REF index * Note: If there is only one sheet in the range, one gets e.g Sheet1 * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets, * in which case an Exception is thrown. */ - private function readSheetRangeByRefIndex(int $index): string|false + protected function readSheetRangeByRefIndex(int $index): string|false { if (isset($this->ref[$index])) { $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type']; @@ -6980,124 +4690,11 @@ class Xls extends BaseReader return false; } - /** - * read BIFF8 constant value array from array data - * returns e.g. ['value' => '{1,2;3,4}', 'size' => 40] - * section 2.5.8. - */ - private static function readBIFF8ConstantArray(string $arrayData): array - { - // offset: 0; size: 1; number of columns decreased by 1 - $nc = ord($arrayData[0]); - - // offset: 1; size: 2; number of rows decreased by 1 - $nr = self::getUInt2d($arrayData, 1); - $size = 3; // initialize - $arrayData = substr($arrayData, 3); - - // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values - $matrixChunks = []; - for ($r = 1; $r <= $nr + 1; ++$r) { - $items = []; - for ($c = 1; $c <= $nc + 1; ++$c) { - $constant = self::readBIFF8Constant($arrayData); - $items[] = $constant['value']; - $arrayData = substr($arrayData, $constant['size']); - $size += $constant['size']; - } - $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"' - } - $matrix = '{' . implode(';', $matrixChunks) . '}'; - - return [ - 'value' => $matrix, - 'size' => $size, - ]; - } - - /** - * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value' - * section 2.5.7 - * returns e.g. ['value' => '5', 'size' => 9]. - */ - private static function readBIFF8Constant(string $valueData): array - { - // offset: 0; size: 1; identifier for type of constant - $identifier = ord($valueData[0]); - - switch ($identifier) { - case 0x00: // empty constant (what is this?) - $value = ''; - $size = 9; - - break; - case 0x01: // number - // offset: 1; size: 8; IEEE 754 floating-point value - $value = self::extractNumber(substr($valueData, 1, 8)); - $size = 9; - - break; - case 0x02: // string value - // offset: 1; size: var; Unicode string, 16-bit string length - $string = self::readUnicodeStringLong(substr($valueData, 1)); - $value = '"' . $string['value'] . '"'; - $size = 1 + $string['size']; - - break; - case 0x04: // boolean - // offset: 1; size: 1; 0 = FALSE, 1 = TRUE - if (ord($valueData[1])) { - $value = 'TRUE'; - } else { - $value = 'FALSE'; - } - $size = 9; - - break; - case 0x10: // error code - // offset: 1; size: 1; error code - $value = Xls\ErrorCode::lookup(ord($valueData[1])); - $size = 9; - - break; - default: - throw new PhpSpreadsheetException('Unsupported BIFF8 constant'); - } - - return [ - 'value' => $value, - 'size' => $size, - ]; - } - - /** - * Extract RGB color - * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4. - * - * @param string $rgb Encoded RGB value (4 bytes) - */ - private static function readRGB(string $rgb): array - { - // offset: 0; size 1; Red component - $r = ord($rgb[0]); - - // offset: 1; size: 1; Green component - $g = ord($rgb[1]); - - // offset: 2; size: 1; Blue component - $b = ord($rgb[2]); - - // HEX notation, e.g. 'FF00FC' - $rgb = sprintf('%02X%02X%02X', $r, $g, $b); - - return ['rgb' => $rgb]; - } - /** * Read byte string (8-bit string length) * OpenOffice documentation: 2.5.2. */ - private function readByteStringShort(string $subData): array + protected function readByteStringShort(string $subData): array { // offset: 0; size: 1; length of the string (character count) $ln = ord($subData[0]); @@ -7115,7 +4712,7 @@ class Xls extends BaseReader * Read byte string (16-bit string length) * OpenOffice documentation: 2.5.2. */ - private function readByteStringLong(string $subData): array + protected function readByteStringLong(string $subData): array { // offset: 0; size: 2; length of the string (character count) $ln = self::getUInt2d($subData, 0); @@ -7130,207 +4727,7 @@ class Xls extends BaseReader ]; } - /** - * Extracts an Excel Unicode short string (8-bit string length) - * OpenOffice documentation: 2.5.3 - * function will automatically find out where the Unicode string ends. - */ - private static function readUnicodeStringShort(string $subData): array - { - // offset: 0: size: 1; length of the string (character count) - $characterCount = ord($subData[0]); - - $string = self::readUnicodeString(substr($subData, 1), $characterCount); - - // add 1 for the string length - ++$string['size']; - - return $string; - } - - /** - * Extracts an Excel Unicode long string (16-bit string length) - * OpenOffice documentation: 2.5.3 - * this function is under construction, needs to support rich text, and Asian phonetic settings. - */ - private static function readUnicodeStringLong(string $subData): array - { - // offset: 0: size: 2; length of the string (character count) - $characterCount = self::getUInt2d($subData, 0); - - $string = self::readUnicodeString(substr($subData, 2), $characterCount); - - // add 2 for the string length - $string['size'] += 2; - - return $string; - } - - /** - * Read Unicode string with no string length field, but with known character count - * this function is under construction, needs to support rich text, and Asian phonetic settings - * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3. - */ - private static function readUnicodeString(string $subData, int $characterCount): array - { - // offset: 0: size: 1; option flags - // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit) - $isCompressed = !((0x01 & ord($subData[0])) >> 0); - - // bit: 2; mask: 0x04; Asian phonetic settings - //$hasAsian = (0x04) & ord($subData[0]) >> 2; - - // bit: 3; mask: 0x08; Rich-Text settings - //$hasRichText = (0x08) & ord($subData[0]) >> 3; - - // offset: 1: size: var; character array - // this offset assumes richtext and Asian phonetic settings are off which is generally wrong - // needs to be fixed - $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed); - - return [ - 'value' => $value, - 'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags - ]; - } - - /** - * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas. - * Example: hello"world --> "hello""world". - * - * @param string $value UTF-8 encoded string - */ - private static function UTF8toExcelDoubleQuoted(string $value): string - { - return '"' . str_replace('"', '""', $value) . '"'; - } - - /** - * Reads first 8 bytes of a string and return IEEE 754 float. - * - * @param string $data Binary string that is at least 8 bytes long - */ - private static function extractNumber(string $data): int|float - { - $rknumhigh = self::getInt4d($data, 4); - $rknumlow = self::getInt4d($data, 0); - $sign = ($rknumhigh & self::HIGH_ORDER_BIT) >> 31; - $exp = (($rknumhigh & 0x7FF00000) >> 20) - 1023; - $mantissa = (0x100000 | ($rknumhigh & 0x000FFFFF)); - $mantissalow1 = ($rknumlow & self::HIGH_ORDER_BIT) >> 31; - $mantissalow2 = ($rknumlow & 0x7FFFFFFF); - $value = $mantissa / 2 ** (20 - $exp); - - if ($mantissalow1 != 0) { - $value += 1 / 2 ** (21 - $exp); - } - - if ($mantissalow2 != 0) { - $value += $mantissalow2 / 2 ** (52 - $exp); - } - if ($sign) { - $value *= -1; - } - - return $value; - } - - private static function getIEEE754(int $rknum): float|int - { - if (($rknum & 0x02) != 0) { - $value = $rknum >> 2; - } else { - // changes by mmp, info on IEEE754 encoding from - // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html - // The RK format calls for using only the most significant 30 bits - // of the 64 bit floating point value. The other 34 bits are assumed - // to be 0 so we use the upper 30 bits of $rknum as follows... - $sign = ($rknum & self::HIGH_ORDER_BIT) >> 31; - $exp = ($rknum & 0x7FF00000) >> 20; - $mantissa = (0x100000 | ($rknum & 0x000FFFFC)); - $value = $mantissa / 2 ** (20 - ($exp - 1023)); - if ($sign) { - $value = -1 * $value; - } - //end of changes by mmp - } - if (($rknum & 0x01) != 0) { - $value /= 100; - } - - return $value; - } - - /** - * Get UTF-8 string from (compressed or uncompressed) UTF-16 string. - */ - private static function encodeUTF16(string $string, bool $compressed = false): string - { - if ($compressed) { - $string = self::uncompressByteString($string); - } - - return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE'); - } - - /** - * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8. - */ - private static function uncompressByteString(string $string): string - { - $uncompressedString = ''; - $strLen = strlen($string); - for ($i = 0; $i < $strLen; ++$i) { - $uncompressedString .= $string[$i] . "\0"; - } - - return $uncompressedString; - } - - /** - * Convert string to UTF-8. Only used for BIFF5. - */ - private function decodeCodepage(string $string): string - { - return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage); - } - - /** - * Read 16-bit unsigned integer. - */ - public static function getUInt2d(string $data, int $pos): int - { - return ord($data[$pos]) | (ord($data[$pos + 1]) << 8); - } - - /** - * Read 16-bit signed integer. - */ - public static function getInt2d(string $data, int $pos): int - { - return unpack('s', $data[$pos] . $data[$pos + 1])[1]; // @phpstan-ignore-line - } - - /** - * Read 32-bit signed integer. - */ - public static function getInt4d(string $data, int $pos): int - { - // FIX: represent numbers correctly on 64-bit system - // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 - // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems - $_or_24 = ord($data[$pos + 3]); - if ($_or_24 >= 128) { - // negative number - $_ord_24 = -abs((256 - $_or_24) << 24); - } else { - $_ord_24 = ($_or_24 & 127) << 24; - } - - return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; - } - - private function parseRichText(string $is): RichText + protected function parseRichText(string $is): RichText { $value = new RichText(); $value->createText($is); @@ -7356,286 +4753,14 @@ class Xls extends BaseReader * * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE */ - private function readCFHeader(): array + protected function readCFHeader(): array { - $length = self::getUInt2d($this->data, $this->pos + 2); - $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); - - // move stream pointer forward to next record - $this->pos += 4 + $length; - - if ($this->readDataOnly) { - return []; - } - - // offset: 0; size: 2; Rule Count -// $ruleCount = self::getUInt2d($recordData, 0); - - // offset: var; size: var; cell range address list with - $cellRangeAddressList = ($this->version == self::XLS_BIFF8) - ? $this->readBIFF8CellRangeAddressList(substr($recordData, 12)) - : $this->readBIFF5CellRangeAddressList(substr($recordData, 12)); - $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; - - return $cellRangeAddresses; + return (new Xls\ConditionalFormatting())->readCFHeader2($this); } - private function readCFRule(array $cellRangeAddresses): void + protected function readCFRule(array $cellRangeAddresses): void { - $length = self::getUInt2d($this->data, $this->pos + 2); - $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); - - // move stream pointer forward to next record - $this->pos += 4 + $length; - - if ($this->readDataOnly) { - return; - } - - // offset: 0; size: 2; Options - $cfRule = self::getUInt2d($recordData, 0); - - // bit: 8-15; mask: 0x00FF; type - $type = (0x00FF & $cfRule) >> 0; - $type = ConditionalFormatting::type($type); - - // bit: 0-7; mask: 0xFF00; type - $operator = (0xFF00 & $cfRule) >> 8; - $operator = ConditionalFormatting::operator($operator); - - if ($type === null || $operator === null) { - return; - } - - // offset: 2; size: 2; Size1 - $size1 = self::getUInt2d($recordData, 2); - - // offset: 4; size: 2; Size2 - $size2 = self::getUInt2d($recordData, 4); - - // offset: 6; size: 4; Options - $options = self::getInt4d($recordData, 6); - - $style = new Style(false, true); // non-supervisor, conditional - $noFormatSet = true; - //$this->getCFStyleOptions($options, $style); - - $hasFontRecord = (bool) ((0x04000000 & $options) >> 26); - $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27); - $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28); - $hasFillRecord = (bool) ((0x20000000 & $options) >> 29); - $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30); - // note unexpected values for following 4 - $hasBorderLeft = !(bool) (0x00000400 & $options); - $hasBorderRight = !(bool) (0x00000800 & $options); - $hasBorderTop = !(bool) (0x00001000 & $options); - $hasBorderBottom = !(bool) (0x00002000 & $options); - - $offset = 12; - - if ($hasFontRecord === true) { - $fontStyle = substr($recordData, $offset, 118); - $this->getCFFontStyle($fontStyle, $style); - $offset += 118; - $noFormatSet = false; - } - - if ($hasAlignmentRecord === true) { - //$alignmentStyle = substr($recordData, $offset, 8); - //$this->getCFAlignmentStyle($alignmentStyle, $style); - $offset += 8; - } - - if ($hasBorderRecord === true) { - $borderStyle = substr($recordData, $offset, 8); - $this->getCFBorderStyle($borderStyle, $style, $hasBorderLeft, $hasBorderRight, $hasBorderTop, $hasBorderBottom); - $offset += 8; - $noFormatSet = false; - } - - if ($hasFillRecord === true) { - $fillStyle = substr($recordData, $offset, 4); - $this->getCFFillStyle($fillStyle, $style); - $offset += 4; - $noFormatSet = false; - } - - if ($hasProtectionRecord === true) { - //$protectionStyle = substr($recordData, $offset, 4); - //$this->getCFProtectionStyle($protectionStyle, $style); - $offset += 2; - } - - $formula1 = $formula2 = null; - if ($size1 > 0) { - $formula1 = $this->readCFFormula($recordData, $offset, $size1); - if ($formula1 === null) { - return; - } - - $offset += $size1; - } - - if ($size2 > 0) { - $formula2 = $this->readCFFormula($recordData, $offset, $size2); - if ($formula2 === null) { - return; - } - - $offset += $size2; - } - - $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style, $noFormatSet); - } - - /*private function getCFStyleOptions(int $options, Style $style): void - { - }*/ - - private function getCFFontStyle(string $options, Style $style): void - { - $fontSize = self::getInt4d($options, 64); - if ($fontSize !== -1) { - $style->getFont()->setSize($fontSize / 20); // Convert twips to points - } - $options68 = self::getInt4d($options, 68); - $options88 = self::getInt4d($options, 88); - - if (($options88 & 2) === 0) { - $bold = self::getUInt2d($options, 72); // 400 = normal, 700 = bold - if ($bold !== 0) { - $style->getFont()->setBold($bold >= 550); - } - if (($options68 & 2) !== 0) { - $style->getFont()->setItalic(true); - } - } - if (($options88 & 0x80) === 0) { - if (($options68 & 0x80) !== 0) { - $style->getFont()->setStrikethrough(true); - } - } - - $color = self::getInt4d($options, 80); - - if ($color !== -1) { - $style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']); - } - } - - /*private function getCFAlignmentStyle(string $options, Style $style): void - { - }*/ - - private function getCFBorderStyle(string $options, Style $style, bool $hasBorderLeft, bool $hasBorderRight, bool $hasBorderTop, bool $hasBorderBottom): void - { - $valueArray = unpack('V', $options); - $value = is_array($valueArray) ? $valueArray[1] : 0; - $left = $value & 15; - $right = ($value >> 4) & 15; - $top = ($value >> 8) & 15; - $bottom = ($value >> 12) & 15; - $leftc = ($value >> 16) & 0x7F; - $rightc = ($value >> 23) & 0x7F; - $valueArray = unpack('V', substr($options, 4)); - $value = is_array($valueArray) ? $valueArray[1] : 0; - $topc = $value & 0x7F; - $bottomc = ($value & 0x3F80) >> 7; - if ($hasBorderLeft) { - $style->getBorders()->getLeft() - ->setBorderStyle(self::BORDER_STYLE_MAP[$left]); - $style->getBorders()->getLeft()->getColor() - ->setRGB(Xls\Color::map($leftc, $this->palette, $this->version)['rgb']); - } - if ($hasBorderRight) { - $style->getBorders()->getRight() - ->setBorderStyle(self::BORDER_STYLE_MAP[$right]); - $style->getBorders()->getRight()->getColor() - ->setRGB(Xls\Color::map($rightc, $this->palette, $this->version)['rgb']); - } - if ($hasBorderTop) { - $style->getBorders()->getTop() - ->setBorderStyle(self::BORDER_STYLE_MAP[$top]); - $style->getBorders()->getTop()->getColor() - ->setRGB(Xls\Color::map($topc, $this->palette, $this->version)['rgb']); - } - if ($hasBorderBottom) { - $style->getBorders()->getBottom() - ->setBorderStyle(self::BORDER_STYLE_MAP[$bottom]); - $style->getBorders()->getBottom()->getColor() - ->setRGB(Xls\Color::map($bottomc, $this->palette, $this->version)['rgb']); - } - } - - private function getCFFillStyle(string $options, Style $style): void - { - $fillPattern = self::getUInt2d($options, 0); - // bit: 10-15; mask: 0xFC00; type - $fillPattern = (0xFC00 & $fillPattern) >> 10; - $fillPattern = FillPattern::lookup($fillPattern); - $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern; - - if ($fillPattern !== Fill::FILL_NONE) { - $style->getFill()->setFillType($fillPattern); - - $fillColors = self::getUInt2d($options, 2); - - // bit: 0-6; mask: 0x007F; type - $color1 = (0x007F & $fillColors) >> 0; - - // bit: 7-13; mask: 0x3F80; type - $color2 = (0x3F80 & $fillColors) >> 7; - if ($fillPattern === Fill::FILL_SOLID) { - $style->getFill()->getStartColor()->setRGB(Xls\Color::map($color2, $this->palette, $this->version)['rgb']); - } else { - $style->getFill()->getStartColor()->setRGB(Xls\Color::map($color1, $this->palette, $this->version)['rgb']); - $style->getFill()->getEndColor()->setRGB(Xls\Color::map($color2, $this->palette, $this->version)['rgb']); - } - } - } - - /*private function getCFProtectionStyle(string $options, Style $style): void - { - }*/ - - private function readCFFormula(string $recordData, int $offset, int $size): float|int|string|null - { - try { - $formula = substr($recordData, $offset, $size); - $formula = pack('v', $size) . $formula; // prepend the length - - $formula = $this->getFormulaFromStructure($formula); - if (is_numeric($formula)) { - return (str_contains($formula, '.')) ? (float) $formula : (int) $formula; - } - - return $formula; - } catch (PhpSpreadsheetException) { - return null; - } - } - - private function setCFRules(array $cellRanges, string $type, string $operator, null|float|int|string $formula1, null|float|int|string $formula2, Style $style, bool $noFormatSet): void - { - foreach ($cellRanges as $cellRange) { - $conditional = new Conditional(); - $conditional->setNoFormatSet($noFormatSet); - $conditional->setConditionType($type); - $conditional->setOperatorType($operator); - $conditional->setStopIfTrue(true); - if ($formula1 !== null) { - $conditional->addCondition($formula1); - } - if ($formula2 !== null) { - $conditional->addCondition($formula2); - } - $conditional->setStyle($style); - - $conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles(); - $conditionalStyles[] = $conditional; - - $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); - } + (new Xls\ConditionalFormatting())->readCFRule2($cellRangeAddresses, $this); } public function getVersion(): int diff --git a/src/PhpSpreadsheet/Reader/Xls/Biff5.php b/src/PhpSpreadsheet/Reader/Xls/Biff5.php new file mode 100644 index 000000000..ef9619eec --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/Biff5.php @@ -0,0 +1,69 @@ + $lr || $fc > $lc) { + throw new ReaderException('Not a cell range address'); + } + + // column index to letter + $fc = Coordinate::stringFromColumnIndex($fc + 1); + $lc = Coordinate::stringFromColumnIndex($lc + 1); + + if ($fr == $lr && $fc == $lc) { + return "$fc$fr"; + } + + return "$fc$fr:$lc$lr"; + } + + /** + * Read BIFF5 cell range address list + * section 2.5.15. + */ + public static function readBIFF5CellRangeAddressList(string $subData): array + { + $cellRangeAddresses = []; + + // offset: 0; size: 2; number of the following cell range addresses + $nm = self::getUInt2d($subData, 0); + + $offset = 2; + // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses + for ($i = 0; $i < $nm; ++$i) { + $cellRangeAddresses[] = self::readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6)); + $offset += 6; + } + + return [ + 'size' => 2 + 6 * $nm, + 'cellRangeAddresses' => $cellRangeAddresses, + ]; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xls/Biff8.php b/src/PhpSpreadsheet/Reader/Xls/Biff8.php new file mode 100644 index 000000000..f95c0b5ea --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/Biff8.php @@ -0,0 +1,365 @@ + '{1,2;3,4}', 'size' => 40] + * section 2.5.8. + */ + protected static function readBIFF8ConstantArray(string $arrayData): array + { + // offset: 0; size: 1; number of columns decreased by 1 + $nc = ord($arrayData[0]); + + // offset: 1; size: 2; number of rows decreased by 1 + $nr = self::getUInt2d($arrayData, 1); + $size = 3; // initialize + $arrayData = substr($arrayData, 3); + + // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values + $matrixChunks = []; + for ($r = 1; $r <= $nr + 1; ++$r) { + $items = []; + for ($c = 1; $c <= $nc + 1; ++$c) { + $constant = self::readBIFF8Constant($arrayData); + $items[] = $constant['value']; + $arrayData = substr($arrayData, $constant['size']); + $size += $constant['size']; + } + $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"' + } + $matrix = '{' . implode(';', $matrixChunks) . '}'; + + return [ + 'value' => $matrix, + 'size' => $size, + ]; + } + + /** + * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value' + * section 2.5.7 + * returns e.g. ['value' => '5', 'size' => 9]. + */ + private static function readBIFF8Constant(string $valueData): array + { + // offset: 0; size: 1; identifier for type of constant + $identifier = ord($valueData[0]); + + switch ($identifier) { + case 0x00: // empty constant (what is this?) + $value = ''; + $size = 9; + + break; + case 0x01: // number + // offset: 1; size: 8; IEEE 754 floating-point value + $value = self::extractNumber(substr($valueData, 1, 8)); + $size = 9; + + break; + case 0x02: // string value + // offset: 1; size: var; Unicode string, 16-bit string length + $string = self::readUnicodeStringLong(substr($valueData, 1)); + $value = '"' . $string['value'] . '"'; + $size = 1 + $string['size']; + + break; + case 0x04: // boolean + // offset: 1; size: 1; 0 = FALSE, 1 = TRUE + if (ord($valueData[1])) { + $value = 'TRUE'; + } else { + $value = 'FALSE'; + } + $size = 9; + + break; + case 0x10: // error code + // offset: 1; size: 1; error code + $value = ErrorCode::lookup(ord($valueData[1])); + $size = 9; + + break; + default: + throw new ReaderException('Unsupported BIFF8 constant'); + } + + return [ + 'value' => $value, + 'size' => $size, + ]; + } + + /** + * Read BIFF8 cell range address list + * section 2.5.15. + */ + public static function readBIFF8CellRangeAddressList(string $subData): array + { + $cellRangeAddresses = []; + + // offset: 0; size: 2; number of the following cell range addresses + $nm = self::getUInt2d($subData, 0); + + $offset = 2; + // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses + for ($i = 0; $i < $nm; ++$i) { + $cellRangeAddresses[] = self::readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8)); + $offset += 8; + } + + return [ + 'size' => 2 + 8 * $nm, + 'cellRangeAddresses' => $cellRangeAddresses, + ]; + } + + /** + * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2' + * section 3.3.4. + */ + protected static function readBIFF8CellAddress(string $cellAddressStructure): string + { + // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767)) + $row = self::getUInt2d($cellAddressStructure, 0) + 1; + + // offset: 2; size: 2; index to column or column offset + relative flags + // bit: 7-0; mask 0x00FF; column index + $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1); + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) { + $column = '$' . $column; + } + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) { + $row = '$' . $row; + } + + return $column . $row; + } + + /** + * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column + * to indicate offsets from a base cell + * section 3.3.4. + * + * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas + */ + protected static function readBIFF8CellAddressB(string $cellAddressStructure, string $baseCell = 'A1'): string + { + [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell); + $baseCol = Coordinate::columnIndexFromString($baseCol) - 1; + $baseRow = (int) $baseRow; + + // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767)) + $rowIndex = self::getUInt2d($cellAddressStructure, 0); + $row = self::getUInt2d($cellAddressStructure, 0) + 1; + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) { + // offset: 2; size: 2; index to column or column offset + relative flags + // bit: 7-0; mask 0x00FF; column index + $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2); + + $column = Coordinate::stringFromColumnIndex($colIndex + 1); + $column = '$' . $column; + } else { + // offset: 2; size: 2; index to column or column offset + relative flags + // bit: 7-0; mask 0x00FF; column index + $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2); + $colIndex = $baseCol + $relativeColIndex; + $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256; + $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256; + $column = Coordinate::stringFromColumnIndex($colIndex + 1); + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) { + $row = '$' . $row; + } else { + $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536; + $row = $baseRow + $rowIndex; + } + + return $column . $row; + } + + /** + * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1' + * always fixed range + * section 2.5.14. + */ + protected static function readBIFF8CellRangeAddressFixed(string $subData): string + { + // offset: 0; size: 2; index to first row + $fr = self::getUInt2d($subData, 0) + 1; + + // offset: 2; size: 2; index to last row + $lr = self::getUInt2d($subData, 2) + 1; + + // offset: 4; size: 2; index to first column + $fc = self::getUInt2d($subData, 4); + + // offset: 6; size: 2; index to last column + $lc = self::getUInt2d($subData, 6); + + // check values + if ($fr > $lr || $fc > $lc) { + throw new ReaderException('Not a cell range address'); + } + + // column index to letter + $fc = Coordinate::stringFromColumnIndex($fc + 1); + $lc = Coordinate::stringFromColumnIndex($lc + 1); + + if ($fr == $lr && $fc == $lc) { + return "$fc$fr"; + } + + return "$fc$fr:$lc$lr"; + } + + /** + * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6' + * there are flags indicating whether column/row index is relative + * section 3.3.4. + */ + protected static function readBIFF8CellRangeAddress(string $subData): string + { + // todo: if cell range is just a single cell, should this funciton + // not just return e.g. 'A1' and not 'A1:A1' ? + + // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767)) + $fr = self::getUInt2d($subData, 0) + 1; + + // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767)) + $lr = self::getUInt2d($subData, 2) + 1; + + // offset: 4; size: 2; index to first column or column offset + relative flags + + // bit: 7-0; mask 0x00FF; column index + $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1); + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 4))) { + $fc = '$' . $fc; + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 4))) { + $fr = '$' . $fr; + } + + // offset: 6; size: 2; index to last column or column offset + relative flags + + // bit: 7-0; mask 0x00FF; column index + $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1); + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 6))) { + $lc = '$' . $lc; + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 6))) { + $lr = '$' . $lr; + } + + return "$fc$fr:$lc$lr"; + } + + /** + * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column + * to indicate offsets from a base cell + * section 3.3.4. + * + * @param string $baseCell Base cell + * + * @return string Cell range address + */ + protected static function readBIFF8CellRangeAddressB(string $subData, string $baseCell = 'A1'): string + { + [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell); + $baseCol = $baseCol - 1; + + // TODO: if cell range is just a single cell, should this funciton + // not just return e.g. 'A1' and not 'A1:A1' ? + + // offset: 0; size: 2; first row + $frIndex = self::getUInt2d($subData, 0); // adjust below + + // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767) + $lrIndex = self::getUInt2d($subData, 2); // adjust below + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 4))) { + // absolute column index + // offset: 4; size: 2; first column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $fcIndex = 0x00FF & self::getUInt2d($subData, 4); + $fc = Coordinate::stringFromColumnIndex($fcIndex + 1); + $fc = '$' . $fc; + } else { + // column offset + // offset: 4; size: 2; first column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4); + $fcIndex = $baseCol + $relativeFcIndex; + $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256; + $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256; + $fc = Coordinate::stringFromColumnIndex($fcIndex + 1); + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 4))) { + // absolute row index + $fr = $frIndex + 1; + $fr = '$' . $fr; + } else { + // row offset + $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536; + $fr = $baseRow + $frIndex; + } + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 6))) { + // absolute column index + // offset: 6; size: 2; last column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $lcIndex = 0x00FF & self::getUInt2d($subData, 6); + $lc = Coordinate::stringFromColumnIndex($lcIndex + 1); + $lc = '$' . $lc; + } else { + // column offset + // offset: 6; size: 2; last column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $relativeLcIndex = 0x00FF & self::getInt2d($subData, 6); + $lcIndex = $baseCol + $relativeLcIndex; + $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256; + $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256; + $lc = Coordinate::stringFromColumnIndex($lcIndex + 1); + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 6))) { + // absolute row index + $lr = $lrIndex + 1; + $lr = '$' . $lr; + } else { + // row offset + $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536; + $lr = $baseRow + $lrIndex; + } + + return "$fc$fr:$lc$lr"; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php index fbd31d565..324d12cb2 100644 --- a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php +++ b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php @@ -2,9 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Reader\Xls; +use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern; use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\Fill; +use PhpOffice\PhpSpreadsheet\Style\Style; -class ConditionalFormatting +class ConditionalFormatting extends Xls { /** * @var array @@ -46,4 +51,291 @@ class ConditionalFormatting return null; } + + /** + * Parse conditional formatting blocks. + * + * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE + */ + protected function readCFHeader2(Xls $xls): array + { + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer forward to next record + $xls->pos += 4 + $length; + + if ($xls->readDataOnly) { + return []; + } + + // offset: 0; size: 2; Rule Count +// $ruleCount = self::getUInt2d($recordData, 0); + + // offset: var; size: var; cell range address list with + $cellRangeAddressList = ($xls->version == self::XLS_BIFF8) + ? Biff8::readBIFF8CellRangeAddressList(substr($recordData, 12)) + : Biff5::readBIFF5CellRangeAddressList(substr($recordData, 12)); + $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; + + return $cellRangeAddresses; + } + + protected function readCFRule2(array $cellRangeAddresses, Xls $xls): void + { + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer forward to next record + $xls->pos += 4 + $length; + + if ($xls->readDataOnly) { + return; + } + + // offset: 0; size: 2; Options + $cfRule = self::getUInt2d($recordData, 0); + + // bit: 8-15; mask: 0x00FF; type + $type = (0x00FF & $cfRule) >> 0; + $type = self::type($type); + + // bit: 0-7; mask: 0xFF00; type + $operator = (0xFF00 & $cfRule) >> 8; + $operator = self::operator($operator); + + if ($type === null || $operator === null) { + return; + } + + // offset: 2; size: 2; Size1 + $size1 = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2; Size2 + $size2 = self::getUInt2d($recordData, 4); + + // offset: 6; size: 4; Options + $options = self::getInt4d($recordData, 6); + + $style = new Style(false, true); // non-supervisor, conditional + $noFormatSet = true; + //$xls->getCFStyleOptions($options, $style); + + $hasFontRecord = (bool) ((0x04000000 & $options) >> 26); + $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27); + $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28); + $hasFillRecord = (bool) ((0x20000000 & $options) >> 29); + $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30); + // note unexpected values for following 4 + $hasBorderLeft = !(bool) (0x00000400 & $options); + $hasBorderRight = !(bool) (0x00000800 & $options); + $hasBorderTop = !(bool) (0x00001000 & $options); + $hasBorderBottom = !(bool) (0x00002000 & $options); + + $offset = 12; + + if ($hasFontRecord === true) { + $fontStyle = substr($recordData, $offset, 118); + $this->getCFFontStyle($fontStyle, $style, $xls); + $offset += 118; + $noFormatSet = false; + } + + if ($hasAlignmentRecord === true) { + //$alignmentStyle = substr($recordData, $offset, 8); + //$this->getCFAlignmentStyle($alignmentStyle, $style, $xls); + $offset += 8; + } + + if ($hasBorderRecord === true) { + $borderStyle = substr($recordData, $offset, 8); + $this->getCFBorderStyle($borderStyle, $style, $hasBorderLeft, $hasBorderRight, $hasBorderTop, $hasBorderBottom, $xls); + $offset += 8; + $noFormatSet = false; + } + + if ($hasFillRecord === true) { + $fillStyle = substr($recordData, $offset, 4); + $this->getCFFillStyle($fillStyle, $style, $xls); + $offset += 4; + $noFormatSet = false; + } + + if ($hasProtectionRecord === true) { + //$protectionStyle = substr($recordData, $offset, 4); + //$this->getCFProtectionStyle($protectionStyle, $style, $xls); + $offset += 2; + } + + $formula1 = $formula2 = null; + if ($size1 > 0) { + $formula1 = $this->readCFFormula($recordData, $offset, $size1, $xls); + if ($formula1 === null) { + return; + } + + $offset += $size1; + } + + if ($size2 > 0) { + $formula2 = $this->readCFFormula($recordData, $offset, $size2, $xls); + if ($formula2 === null) { + return; + } + + $offset += $size2; + } + + $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style, $noFormatSet, $xls); + } + + /*private function getCFStyleOptions(int $options, Style $style, Xls $xls): void + { + }*/ + + private function getCFFontStyle(string $options, Style $style, Xls $xls): void + { + $fontSize = self::getInt4d($options, 64); + if ($fontSize !== -1) { + $style->getFont()->setSize($fontSize / 20); // Convert twips to points + } + $options68 = self::getInt4d($options, 68); + $options88 = self::getInt4d($options, 88); + + if (($options88 & 2) === 0) { + $bold = self::getUInt2d($options, 72); // 400 = normal, 700 = bold + if ($bold !== 0) { + $style->getFont()->setBold($bold >= 550); + } + if (($options68 & 2) !== 0) { + $style->getFont()->setItalic(true); + } + } + if (($options88 & 0x80) === 0) { + if (($options68 & 0x80) !== 0) { + $style->getFont()->setStrikethrough(true); + } + } + + $color = self::getInt4d($options, 80); + + if ($color !== -1) { + $style->getFont()->getColor()->setRGB(Color::map($color, $xls->palette, $xls->version)['rgb']); + } + } + + /*private function getCFAlignmentStyle(string $options, Style $style, Xls $xls): void + { + }*/ + + private function getCFBorderStyle(string $options, Style $style, bool $hasBorderLeft, bool $hasBorderRight, bool $hasBorderTop, bool $hasBorderBottom, Xls $xls): void + { + $valueArray = unpack('V', $options); + $value = is_array($valueArray) ? $valueArray[1] : 0; + $left = $value & 15; + $right = ($value >> 4) & 15; + $top = ($value >> 8) & 15; + $bottom = ($value >> 12) & 15; + $leftc = ($value >> 16) & 0x7F; + $rightc = ($value >> 23) & 0x7F; + $valueArray = unpack('V', substr($options, 4)); + $value = is_array($valueArray) ? $valueArray[1] : 0; + $topc = $value & 0x7F; + $bottomc = ($value & 0x3F80) >> 7; + if ($hasBorderLeft) { + $style->getBorders()->getLeft() + ->setBorderStyle(self::BORDER_STYLE_MAP[$left]); + $style->getBorders()->getLeft()->getColor() + ->setRGB(Color::map($leftc, $xls->palette, $xls->version)['rgb']); + } + if ($hasBorderRight) { + $style->getBorders()->getRight() + ->setBorderStyle(self::BORDER_STYLE_MAP[$right]); + $style->getBorders()->getRight()->getColor() + ->setRGB(Color::map($rightc, $xls->palette, $xls->version)['rgb']); + } + if ($hasBorderTop) { + $style->getBorders()->getTop() + ->setBorderStyle(self::BORDER_STYLE_MAP[$top]); + $style->getBorders()->getTop()->getColor() + ->setRGB(Color::map($topc, $xls->palette, $xls->version)['rgb']); + } + if ($hasBorderBottom) { + $style->getBorders()->getBottom() + ->setBorderStyle(self::BORDER_STYLE_MAP[$bottom]); + $style->getBorders()->getBottom()->getColor() + ->setRGB(Color::map($bottomc, $xls->palette, $xls->version)['rgb']); + } + } + + private function getCFFillStyle(string $options, Style $style, Xls $xls): void + { + $fillPattern = self::getUInt2d($options, 0); + // bit: 10-15; mask: 0xFC00; type + $fillPattern = (0xFC00 & $fillPattern) >> 10; + $fillPattern = FillPattern::lookup($fillPattern); + $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern; + + if ($fillPattern !== Fill::FILL_NONE) { + $style->getFill()->setFillType($fillPattern); + + $fillColors = self::getUInt2d($options, 2); + + // bit: 0-6; mask: 0x007F; type + $color1 = (0x007F & $fillColors) >> 0; + + // bit: 7-13; mask: 0x3F80; type + $color2 = (0x3F80 & $fillColors) >> 7; + if ($fillPattern === Fill::FILL_SOLID) { + $style->getFill()->getStartColor()->setRGB(Color::map($color2, $xls->palette, $xls->version)['rgb']); + } else { + $style->getFill()->getStartColor()->setRGB(Color::map($color1, $xls->palette, $xls->version)['rgb']); + $style->getFill()->getEndColor()->setRGB(Color::map($color2, $xls->palette, $xls->version)['rgb']); + } + } + } + + /*private function getCFProtectionStyle(string $options, Style $style, Xls $xls): void + { + }*/ + + private function readCFFormula(string $recordData, int $offset, int $size, Xls $xls): float|int|string|null + { + try { + $formula = substr($recordData, $offset, $size); + $formula = pack('v', $size) . $formula; // prepend the length + + $formula = $xls->getFormulaFromStructure($formula); + if (is_numeric($formula)) { + return (str_contains($formula, '.')) ? (float) $formula : (int) $formula; + } + + return $formula; + } catch (PhpSpreadsheetException) { + return null; + } + } + + private function setCFRules(array $cellRanges, string $type, string $operator, null|float|int|string $formula1, null|float|int|string $formula2, Style $style, bool $noFormatSet, Xls $xls): void + { + foreach ($cellRanges as $cellRange) { + $conditional = new Conditional(); + $conditional->setNoFormatSet($noFormatSet); + $conditional->setConditionType($type); + $conditional->setOperatorType($operator); + $conditional->setStopIfTrue(true); + if ($formula1 !== null) { + $conditional->addCondition($formula1); + } + if ($formula2 !== null) { + $conditional->addCondition($formula2); + } + $conditional->setStyle($style); + + $conditionalStyles = $xls->phpSheet->getStyle($cellRange)->getConditionalStyles(); + $conditionalStyles[] = $conditional; + + $xls->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + } + } } diff --git a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php index 874e6994f..a0c897efc 100644 --- a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php +++ b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php @@ -2,9 +2,12 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Reader\Xls; -class DataValidationHelper +class DataValidationHelper extends Xls { /** * @var array @@ -69,4 +72,141 @@ class DataValidationHelper return null; } + + /** + * Read DATAVALIDATION record. + */ + protected function readDataValidation2(Xls $xls): void + { + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer forward to next record + $xls->pos += 4 + $length; + + if ($xls->readDataOnly) { + return; + } + + // offset: 0; size: 4; Options + $options = self::getInt4d($recordData, 0); + + // bit: 0-3; mask: 0x0000000F; type + $type = (0x0000000F & $options) >> 0; + $type = self::type($type); + + // bit: 4-6; mask: 0x00000070; error type + $errorStyle = (0x00000070 & $options) >> 4; + $errorStyle = self::errorStyle($errorStyle); + + // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list) + // I have only seen cases where this is 1 + //$explicitFormula = (0x00000080 & $options) >> 7; + + // bit: 8; mask: 0x00000100; 1= empty cells allowed + $allowBlank = (0x00000100 & $options) >> 8; + + // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity + $suppressDropDown = (0x00000200 & $options) >> 9; + + // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected + $showInputMessage = (0x00040000 & $options) >> 18; + + // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered + $showErrorMessage = (0x00080000 & $options) >> 19; + + // bit: 20-23; mask: 0x00F00000; condition operator + $operator = (0x00F00000 & $options) >> 20; + $operator = self::operator($operator); + + if ($type === null || $errorStyle === null || $operator === null) { + return; + } + + // offset: 4; size: var; title of the prompt box + $offset = 4; + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $promptTitle = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: var; title of the error box + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $errorTitle = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: var; text of the prompt box + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $prompt = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: var; text of the error box + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $error = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: 2; size of the formula data for the first condition + $sz1 = self::getUInt2d($recordData, $offset); + $offset += 2; + + // offset: var; size: 2; not used + $offset += 2; + + // offset: var; size: $sz1; formula data for first condition (without size field) + $formula1 = substr($recordData, $offset, $sz1); + $formula1 = pack('v', $sz1) . $formula1; // prepend the length + + try { + $formula1 = $xls->getFormulaFromStructure($formula1); + + // in list type validity, null characters are used as item separators + if ($type == DataValidation::TYPE_LIST) { + $formula1 = str_replace(chr(0), ',', $formula1); + } + } catch (PhpSpreadsheetException $e) { + return; + } + $offset += $sz1; + + // offset: var; size: 2; size of the formula data for the first condition + $sz2 = self::getUInt2d($recordData, $offset); + $offset += 2; + + // offset: var; size: 2; not used + $offset += 2; + + // offset: var; size: $sz2; formula data for second condition (without size field) + $formula2 = substr($recordData, $offset, $sz2); + $formula2 = pack('v', $sz2) . $formula2; // prepend the length + + try { + $formula2 = $xls->getFormulaFromStructure($formula2); + } catch (PhpSpreadsheetException) { + return; + } + $offset += $sz2; + + // offset: var; size: var; cell range address list with + $cellRangeAddressList = Biff8::readBIFF8CellRangeAddressList(substr($recordData, $offset)); + $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; + + foreach ($cellRangeAddresses as $cellRange) { + $stRange = $xls->phpSheet->shrinkRangeToFit($cellRange); + foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) { + $objValidation = $xls->phpSheet->getCell($coordinate)->getDataValidation(); + $objValidation->setType($type); + $objValidation->setErrorStyle($errorStyle); + $objValidation->setAllowBlank((bool) $allowBlank); + $objValidation->setShowInputMessage((bool) $showInputMessage); + $objValidation->setShowErrorMessage((bool) $showErrorMessage); + $objValidation->setShowDropDown(!$suppressDropDown); + $objValidation->setOperator($operator); + $objValidation->setErrorTitle($errorTitle); + $objValidation->setError($error); + $objValidation->setPromptTitle($promptTitle); + $objValidation->setPrompt($prompt); + $objValidation->setFormula1($formula1); + $objValidation->setFormula2($formula2); + } + } + } } diff --git a/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php b/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php new file mode 100644 index 000000000..2d8778ff2 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php @@ -0,0 +1,158 @@ +loadOLE($filename); + + // total byte size of Excel data (workbook global substream + sheet substreams) + $xls->dataSize = strlen($xls->data); + + $xls->pos = 0; + $xls->sheets = []; + + // Parse Workbook Global Substream + while ($xls->pos < $xls->dataSize) { + $code = self::getUInt2d($xls->data, $xls->pos); + + match ($code) { + self::XLS_TYPE_BOF => $xls->readBof(), + self::XLS_TYPE_SHEET => $xls->readSheet(), + self::XLS_TYPE_EOF => $xls->readDefault(), + self::XLS_TYPE_CODEPAGE => $xls->readCodepage(), + default => $xls->readDefault(), + }; + + if ($code === self::XLS_TYPE_EOF) { + break; + } + } + + foreach ($xls->sheets as $sheet) { + if ($sheet['sheetType'] != 0x00) { + // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module + continue; + } + + $worksheetNames[] = $sheet['name']; + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + */ + protected function listWorksheetInfo2(string $filename, Xls $xls): array + { + File::assertFile($filename); + + $worksheetInfo = []; + + // Read the OLE file + $xls->loadOLE($filename); + + // total byte size of Excel data (workbook global substream + sheet substreams) + $xls->dataSize = strlen($xls->data); + + // initialize + $xls->pos = 0; + $xls->sheets = []; + + // Parse Workbook Global Substream + while ($xls->pos < $xls->dataSize) { + $code = self::getUInt2d($xls->data, $xls->pos); + + match ($code) { + self::XLS_TYPE_BOF => $xls->readBof(), + self::XLS_TYPE_SHEET => $xls->readSheet(), + self::XLS_TYPE_EOF => $xls->readDefault(), + self::XLS_TYPE_CODEPAGE => $xls->readCodepage(), + default => $xls->readDefault(), + }; + + if ($code === self::XLS_TYPE_EOF) { + break; + } + } + + // Parse the individual sheets + foreach ($xls->sheets as $sheet) { + if ($sheet['sheetType'] != 0x00) { + // 0x00: Worksheet + // 0x02: Chart + // 0x06: Visual Basic module + continue; + } + + $tmpInfo = []; + $tmpInfo['worksheetName'] = $sheet['name']; + $tmpInfo['lastColumnLetter'] = 'A'; + $tmpInfo['lastColumnIndex'] = 0; + $tmpInfo['totalRows'] = 0; + $tmpInfo['totalColumns'] = 0; + + $xls->pos = $sheet['offset']; + + while ($xls->pos <= $xls->dataSize - 4) { + $code = self::getUInt2d($xls->data, $xls->pos); + + switch ($code) { + case self::XLS_TYPE_RK: + case self::XLS_TYPE_LABELSST: + case self::XLS_TYPE_NUMBER: + case self::XLS_TYPE_FORMULA: + case self::XLS_TYPE_BOOLERR: + case self::XLS_TYPE_LABEL: + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer to next record + $xls->pos += 4 + $length; + + $rowIndex = self::getUInt2d($recordData, 0) + 1; + $columnIndex = self::getUInt2d($recordData, 2); + + $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); + $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); + + break; + case self::XLS_TYPE_BOF: + $xls->readBof(); + + break; + case self::XLS_TYPE_EOF: + $xls->readDefault(); + + break 2; + default: + $xls->readDefault(); + + break; + } + } + + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; + + $worksheetInfo[] = $tmpInfo; + } + + return $worksheetInfo; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php new file mode 100644 index 000000000..aeda44aa2 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php @@ -0,0 +1,671 @@ +loadOLE($filename); + + // Initialisations + $xls->spreadsheet = new Spreadsheet(); + $xls->spreadsheet->setValueBinder($this->valueBinder); + $xls->spreadsheet->removeSheetByIndex(0); // remove 1st sheet + if (!$xls->readDataOnly) { + $xls->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style + $xls->spreadsheet->removeCellXfByIndex(0); // remove the default style + } + + // Read the summary information stream (containing meta data) + $xls->readSummaryInformation(); + + // Read the Additional document summary information stream (containing application-specific meta data) + $xls->readDocumentSummaryInformation(); + + // total byte size of Excel data (workbook global substream + sheet substreams) + $xls->dataSize = strlen($xls->data); + + // initialize + $xls->pos = 0; + $xls->codepage = $xls->codepage ?: CodePage::DEFAULT_CODE_PAGE; + $xls->formats = []; + $xls->objFonts = []; + $xls->palette = []; + $xls->sheets = []; + $xls->externalBooks = []; + $xls->ref = []; + $xls->definedname = []; + $xls->sst = []; + $xls->drawingGroupData = ''; + $xls->xfIndex = 0; + $xls->mapCellXfIndex = []; + $xls->mapCellStyleXfIndex = []; + + // Parse Workbook Global Substream + while ($xls->pos < $xls->dataSize) { + $code = self::getUInt2d($xls->data, $xls->pos); + + match ($code) { + self::XLS_TYPE_BOF => $xls->readBof(), + self::XLS_TYPE_FILEPASS => $xls->readFilepass(), + self::XLS_TYPE_CODEPAGE => $xls->readCodepage(), + self::XLS_TYPE_DATEMODE => $xls->readDateMode(), + self::XLS_TYPE_FONT => $xls->readFont(), + self::XLS_TYPE_FORMAT => $xls->readFormat(), + self::XLS_TYPE_XF => $xls->readXf(), + self::XLS_TYPE_XFEXT => $xls->readXfExt(), + self::XLS_TYPE_STYLE => $xls->readStyle(), + self::XLS_TYPE_PALETTE => $xls->readPalette(), + self::XLS_TYPE_SHEET => $xls->readSheet(), + self::XLS_TYPE_EXTERNALBOOK => $xls->readExternalBook(), + self::XLS_TYPE_EXTERNNAME => $xls->readExternName(), + self::XLS_TYPE_EXTERNSHEET => $xls->readExternSheet(), + self::XLS_TYPE_DEFINEDNAME => $xls->readDefinedName(), + self::XLS_TYPE_MSODRAWINGGROUP => $xls->readMsoDrawingGroup(), + self::XLS_TYPE_SST => $xls->readSst(), + self::XLS_TYPE_EOF => $xls->readDefault(), + default => $xls->readDefault(), + }; + + if ($code === self::XLS_TYPE_EOF) { + break; + } + } + + // Resolve indexed colors for font, fill, and border colors + // Cannot be resolved already in XF record, because PALETTE record comes afterwards + if (!$xls->readDataOnly) { + foreach ($xls->objFonts as $objFont) { + if (isset($objFont->colorIndex)) { + $color = Color::map($objFont->colorIndex, $xls->palette, $xls->version); + $objFont->getColor()->setRGB($color['rgb']); + } + } + + foreach ($xls->spreadsheet->getCellXfCollection() as $objStyle) { + // fill start and end color + $fill = $objStyle->getFill(); + + if (isset($fill->startcolorIndex)) { + $startColor = Color::map($fill->startcolorIndex, $xls->palette, $xls->version); + $fill->getStartColor()->setRGB($startColor['rgb']); + } + if (isset($fill->endcolorIndex)) { + $endColor = Color::map($fill->endcolorIndex, $xls->palette, $xls->version); + $fill->getEndColor()->setRGB($endColor['rgb']); + } + + // border colors + $top = $objStyle->getBorders()->getTop(); + $right = $objStyle->getBorders()->getRight(); + $bottom = $objStyle->getBorders()->getBottom(); + $left = $objStyle->getBorders()->getLeft(); + $diagonal = $objStyle->getBorders()->getDiagonal(); + + if (isset($top->colorIndex)) { + $borderTopColor = Color::map($top->colorIndex, $xls->palette, $xls->version); + $top->getColor()->setRGB($borderTopColor['rgb']); + } + if (isset($right->colorIndex)) { + $borderRightColor = Color::map($right->colorIndex, $xls->palette, $xls->version); + $right->getColor()->setRGB($borderRightColor['rgb']); + } + if (isset($bottom->colorIndex)) { + $borderBottomColor = Color::map($bottom->colorIndex, $xls->palette, $xls->version); + $bottom->getColor()->setRGB($borderBottomColor['rgb']); + } + if (isset($left->colorIndex)) { + $borderLeftColor = Color::map($left->colorIndex, $xls->palette, $xls->version); + $left->getColor()->setRGB($borderLeftColor['rgb']); + } + if (isset($diagonal->colorIndex)) { + $borderDiagonalColor = Color::map($diagonal->colorIndex, $xls->palette, $xls->version); + $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']); + } + } + } + + // treat MSODRAWINGGROUP records, workbook-level Escher + $escherWorkbook = null; + if (!$xls->readDataOnly && $xls->drawingGroupData) { + $escher = new SharedEscher(); + $reader = new Escher($escher); + $escherWorkbook = $reader->load($xls->drawingGroupData); + } + + // Parse the individual sheets + $xls->activeSheetSet = false; + foreach ($xls->sheets as $sheet) { + $selectedCells = ''; + if ($sheet['sheetType'] != 0x00) { + // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module + continue; + } + + // check if sheet should be skipped + if (isset($xls->loadSheetsOnly) && !in_array($sheet['name'], $xls->loadSheetsOnly)) { + continue; + } + + // add sheet to PhpSpreadsheet object + $xls->phpSheet = $xls->spreadsheet->createSheet(); + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula + // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet + // name in line with the formula, not the reverse + $xls->phpSheet->setTitle($sheet['name'], false, false); + $xls->phpSheet->setSheetState($sheet['sheetState']); + + $xls->pos = $sheet['offset']; + + // Initialize isFitToPages. May change after reading SHEETPR record. + $xls->isFitToPages = false; + + // Initialize drawingData + $xls->drawingData = ''; + + // Initialize objs + $xls->objs = []; + + // Initialize shared formula parts + $xls->sharedFormulaParts = []; + + // Initialize shared formulas + $xls->sharedFormulas = []; + + // Initialize text objs + $xls->textObjects = []; + + // Initialize cell annotations + $xls->cellNotes = []; + $xls->textObjRef = -1; + + while ($xls->pos <= $xls->dataSize - 4) { + $code = self::getUInt2d($xls->data, $xls->pos); + + switch ($code) { + case self::XLS_TYPE_BOF: + $xls->readBof(); + + break; + case self::XLS_TYPE_PRINTGRIDLINES: + $xls->readPrintGridlines(); + + break; + case self::XLS_TYPE_DEFAULTROWHEIGHT: + $xls->readDefaultRowHeight(); + + break; + case self::XLS_TYPE_SHEETPR: + $xls->readSheetPr(); + + break; + case self::XLS_TYPE_HORIZONTALPAGEBREAKS: + $xls->readHorizontalPageBreaks(); + + break; + case self::XLS_TYPE_VERTICALPAGEBREAKS: + $xls->readVerticalPageBreaks(); + + break; + case self::XLS_TYPE_HEADER: + $xls->readHeader(); + + break; + case self::XLS_TYPE_FOOTER: + $xls->readFooter(); + + break; + case self::XLS_TYPE_HCENTER: + $xls->readHcenter(); + + break; + case self::XLS_TYPE_VCENTER: + $xls->readVcenter(); + + break; + case self::XLS_TYPE_LEFTMARGIN: + $xls->readLeftMargin(); + + break; + case self::XLS_TYPE_RIGHTMARGIN: + $xls->readRightMargin(); + + break; + case self::XLS_TYPE_TOPMARGIN: + $xls->readTopMargin(); + + break; + case self::XLS_TYPE_BOTTOMMARGIN: + $xls->readBottomMargin(); + + break; + case self::XLS_TYPE_PAGESETUP: + $xls->readPageSetup(); + + break; + case self::XLS_TYPE_PROTECT: + $xls->readProtect(); + + break; + case self::XLS_TYPE_SCENPROTECT: + $xls->readScenProtect(); + + break; + case self::XLS_TYPE_OBJECTPROTECT: + $xls->readObjectProtect(); + + break; + case self::XLS_TYPE_PASSWORD: + $xls->readPassword(); + + break; + case self::XLS_TYPE_DEFCOLWIDTH: + $xls->readDefColWidth(); + + break; + case self::XLS_TYPE_COLINFO: + $xls->readColInfo(); + + break; + case self::XLS_TYPE_DIMENSION: + $xls->readDefault(); + + break; + case self::XLS_TYPE_ROW: + $xls->readRow(); + + break; + case self::XLS_TYPE_DBCELL: + $xls->readDefault(); + + break; + case self::XLS_TYPE_RK: + $xls->readRk(); + + break; + case self::XLS_TYPE_LABELSST: + $xls->readLabelSst(); + + break; + case self::XLS_TYPE_MULRK: + $xls->readMulRk(); + + break; + case self::XLS_TYPE_NUMBER: + $xls->readNumber(); + + break; + case self::XLS_TYPE_FORMULA: + $xls->readFormula(); + + break; + case self::XLS_TYPE_SHAREDFMLA: + $xls->readSharedFmla(); + + break; + case self::XLS_TYPE_BOOLERR: + $xls->readBoolErr(); + + break; + case self::XLS_TYPE_MULBLANK: + $xls->readMulBlank(); + + break; + case self::XLS_TYPE_LABEL: + $xls->readLabel(); + + break; + case self::XLS_TYPE_BLANK: + $xls->readBlank(); + + break; + case self::XLS_TYPE_MSODRAWING: + $xls->readMsoDrawing(); + + break; + case self::XLS_TYPE_OBJ: + $xls->readObj(); + + break; + case self::XLS_TYPE_WINDOW2: + $xls->readWindow2(); + + break; + case self::XLS_TYPE_PAGELAYOUTVIEW: + $xls->readPageLayoutView(); + + break; + case self::XLS_TYPE_SCL: + $xls->readScl(); + + break; + case self::XLS_TYPE_PANE: + $xls->readPane(); + + break; + case self::XLS_TYPE_SELECTION: + $selectedCells = $xls->readSelection(); + + break; + case self::XLS_TYPE_MERGEDCELLS: + $xls->readMergedCells(); + + break; + case self::XLS_TYPE_HYPERLINK: + $xls->readHyperLink(); + + break; + case self::XLS_TYPE_DATAVALIDATIONS: + $xls->readDataValidations(); + + break; + case self::XLS_TYPE_DATAVALIDATION: + $xls->readDataValidation(); + + break; + case self::XLS_TYPE_CFHEADER: + $cellRangeAddresses = $xls->readCFHeader(); + + break; + case self::XLS_TYPE_CFRULE: + $xls->readCFRule($cellRangeAddresses ?? []); + + break; + case self::XLS_TYPE_SHEETLAYOUT: + $xls->readSheetLayout(); + + break; + case self::XLS_TYPE_SHEETPROTECTION: + $xls->readSheetProtection(); + + break; + case self::XLS_TYPE_RANGEPROTECTION: + $xls->readRangeProtection(); + + break; + case self::XLS_TYPE_NOTE: + $xls->readNote(); + + break; + case self::XLS_TYPE_TXO: + $xls->readTextObject(); + + break; + case self::XLS_TYPE_CONTINUE: + $xls->readContinue(); + + break; + case self::XLS_TYPE_EOF: + $xls->readDefault(); + + break 2; + default: + $xls->readDefault(); + + break; + } + } + + // treat MSODRAWING records, sheet-level Escher + if (!$xls->readDataOnly && $xls->drawingData) { + $escherWorksheet = new SharedEscher(); + $reader = new Escher($escherWorksheet); + $escherWorksheet = $reader->load($xls->drawingData); + + // get all spContainers in one long array, so they can be mapped to OBJ records + /** @var SpContainer[] $allSpContainers */ + $allSpContainers = method_exists($escherWorksheet, 'getDgContainer') ? $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers() : []; + } + + // treat OBJ records + foreach ($xls->objs as $n => $obj) { + // the first shape container never has a corresponding OBJ record, hence $n + 1 + if (isset($allSpContainers[$n + 1])) { + $spContainer = $allSpContainers[$n + 1]; + + // we skip all spContainers that are a part of a group shape since we cannot yet handle those + if ($spContainer->getNestingLevel() > 1) { + continue; + } + + // calculate the width and height of the shape + /** @var int $startRow */ + [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates()); + /** @var int $endRow */ + [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates()); + + $startOffsetX = $spContainer->getStartOffsetX(); + $startOffsetY = $spContainer->getStartOffsetY(); + $endOffsetX = $spContainer->getEndOffsetX(); + $endOffsetY = $spContainer->getEndOffsetY(); + + $width = SharedXls::getDistanceX($xls->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX); + $height = SharedXls::getDistanceY($xls->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY); + + // calculate offsetX and offsetY of the shape + $offsetX = (int) ($startOffsetX * SharedXls::sizeCol($xls->phpSheet, $startColumn) / 1024); + $offsetY = (int) ($startOffsetY * SharedXls::sizeRow($xls->phpSheet, $startRow) / 256); + + switch ($obj['otObjType']) { + case 0x19: + // Note + if (isset($xls->cellNotes[$obj['idObjID']])) { + //$cellNote = $xls->cellNotes[$obj['idObjID']]; + + if (isset($xls->textObjects[$obj['idObjID']])) { + $textObject = $xls->textObjects[$obj['idObjID']]; + $xls->cellNotes[$obj['idObjID']]['objTextData'] = $textObject; + } + } + + break; + case 0x08: + // picture + // get index to BSE entry (1-based) + $BSEindex = $spContainer->getOPT(0x0104); + + // If there is no BSE Index, we will fail here and other fields are not read. + // Fix by checking here. + // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field? + // More likely : a uncompatible picture + if (!$BSEindex) { + continue 2; + } + + if ($escherWorkbook) { + $BSECollection = method_exists($escherWorkbook, 'getDggContainer') ? $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection() : []; + $BSE = $BSECollection[$BSEindex - 1]; + $blipType = $BSE->getBlipType(); + + // need check because some blip types are not supported by Escher reader such as EMF + if ($blip = $BSE->getBlip()) { + $ih = imagecreatefromstring($blip->getData()); + if ($ih !== false) { + $drawing = new MemoryDrawing(); + $drawing->setImageResource($ih); + + // width, height, offsetX, offsetY + $drawing->setResizeProportional(false); + $drawing->setWidth($width); + $drawing->setHeight($height); + $drawing->setOffsetX($offsetX); + $drawing->setOffsetY($offsetY); + + switch ($blipType) { + case BSE::BLIPTYPE_JPEG: + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG); + + break; + case BSE::BLIPTYPE_PNG: + imagealphablending($ih, false); + imagesavealpha($ih, true); + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG); + + break; + } + + $drawing->setWorksheet($xls->phpSheet); + $drawing->setCoordinates($spContainer->getStartCoordinates()); + } + } + } + + break; + default: + // other object type + break; + } + } + } + + // treat SHAREDFMLA records + if ($xls->version == self::XLS_BIFF8) { + foreach ($xls->sharedFormulaParts as $cell => $baseCell) { + /** @var int $row */ + [$column, $row] = Coordinate::coordinateFromString($cell); + if (($xls->getReadFilter() !== null) && $xls->getReadFilter()->readCell($column, $row, $xls->phpSheet->getTitle())) { + $formula = $xls->getFormulaFromStructure($xls->sharedFormulas[$baseCell], $cell); + $xls->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA); + } + } + } + + if (!empty($xls->cellNotes)) { + foreach ($xls->cellNotes as $note => $noteDetails) { + if (!isset($noteDetails['objTextData'])) { + if (isset($xls->textObjects[$note])) { + $textObject = $xls->textObjects[$note]; + $noteDetails['objTextData'] = $textObject; + } else { + $noteDetails['objTextData']['text'] = ''; + } + } + $cellAddress = str_replace('$', '', $noteDetails['cellRef']); + $xls->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($xls->parseRichText($noteDetails['objTextData']['text'])); + } + } + if ($selectedCells !== '') { + $xls->phpSheet->setSelectedCells($selectedCells); + } + } + if ($xls->activeSheetSet === false) { + $xls->spreadsheet->setActiveSheetIndex(0); + } + + // add the named ranges (defined names) + foreach ($xls->definedname as $definedName) { + if ($definedName['isBuiltInName']) { + switch ($definedName['name']) { + case pack('C', 0x06): + // print area + // in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2 + $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma? + + $extractedRanges = []; + $sheetName = ''; + /** @var non-empty-string $range */ + foreach ($ranges as $range) { + // $range should look like one of these + // Foo!$C$7:$J$66 + // Bar!$A$1:$IV$2 + $explodes = Worksheet::extractSheetTitle($range, true); + $sheetName = trim($explodes[0], "'"); + if (!str_contains($explodes[1], ':')) { + $explodes[1] = $explodes[1] . ':' . $explodes[1]; + } + $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66 + } + if ($docSheet = $xls->spreadsheet->getSheetByName($sheetName)) { + $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2 + } + + break; + case pack('C', 0x07): + // print titles (repeating rows) + // Assuming BIFF8, there are 3 cases + // 1. repeating rows + // formula looks like this: Sheet!$A$1:$IV$2 + // rows 1-2 repeat + // 2. repeating columns + // formula looks like this: Sheet!$A$1:$B$65536 + // columns A-B repeat + // 3. both repeating rows and repeating columns + // formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2 + $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma? + foreach ($ranges as $range) { + // $range should look like this one of these + // Sheet!$A$1:$B$65536 + // Sheet!$A$1:$IV$2 + if (str_contains($range, '!')) { + $explodes = Worksheet::extractSheetTitle($range, true); + if ($docSheet = $xls->spreadsheet->getSheetByName($explodes[0])) { + $extractedRange = $explodes[1]; + $extractedRange = str_replace('$', '', $extractedRange); + + $coordinateStrings = explode(':', $extractedRange); + if (count($coordinateStrings) == 2) { + [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]); + [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]); + + if ($firstColumn == 'A' && $lastColumn == 'IV') { + // then we have repeating rows + $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]); + } elseif ($firstRow == 1 && $lastRow == 65536) { + // then we have repeating columns + $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]); + } + } + } + } + } + + break; + } + } else { + // Extract range + /** @var non-empty-string $formula */ + $formula = $definedName['formula']; + if (str_contains($formula, '!')) { + $explodes = Worksheet::extractSheetTitle($formula, true); + if ( + ($docSheet = $xls->spreadsheet->getSheetByName($explodes[0])) + || ($docSheet = $xls->spreadsheet->getSheetByName(trim($explodes[0], "'"))) + ) { + $extractedRange = $explodes[1]; + + $localOnly = ($definedName['scope'] === 0) ? false : true; + + $scope = ($definedName['scope'] === 0) ? null : $xls->spreadsheet->getSheetByName($xls->sheets[$definedName['scope'] - 1]['name']); + + $xls->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope)); + } + } + // Named Value + // TODO Provide support for named values + } + } + $xls->data = ''; + + return $xls->spreadsheet; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xls/Mappings.php b/src/PhpSpreadsheet/Reader/Xls/Mappings.php new file mode 100644 index 000000000..7a6be8e86 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/Mappings.php @@ -0,0 +1,271 @@ + ['ISNA', 1], + 3 => ['ISERROR', 1], + 10 => ['NA', 0], + 15 => ['SIN', 1], + 16 => ['COS', 1], + 17 => ['TAN', 1], + 18 => ['ATAN', 1], + 19 => ['PI', 0], + 20 => ['SQRT', 1], + 21 => ['EXP', 1], + 22 => ['LN', 1], + 23 => ['LOG10', 1], + 24 => ['ABS', 1], + 25 => ['INT', 1], + 26 => ['SIGN', 1], + 27 => ['ROUND', 2], + 30 => ['REPT', 2], + 31 => ['MID', 3], + 32 => ['LEN', 1], + 33 => ['VALUE', 1], + 34 => ['TRUE', 0], + 35 => ['FALSE', 0], + 38 => ['NOT', 1], + 39 => ['MOD', 2], + 40 => ['DCOUNT', 3], + 41 => ['DSUM', 3], + 42 => ['DAVERAGE', 3], + 43 => ['DMIN', 3], + 44 => ['DMAX', 3], + 45 => ['DSTDEV', 3], + 48 => ['TEXT', 2], + 61 => ['MIRR', 3], + 63 => ['RAND', 0], + 65 => ['DATE', 3], + 66 => ['TIME', 3], + 67 => ['DAY', 1], + 68 => ['MONTH', 1], + 69 => ['YEAR', 1], + 71 => ['HOUR', 1], + 72 => ['MINUTE', 1], + 73 => ['SECOND', 1], + 74 => ['NOW', 0], + 75 => ['AREAS', 1], + 76 => ['ROWS', 1], + 77 => ['COLUMNS', 1], + 83 => ['TRANSPOSE', 1], + 86 => ['TYPE', 1], + 97 => ['ATAN2', 2], + 98 => ['ASIN', 1], + 99 => ['ACOS', 1], + 105 => ['ISREF', 1], + 111 => ['CHAR', 1], + 112 => ['LOWER', 1], + 113 => ['UPPER', 1], + 114 => ['PROPER', 1], + 117 => ['EXACT', 2], + 118 => ['TRIM', 1], + 119 => ['REPLACE', 4], + 121 => ['CODE', 1], + 126 => ['ISERR', 1], + 127 => ['ISTEXT', 1], + 128 => ['ISNUMBER', 1], + 129 => ['ISBLANK', 1], + 130 => ['T', 1], + 131 => ['N', 1], + 140 => ['DATEVALUE', 1], + 141 => ['TIMEVALUE', 1], + 142 => ['SLN', 3], + 143 => ['SYD', 4], + 162 => ['CLEAN', 1], + 163 => ['MDETERM', 1], + 164 => ['MINVERSE', 1], + 165 => ['MMULT', 2], + 184 => ['FACT', 1], + 189 => ['DPRODUCT', 3], + 190 => ['ISNONTEXT', 1], + 195 => ['DSTDEVP', 3], + 196 => ['DVARP', 3], + 198 => ['ISLOGICAL', 1], + 199 => ['DCOUNTA', 3], + 207 => ['REPLACEB', 4], + 210 => ['MIDB', 3], + 211 => ['LENB', 1], + 212 => ['ROUNDUP', 2], + 213 => ['ROUNDDOWN', 2], + 214 => ['ASC', 1], + 215 => ['DBCS', 1], + 221 => ['TODAY', 0], + 229 => ['SINH', 1], + 230 => ['COSH', 1], + 231 => ['TANH', 1], + 232 => ['ASINH', 1], + 233 => ['ACOSH', 1], + 234 => ['ATANH', 1], + 235 => ['DGET', 3], + 244 => ['INFO', 1], + 252 => ['FREQUENCY', 2], + 261 => ['ERROR.TYPE', 1], + 271 => ['GAMMALN', 1], + 273 => ['BINOMDIST', 4], + 274 => ['CHIDIST', 2], + 275 => ['CHIINV', 2], + 276 => ['COMBIN', 2], + 277 => ['CONFIDENCE', 3], + 278 => ['CRITBINOM', 3], + 279 => ['EVEN', 1], + 280 => ['EXPONDIST', 3], + 281 => ['FDIST', 3], + 282 => ['FINV', 3], + 283 => ['FISHER', 1], + 284 => ['FISHERINV', 1], + 285 => ['FLOOR', 2], + 286 => ['GAMMADIST', 4], + 287 => ['GAMMAINV', 3], + 288 => ['CEILING', 2], + 289 => ['HYPGEOMDIST', 4], + 290 => ['LOGNORMDIST', 3], + 291 => ['LOGINV', 3], + 292 => ['NEGBINOMDIST', 3], + 293 => ['NORMDIST', 4], + 294 => ['NORMSDIST', 1], + 295 => ['NORMINV', 3], + 296 => ['NORMSINV', 1], + 297 => ['STANDARDIZE', 3], + 298 => ['ODD', 1], + 299 => ['PERMUT', 2], + 300 => ['POISSON', 3], + 301 => ['TDIST', 3], + 302 => ['WEIBULL', 4], + 303 => ['SUMXMY2', 2], + 304 => ['SUMX2MY2', 2], + 305 => ['SUMX2PY2', 2], + 306 => ['CHITEST', 2], + 307 => ['CORREL', 2], + 308 => ['COVAR', 2], + 309 => ['FORECAST', 3], + 310 => ['FTEST', 2], + 311 => ['INTERCEPT', 2], + 312 => ['PEARSON', 2], + 313 => ['RSQ', 2], + 314 => ['STEYX', 2], + 315 => ['SLOPE', 2], + 316 => ['TTEST', 4], + 325 => ['LARGE', 2], + 326 => ['SMALL', 2], + 327 => ['QUARTILE', 2], + 328 => ['PERCENTILE', 2], + 331 => ['TRIMMEAN', 2], + 332 => ['TINV', 2], + 337 => ['POWER', 2], + 342 => ['RADIANS', 1], + 343 => ['DEGREES', 1], + 346 => ['COUNTIF', 2], + 347 => ['COUNTBLANK', 1], + 350 => ['ISPMT', 4], + 351 => ['DATEDIF', 3], + 352 => ['DATESTRING', 1], + 353 => ['NUMBERSTRING', 2], + 360 => ['PHONETIC', 1], + 368 => ['BAHTTEXT', 1], + ]; + + /** + * Map tFuncV values (functions with variable number of arguments). + * Key is tFuncV value. + * Value is Excel function name. + */ + const TFUNCV_MAPPINGS = [ + 0 => 'COUNT', + 1 => 'IF', + 4 => 'SUM', + 5 => 'AVERAGE', + 6 => 'MIN', + 7 => 'MAX', + 8 => 'ROW', + 9 => 'COLUMN', + 11 => 'NPV', + 12 => 'STDEV', + 13 => 'DOLLAR', + 14 => 'FIXED', + 28 => 'LOOKUP', + 29 => 'INDEX', + 36 => 'AND', + 37 => 'OR', + 46 => 'VAR', + 49 => 'LINEST', + 50 => 'TREND', + 51 => 'LOGEST', + 52 => 'GROWTH', + 56 => 'PV', + 57 => 'FV', + 58 => 'NPER', + 59 => 'PMT', + 60 => 'RATE', + 62 => 'IRR', + 64 => 'MATCH', + 70 => 'WEEKDAY', + 78 => 'OFFSET', + 82 => 'SEARCH', + 100 => 'CHOOSE', + 101 => 'HLOOKUP', + 102 => 'VLOOKUP', + 109 => 'LOG', + 115 => 'LEFT', + 116 => 'RIGHT', + 120 => 'SUBSTITUTE', + 124 => 'FIND', + 125 => 'CELL', + 144 => 'DDB', + 148 => 'INDIRECT', + 167 => 'IPMT', + 168 => 'PPMT', + 169 => 'COUNTA', + 183 => 'PRODUCT', + 193 => 'STDEVP', + 194 => 'VARP', + 197 => 'TRUNC', + 204 => 'USDOLLAR', + 205 => 'FINDB', + 206 => 'SEARCHB', + 208 => 'LEFTB', + 209 => 'RIGHTB', + 216 => 'RANK', + 219 => 'ADDRESS', + 220 => 'DAYS360', + 222 => 'VDB', + 227 => 'MEDIAN', + 228 => 'SUMPRODUCT', + 247 => 'DB', + 255 => '', + 269 => 'AVEDEV', + 270 => 'BETADIST', + 272 => 'BETAINV', + 317 => 'PROB', + 318 => 'DEVSQ', + 319 => 'GEOMEAN', + 320 => 'HARMEAN', + 321 => 'SUMSQ', + 322 => 'KURT', + 323 => 'SKEW', + 324 => 'ZTEST', + 329 => 'PERCENTRANK', + 330 => 'MODE', + 336 => 'CONCATENATE', + 344 => 'SUBTOTAL', + 345 => 'SUMIF', + 354 => 'ROMAN', + 358 => 'GETPIVOTDATA', + 359 => 'HYPERLINK', + 361 => 'AVERAGEA', + 362 => 'MAXA', + 363 => 'MINA', + 364 => 'STDEVPA', + 365 => 'VARPA', + 366 => 'STDEVA', + 367 => 'VARA', + ]; +} diff --git a/src/PhpSpreadsheet/Reader/XlsBase.php b/src/PhpSpreadsheet/Reader/XlsBase.php new file mode 100644 index 000000000..e6969db04 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/XlsBase.php @@ -0,0 +1,397 @@ + 0x00, + Border::BORDER_THIN, // => 0x01, + Border::BORDER_MEDIUM, // => 0x02, + Border::BORDER_DASHED, // => 0x03, + Border::BORDER_DOTTED, // => 0x04, + Border::BORDER_THICK, // => 0x05, + Border::BORDER_DOUBLE, // => 0x06, + Border::BORDER_HAIR, // => 0x07, + Border::BORDER_MEDIUMDASHED, // => 0x08, + Border::BORDER_DASHDOT, // => 0x09, + Border::BORDER_MEDIUMDASHDOT, // => 0x0A, + Border::BORDER_DASHDOTDOT, // => 0x0B, + Border::BORDER_MEDIUMDASHDOTDOT, // => 0x0C, + Border::BORDER_SLANTDASHDOT, // => 0x0D, + Border::BORDER_OMIT, // => 0x0E, + Border::BORDER_OMIT, // => 0x0F, + ]; + + /** + * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95) + * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'. + */ + protected string $codepage = ''; + + public function setCodepage(string $codepage): void + { + if (CodePage::validate($codepage) === false) { + throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage); + } + + $this->codepage = $codepage; + } + + public function getCodepage(): string + { + return $this->codepage; + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + if (File::testFileNoThrow($filename) === false) { + return false; + } + + try { + // Use ParseXL for the hard work. + $ole = new OLERead(); + + // get excel data + $ole->read($filename); + if ($ole->wrkbook === null) { + throw new Exception('The filename ' . $filename . ' is not recognised as a Spreadsheet file'); + } + + return true; + } catch (PhpSpreadsheetException) { + return false; + } + } + + /** + * Extract RGB color + * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4. + * + * @param string $rgb Encoded RGB value (4 bytes) + */ + protected static function readRGB(string $rgb): array + { + // offset: 0; size 1; Red component + $r = ord($rgb[0]); + + // offset: 1; size: 1; Green component + $g = ord($rgb[1]); + + // offset: 2; size: 1; Blue component + $b = ord($rgb[2]); + + // HEX notation, e.g. 'FF00FC' + $rgb = sprintf('%02X%02X%02X', $r, $g, $b); + + return ['rgb' => $rgb]; + } + + /** + * Extracts an Excel Unicode short string (8-bit string length) + * OpenOffice documentation: 2.5.3 + * function will automatically find out where the Unicode string ends. + */ + protected static function readUnicodeStringShort(string $subData): array + { + // offset: 0: size: 1; length of the string (character count) + $characterCount = ord($subData[0]); + + $string = self::readUnicodeString(substr($subData, 1), $characterCount); + + // add 1 for the string length + ++$string['size']; + + return $string; + } + + /** + * Extracts an Excel Unicode long string (16-bit string length) + * OpenOffice documentation: 2.5.3 + * this function is under construction, needs to support rich text, and Asian phonetic settings. + */ + protected static function readUnicodeStringLong(string $subData): array + { + // offset: 0: size: 2; length of the string (character count) + $characterCount = self::getUInt2d($subData, 0); + + $string = self::readUnicodeString(substr($subData, 2), $characterCount); + + // add 2 for the string length + $string['size'] += 2; + + return $string; + } + + /** + * Read Unicode string with no string length field, but with known character count + * this function is under construction, needs to support rich text, and Asian phonetic settings + * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3. + */ + protected static function readUnicodeString(string $subData, int $characterCount): array + { + // offset: 0: size: 1; option flags + // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit) + $isCompressed = !((0x01 & ord($subData[0])) >> 0); + + // bit: 2; mask: 0x04; Asian phonetic settings + //$hasAsian = (0x04) & ord($subData[0]) >> 2; + + // bit: 3; mask: 0x08; Rich-Text settings + //$hasRichText = (0x08) & ord($subData[0]) >> 3; + + // offset: 1: size: var; character array + // this offset assumes richtext and Asian phonetic settings are off which is generally wrong + // needs to be fixed + $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed); + + return [ + 'value' => $value, + 'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags + ]; + } + + /** + * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas. + * Example: hello"world --> "hello""world". + * + * @param string $value UTF-8 encoded string + */ + protected static function UTF8toExcelDoubleQuoted(string $value): string + { + return '"' . str_replace('"', '""', $value) . '"'; + } + + /** + * Reads first 8 bytes of a string and return IEEE 754 float. + * + * @param string $data Binary string that is at least 8 bytes long + */ + protected static function extractNumber(string $data): int|float + { + $rknumhigh = self::getInt4d($data, 4); + $rknumlow = self::getInt4d($data, 0); + $sign = ($rknumhigh & self::HIGH_ORDER_BIT) >> 31; + $exp = (($rknumhigh & 0x7FF00000) >> 20) - 1023; + $mantissa = (0x100000 | ($rknumhigh & 0x000FFFFF)); + $mantissalow1 = ($rknumlow & self::HIGH_ORDER_BIT) >> 31; + $mantissalow2 = ($rknumlow & 0x7FFFFFFF); + $value = $mantissa / 2 ** (20 - $exp); + + if ($mantissalow1 != 0) { + $value += 1 / 2 ** (21 - $exp); + } + + if ($mantissalow2 != 0) { + $value += $mantissalow2 / 2 ** (52 - $exp); + } + if ($sign) { + $value *= -1; + } + + return $value; + } + + protected static function getIEEE754(int $rknum): float|int + { + if (($rknum & 0x02) != 0) { + $value = $rknum >> 2; + } else { + // changes by mmp, info on IEEE754 encoding from + // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html + // The RK format calls for using only the most significant 30 bits + // of the 64 bit floating point value. The other 34 bits are assumed + // to be 0 so we use the upper 30 bits of $rknum as follows... + $sign = ($rknum & self::HIGH_ORDER_BIT) >> 31; + $exp = ($rknum & 0x7FF00000) >> 20; + $mantissa = (0x100000 | ($rknum & 0x000FFFFC)); + $value = $mantissa / 2 ** (20 - ($exp - 1023)); + if ($sign) { + $value = -1 * $value; + } + //end of changes by mmp + } + if (($rknum & 0x01) != 0) { + $value /= 100; + } + + return $value; + } + + /** + * Get UTF-8 string from (compressed or uncompressed) UTF-16 string. + */ + protected static function encodeUTF16(string $string, bool $compressed = false): string + { + if ($compressed) { + $string = self::uncompressByteString($string); + } + + return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE'); + } + + /** + * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8. + */ + protected static function uncompressByteString(string $string): string + { + $uncompressedString = ''; + $strLen = strlen($string); + for ($i = 0; $i < $strLen; ++$i) { + $uncompressedString .= $string[$i] . "\0"; + } + + return $uncompressedString; + } + + /** + * Convert string to UTF-8. Only used for BIFF5. + */ + protected function decodeCodepage(string $string): string + { + return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage); + } + + /** + * Read 16-bit unsigned integer. + */ + public static function getUInt2d(string $data, int $pos): int + { + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8); + } + + /** + * Read 16-bit signed integer. + */ + public static function getInt2d(string $data, int $pos): int + { + return unpack('s', $data[$pos] . $data[$pos + 1])[1]; // @phpstan-ignore-line + } + + /** + * Read 32-bit signed integer. + */ + public static function getInt4d(string $data, int $pos): int + { + // FIX: represent numbers correctly on 64-bit system + // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 + // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems + $_or_24 = ord($data[$pos + 3]); + if ($_or_24 >= 128) { + // negative number + $_ord_24 = -abs((256 - $_or_24) << 24); + } else { + $_ord_24 = ($_or_24 & 127) << 24; + } + + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 53cc1b53d..88d59634c 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -41,6 +41,7 @@ use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; +use Stringable; use Throwable; use XMLReader; use ZipArchive; @@ -230,7 +231,6 @@ class Xlsx extends BaseReader if ($xmlWorkbook->sheets) { $dir = dirname($relTarget); - /** @var SimpleXMLElement $eleSheet */ foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { $tmpInfo = [ 'worksheetName' => (string) self::getAttributes($eleSheet)['name'], @@ -240,7 +240,7 @@ class Xlsx extends BaseReader 'totalColumns' => 0, ]; - $fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $namespace), 'id')]; + $fileWorksheet = (string) $worksheets[self::getArrayItemString(self::getAttributes($eleSheet, $namespace), 'id')]; $fileWorksheetPath = str_starts_with($fileWorksheet, '/') ? substr($fileWorksheet, 1) : "$dir/$fileWorksheet"; $xml = new XMLReader(); @@ -304,6 +304,11 @@ class Xlsx extends BaseReader return isset($c, $c->v) ? (string) $c->v : null; } + public static function replacePrefixes(string $formula): string + { + return str_replace(['_xlfn.', '_xlws.'], '', $formula); + } + private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, mixed &$value, mixed &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void { if ($c === null) { @@ -311,8 +316,7 @@ class Xlsx extends BaseReader } $attr = $c->f->attributes(); $cellDataType = DataType::TYPE_FORMULA; - $formula = (string) $c->f; - $formula = str_replace(['_xlfn.', '_xlws.'], '', $formula); + $formula = self::replacePrefixes((string) $c->f); $value = "=$formula"; $calculatedValue = self::$castBaseType($c); @@ -395,6 +399,7 @@ class Xlsx extends BaseReader // Initialisations $excel = new Spreadsheet(); + $excel->setValueBinder($this->valueBinder); $excel->removeSheetByIndex(0); $addingFirstCellStyleXf = true; $addingFirstCellXf = true; @@ -569,6 +574,7 @@ class Xlsx extends BaseReader $relType = "rel:Relationship[@Type='" . "$xmlNamespaceBase/styles" . "']"; + /** @var ?SimpleXMLElement */ $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType)); if ($xpath === null) { @@ -604,6 +610,7 @@ class Xlsx extends BaseReader if ($xf['numFmtId']) { if (isset($numFmts)) { + /** @var ?SimpleXMLElement */ $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); if (isset($tmpNumFmt['formatCode'])) { @@ -649,6 +656,7 @@ class Xlsx extends BaseReader $xf = self::getAttributes($xfTag); $numFmt = NumberFormat::FORMAT_GENERAL; if ($numFmts && $xf['numFmtId']) { + /** @var ?SimpleXMLElement */ $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); if (isset($tmpNumFmt['formatCode'])) { $numFmt = (string) $tmpNumFmt['formatCode']; @@ -692,6 +700,7 @@ class Xlsx extends BaseReader //. Namespaces::SHARED_STRINGS . "$xmlNamespaceBase/sharedStrings" . "']"; + /** @var ?SimpleXMLElement */ $xpath = self::getArrayItem($relsWorkbook->xpath($relType)); if ($xpath) { @@ -738,7 +747,6 @@ class Xlsx extends BaseReader $charts = $chartDetails = []; if ($xmlWorkbookNS->sheets) { - /** @var SimpleXMLElement $eleSheet */ foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) { $eleSheetAttr = self::getAttributes($eleSheet); ++$oldSheetId; @@ -751,7 +759,7 @@ class Xlsx extends BaseReader continue; } - $sheetReferenceId = (string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id'); + $sheetReferenceId = self::getArrayItemString(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id'); if (isset($worksheets[$sheetReferenceId]) === false) { ++$countSkippedSheets; $mapSheetId[$oldSheetId] = null; @@ -911,6 +919,7 @@ class Xlsx extends BaseReader $value = self::castToString($c); if (is_numeric($value)) { $value += 0; + $cellDataType = DataType::TYPE_NUMERIC; } } else { // Formula @@ -1377,7 +1386,7 @@ class Xlsx extends BaseReader $unparsedDrawings = []; $fileDrawing = null; foreach ($xmlSheetNS->drawing as $drawing) { - $drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id'); + $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id'); $fileDrawing = $drawings[$drawingRelId]; $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels'; $relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS); @@ -1423,17 +1432,20 @@ class Xlsx extends BaseReader foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) { $oneCellAnchor = self::testSimpleXml($oneCellAnchor); if ($oneCellAnchor->pic->blipFill) { - /** @var SimpleXMLElement $blip */ + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; - /** @var SimpleXMLElement $xfrm */ + if (isset($blip, $blip->alphaModFix)) { + $temp = (string) $blip->alphaModFix->attributes()->amt; + if (is_numeric($temp)) { + $objDrawing->setOpacity((int) $temp); + } + } $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; - /** @var SimpleXMLElement $outerShdw */ $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; - $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); - $objDrawing->setName((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name')); - $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr')); - $embedImageKey = (string) self::getArrayItem( + $objDrawing->setName(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name')); + $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr')); + $embedImageKey = self::getArrayItemString( self::getAttributes($blip, $xmlNamespaceBase), 'embed' ); @@ -1445,7 +1457,7 @@ class Xlsx extends BaseReader $zip ); } else { - $linkImageKey = (string) self::getArrayItem( + $linkImageKey = self::getArrayItemString( $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'link' ); @@ -1462,22 +1474,22 @@ class Xlsx extends BaseReader $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff)); $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff)); $objDrawing->setResizeProportional(false); - $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cx'))); - $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy'))); + $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx'))); + $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy'))); if ($xfrm) { - $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot'))); $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); } if ($outerShdw) { $shadow = $objDrawing->getShadow(); $shadow->setVisible(true); - $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'blurRad'))); - $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'dist'))); - $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($outerShdw), 'dir'))); - $shadow->setAlignment((string) self::getArrayItem(self::getAttributes($outerShdw), 'algn')); + $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad'))); + $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist'))); + $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir'))); + $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn')); $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr; - $shadow->getColor()->setRGB(self::getArrayItem(self::getAttributes($clr), 'val')); + $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val')); $shadow->setAlpha(self::getArrayItem(self::getAttributes($clr->alpha), 'val') / 1000); } @@ -1489,11 +1501,10 @@ class Xlsx extends BaseReader $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1); $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff); $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff); - $width = Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cx')); - $height = Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy')); + $width = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx')); + $height = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy')); $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; - /** @var SimpleXMLElement $chartRef */ $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); @@ -1515,6 +1526,12 @@ class Xlsx extends BaseReader if ($twoCellAnchor->pic->blipFill) { $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; + if (isset($blip, $blip->alphaModFix)) { + $temp = (string) $blip->alphaModFix->attributes()->amt; + if (is_numeric($temp)) { + $objDrawing->setOpacity((int) $temp); + } + } if (isset($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect)) { $objDrawing->setSrcRect($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect->attributes()); } @@ -1524,9 +1541,9 @@ class Xlsx extends BaseReader if (isset($editAs, $editAs['editAs'])) { $objDrawing->setEditAs($editAs['editAs']); } - $objDrawing->setName((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name')); - $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr')); - $embedImageKey = (string) self::getArrayItem( + $objDrawing->setName((string) self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name')); + $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr')); + $embedImageKey = self::getArrayItemString( self::getAttributes($blip, $xmlNamespaceBase), 'embed' ); @@ -1538,7 +1555,7 @@ class Xlsx extends BaseReader $zip ); } else { - $linkImageKey = (string) self::getArrayItem( + $linkImageKey = self::getArrayItemString( $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'link' ); @@ -1563,21 +1580,21 @@ class Xlsx extends BaseReader $objDrawing->setResizeProportional(false); if ($xfrm) { - $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cx'))); - $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cy'))); - $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cx'))); + $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cy'))); + $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot'))); $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); } if ($outerShdw) { $shadow = $objDrawing->getShadow(); $shadow->setVisible(true); - $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'blurRad'))); - $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'dist'))); - $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($outerShdw), 'dir'))); - $shadow->setAlignment((string) self::getArrayItem(self::getAttributes($outerShdw), 'algn')); + $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad'))); + $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist'))); + $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir'))); + $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn')); $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr; - $shadow->getColor()->setRGB(self::getArrayItem(self::getAttributes($clr), 'val')); + $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val')); $shadow->setAlpha(self::getArrayItem(self::getAttributes($clr->alpha), 'val') / 1000); } @@ -1592,7 +1609,6 @@ class Xlsx extends BaseReader $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff); $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff); $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; - /** @var SimpleXMLElement $chartRef */ $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); @@ -1612,11 +1628,10 @@ class Xlsx extends BaseReader 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]); + $width = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cx')[0]); + $height = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cy')[0]); $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ 'fromCoordinate' => 'A1', @@ -1649,7 +1664,7 @@ class Xlsx extends BaseReader } if ($xmlSheet->legacyDrawing && !$this->readDataOnly) { foreach ($xmlSheet->legacyDrawing as $drawing) { - $drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id'); + $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id'); if (isset($vmlDrawingContents[$drawingRelId])) { if (self::onlyNoteVml($vmlDrawingContents[$drawingRelId]) === false) { $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId]; @@ -1895,7 +1910,6 @@ class Xlsx extends BaseReader $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } elseif ($is !== null) { if (is_object($is->r)) { - /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); @@ -2020,6 +2034,20 @@ class Xlsx extends BaseReader return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null); } + private static function getArrayItemString(null|array|bool|SimpleXMLElement $array, int|string $key = 0): string + { + $retVal = self::getArrayItem($array, $key); + + return ($retVal === null || is_scalar($retVal) || $retVal instanceof Stringable) ? ((string) $retVal) : ''; + } + + private static function getArrayItemIntOrSxml(null|array|bool|SimpleXMLElement $array, int|string $key = 0): int|SimpleXMLElement + { + $retVal = self::getArrayItem($array, $key); + + return (is_int($retVal) || $retVal instanceof SimpleXMLElement) ? $retVal : 0; + } + private static function dirAdd(null|SimpleXMLElement|string $base, null|SimpleXMLElement|string $add): string { $base = (string) $base; @@ -2084,7 +2112,7 @@ class Xlsx extends BaseReader $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id']; $hyperlink = new Hyperlink( $hyperlinks[$hlinkId], - (string) self::getArrayItem(self::getAttributes($cellAnchor->pic->nvPicPr->cNvPr), 'name') + self::getArrayItemString(self::getAttributes($cellAnchor->pic->nvPicPr->cNvPr), 'name') ); $objDrawing->setHyperlink($hyperlink); } @@ -2250,7 +2278,7 @@ class Xlsx extends BaseReader string $relsName ): void { if ($xmlSheet && $xmlSheet->picture) { - $id = (string) self::getArrayItem(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id'); + $id = (string) self::getArrayItemString(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id'); $rels = $this->loadZip($relsName); foreach ($rels->Relationship as $rel) { $attrs = $rel->attributes() ?? []; @@ -2359,8 +2387,9 @@ class Xlsx extends BaseReader $firstRow = $matches[2]; $firstCol = $matches[1]; if (array_key_exists(3, $matches)) { - $lastCol = $matches[4]; - $lastRow = $matches[5]; + // https://github.com/phpstan/phpstan/issues/11602 + $lastCol = $matches[4]; // @phpstan-ignore-line + $lastRow = $matches[5]; // @phpstan-ignore-line } else { $lastCol = $firstCol; $lastRow = $firstRow; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 94d9c6af5..beca090da 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -95,6 +95,7 @@ class Chart $gapWidth = null; $useUpBars = null; $useDownBars = null; + $noBorder = false; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'spPr': @@ -108,6 +109,9 @@ class Chart if (isset($children->ln)) { $chartBorderLines = new GridLines(); $this->readLineStyle($chartElementsC, $chartBorderLines); + if (isset($children->ln->noFill)) { + $noBorder = true; + } } break; @@ -470,6 +474,7 @@ class Chart if ($chartBorderLines !== null) { $chart->setBorderLines($chartBorderLines); } + $chart->setNoBorder($noBorder); $chart->setRoundedCorners($roundedCorners); if (is_bool($autoTitleDeleted)) { $chart->setAutoTitleDeleted($autoTitleDeleted); @@ -1187,6 +1192,9 @@ class Chart } $fontArray = []; $fontArray['size'] = self::getAttributeInteger($titleDetailPart->pPr->defRPr, 'sz'); + if ($fontArray['size'] !== null && $fontArray['size'] >= 100) { + $fontArray['size'] /= 100.0; + } $fontArray['bold'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'b'); $fontArray['italic'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i'); $fontArray['underscore'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u'); @@ -1300,6 +1308,10 @@ class Chart case 'showLeaderLines': $plotArea->setShowLeaderLines($plotAttributeValue); + break; + case 'labelFont': + $plotArea->setLabelFont($plotAttributeValue); + break; } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index d494dc9da..d519002a8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; @@ -55,8 +56,8 @@ class DataValidations $docValidation->setError((string) $dataValidation['error']); $docValidation->setPromptTitle((string) $dataValidation['promptTitle']); $docValidation->setPrompt((string) $dataValidation['prompt']); - $docValidation->setFormula1((string) $dataValidation->formula1); - $docValidation->setFormula2((string) $dataValidation->formula2); + $docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1)); + $docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2)); $docValidation->setSqref($range); } } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index e0f218747..b712321a8 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -18,7 +18,6 @@ use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -44,6 +43,18 @@ class Xml extends BaseReader { parent::__construct(); $this->securityScanner = XmlScanner::getInstance($this); + /** @var callable */ + $unentity = [self::class, 'unentity']; + $this->securityScanner->setAdditionalCallback($unentity); + } + + public static function unentity(string $contents): string + { + $contents = preg_replace('/&(amp|lt|gt|quot|apos);/', "\u{fffe}\u{feff}\$1;", trim($contents)) ?? $contents; + $contents = html_entity_decode($contents, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8'); + $contents = str_replace("\u{fffe}\u{feff}", '&', $contents); + + return $contents; } private string $fileContents = ''; @@ -79,7 +90,8 @@ class Xml extends BaseReader ]; // Open file - $data = file_get_contents($filename) ?: ''; + $data = (string) file_get_contents($filename); + $data = $this->getSecurityScannerOrThrow()->scan($data); // Why? //$data = str_replace("'", '"', $data); // fix headers with single quote @@ -94,14 +106,6 @@ class Xml extends BaseReader } } - // Retrieve charset encoding - if (preg_match('//m', $data, $matches)) { - $charSet = strtoupper($matches[1]); - if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) { - $data = StringHelper::convertEncoding($data, 'UTF-8', $charSet); - $data = (string) preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); - } - } $this->fileContents = $data; return $valid; @@ -242,6 +246,7 @@ class Xml extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); $spreadsheet->removeSheetByIndex(0); // Load into this instance @@ -255,6 +260,7 @@ class Xml extends BaseReader { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); $spreadsheet->removeSheetByIndex(0); // Load into this instance @@ -512,9 +518,6 @@ class Xml extends BaseReader if (isset($cell_ss['StyleID'])) { $style = (string) $cell_ss['StyleID']; if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) { - //if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) { - // $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null); - //} $spreadsheet->getActiveSheet()->getStyle($cellRange) ->applyFromArray($this->styles[$style]); } diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index cdbe37861..f139a5d53 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -759,7 +759,7 @@ class ReferenceHelper $column = $columns[$splitCount][0]; $row = $rows[$splitCount][0]; - if (!empty($column) && $column[0] !== '$') { + if ($column[0] !== '$') { $column = ((Coordinate::columnIndexFromString($column) + $numberOfColumns) % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT; $column = Coordinate::stringFromColumnIndex($column); $rowOffset -= ($columnLength - strlen($column)); diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index c0a3a9bc2..6ec75c33c 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet; use JsonSerializable; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Document\Security; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; @@ -173,6 +174,8 @@ class Spreadsheet implements JsonSerializable private Theme $theme; + private ?IValueBinder $valueBinder = null; + public function getTheme(): Theme { return $this->theme; @@ -1591,4 +1594,16 @@ class Spreadsheet implements JsonSerializable { return $this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'] ?? null; } + + public function getValueBinder(): ?IValueBinder + { + return $this->valueBinder; + } + + public function setValueBinder(?IValueBinder $valueBinder): self + { + $this->valueBinder = $valueBinder; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index b96cfca60..7d70dc4f7 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -20,10 +20,12 @@ class Font extends Supervisor protected ?string $cap = null; + public const DEFAULT_FONT_NAME = 'Calibri'; + /** * Font Name. */ - protected ?string $name = 'Calibri'; + protected ?string $name = self::DEFAULT_FONT_NAME; /** * The following 7 are used only for chart titles, I think. diff --git a/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php index ff80aa2f5..86d2c99b8 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -25,9 +25,9 @@ class FractionFormatter extends BaseFormatter $decimalDivisor = 10 ** $decimalLength; preg_match('/(#?.*\?)\/(\?+|\d+)/', $format, $matches); - $formatIntegerPart = $matches[1]; + $formatIntegerPart = $matches[1] ?? '0'; - if (is_numeric($matches[2])) { + if (isset($matches[2]) && is_numeric($matches[2])) { $fractionDivisor = 100 / (int) $matches[2]; } else { /** @var float $fractionDivisor */ diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Wizard/NumberBase.php b/src/PhpSpreadsheet/Style/NumberFormat/Wizard/NumberBase.php index 53f6b3dd8..52a765410 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Wizard/NumberBase.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Wizard/NumberBase.php @@ -60,7 +60,7 @@ abstract class NumberBase implements Stringable ['language' => $language, 'script' => $script, 'country' => $country] = $matches; // Set case and separator to match standardised locale case - $language = strtolower($language ?? ''); + $language = strtolower($language); $script = ($script === null) ? null : ucfirst(strtolower($script)); $country = ($country === null) ? null : strtoupper($country); diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 50323dbca..abf8c7afb 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -131,6 +131,12 @@ class BaseDrawing implements IComparable /** @var null|SimpleXMLElement|string[] */ protected $srcRect = []; + /** + * Percentage multiplied by 100,000, e.g. 40% = 40,000. + * Opacity=x is the same as transparency=100000-x. + */ + protected ?int $opacity = null; + /** * Create a new BaseDrawing. */ @@ -344,9 +350,12 @@ class BaseDrawing implements IComparable */ public function setWidthAndHeight(int $width, int $height): self { - $xratio = $width / ($this->width != 0 ? $this->width : 1); - $yratio = $height / ($this->height != 0 ? $this->height : 1); - if ($this->resizeProportional && !($width == 0 || $height == 0)) { + if ($this->width === 0 || $this->height === 0 || $width === 0 || $height === 0 || !$this->resizeProportional) { + $this->width = $width; + $this->height = $height; + } else { + $xratio = $width / $this->width; + $yratio = $height / $this->height; if (($xratio * $this->height) < $height) { $this->height = (int) ceil($xratio * $this->height); $this->width = $width; @@ -354,9 +363,6 @@ class BaseDrawing implements IComparable $this->width = (int) ceil($yratio * $this->width); $this->height = $height; } - } else { - $this->width = $width; - $this->height = $height; } return $this; @@ -546,4 +552,16 @@ class BaseDrawing implements IComparable { return $this->flipVertical; } + + public function setOpacity(?int $opacity): self + { + $this->opacity = $opacity; + + return $this; + } + + public function getOpacity(): ?int + { + return $this->opacity; + } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 9f01e1697..80339a8ea 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2784,23 +2784,30 @@ class Worksheet implements IComparable [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell); // Loop through $source - foreach ($source as $rowData) { - $currentColumn = $startColumn; - foreach ($rowData as $cellValue) { - if ($strictNullComparison) { + if ($strictNullComparison) { + foreach ($source as $rowData) { + $currentColumn = $startColumn; + foreach ($rowData as $cellValue) { if ($cellValue !== $nullValue) { // Set cell value $this->getCell($currentColumn . $startRow)->setValue($cellValue); } - } else { + ++$currentColumn; + } + ++$startRow; + } + } else { + foreach ($source as $rowData) { + $currentColumn = $startColumn; + foreach ($rowData as $cellValue) { if ($cellValue != $nullValue) { // Set cell value $this->getCell($currentColumn . $startRow)->setValue($cellValue); } + ++$currentColumn; } - ++$currentColumn; + ++$startRow; } - ++$startRow; } return $this; diff --git a/src/PhpSpreadsheet/Writer/BaseWriter.php b/src/PhpSpreadsheet/Writer/BaseWriter.php index 388324415..5e6d3cd49 100644 --- a/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -100,7 +100,7 @@ abstract class BaseWriter implements IWriter */ public function openFileHandle($filename): void { - if (is_resource($filename)) { + if (!is_string($filename)) { $this->fileHandle = $filename; $this->shouldCloseFile = false; diff --git a/src/PhpSpreadsheet/Writer/Csv.php b/src/PhpSpreadsheet/Writer/Csv.php index d15bc654c..19da56848 100644 --- a/src/PhpSpreadsheet/Writer/Csv.php +++ b/src/PhpSpreadsheet/Writer/Csv.php @@ -62,6 +62,8 @@ class Csv extends BaseWriter */ private bool $variableColumns = false; + private bool $preferHyperlinkToLabel = false; + /** * Create a new CSV. */ @@ -123,6 +125,14 @@ class Csv extends BaseWriter array_splice($cellsArray, Coordinate::columnIndexFromString($column)); } } + if ($this->preferHyperlinkToLabel) { + foreach ($cellsArray as $key => $value) { + $url = $sheet->getCell([$key + 1, $row])->getHyperlink()->getUrl(); + if ($url !== '') { + $cellsArray[$key] = $url; + } + } + } $this->writeLine($this->fileHandle, $cellsArray); } @@ -341,4 +351,22 @@ class Csv extends BaseWriter return $this; } + + /** + * Get whether hyperlink or label should be output. + */ + public function getPreferHyperlinkToLabel(): bool + { + return $this->preferHyperlinkToLabel; + } + + /** + * Set whether hyperlink or label should be output. + */ + public function setPreferHyperlinkToLabel(bool $preferHyperlinkToLabel): self + { + $this->preferHyperlinkToLabel = $preferHyperlinkToLabel; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 6f607e421..1b2d33abf 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -622,6 +622,14 @@ class Html extends BaseWriter // Write images $drawing = $this->sheetDrawings[$coordinates] ?? null; if ($drawing !== null) { + $opacity = ''; + $opacityValue = $drawing->getOpacity(); + if ($opacityValue !== null) { + $opacityValue = $opacityValue / 100000; + if ($opacityValue >= 0.0 && $opacityValue <= 1.0) { + $opacity = "opacity:$opacityValue; "; + } + } $filedesc = $drawing->getDescription(); $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image'; if ($drawing instanceof Drawing && $drawing->getPath() !== '') { @@ -655,7 +663,7 @@ class Html extends BaseWriter } } - $html .= 'getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' . $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' . $imageData . '" alt="' . $filedesc . '" />'; @@ -678,7 +686,7 @@ class Html extends BaseWriter // Its use here is suspect and is being eliminated. // width: X sets width of supplied image. // As a result, images bigger than cell will be contained and images smaller will not get stretched - $html .= '' . $filedesc . ''; } } diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index b40054f07..91ac5d1d2 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -238,10 +238,11 @@ class Content extends WriterPart $matrixColSpan = 1; if (isset($matches[3])) { $minRow = (int) $matches[2]; - $maxRow = (int) $matches[5]; + // https://github.com/phpstan/phpstan/issues/11602 + $maxRow = (int) $matches[5]; // @phpstan-ignore-line $matrixRowSpan = $maxRow - $minRow + 1; $minCol = Coordinate::columnIndexFromString($matches[1]); - $maxCol = Coordinate::columnIndexFromString($matches[4]); + $maxCol = Coordinate::columnIndexFromString($matches[4]); // @phpstan-ignore-line $matrixColSpan = $maxCol - $minCol + 1; } $objWriter->writeAttribute('table:number-matrix-columns-spanned', "$matrixColSpan"); diff --git a/src/PhpSpreadsheet/Writer/Ods/Formula.php b/src/PhpSpreadsheet/Writer/Ods/Formula.php index 9385607be..335df56dd 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Formula.php +++ b/src/PhpSpreadsheet/Writer/Ods/Formula.php @@ -102,9 +102,9 @@ class Formula } $newRange .= '.'; - if (!empty($column)) { - $newRange .= $column; - } + //if (!empty($column)) { // phpstan says always true + $newRange .= $column; + //} if (!empty($row)) { $newRange .= $row; } diff --git a/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php b/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php index 72b1dd056..0fda160f3 100644 --- a/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php +++ b/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php @@ -118,9 +118,9 @@ class NamedExpressions $newRange = "'" . str_replace("'", "''", $worksheet) . "'."; } - if (!empty($column)) { - $newRange .= $column; - } + //if (!empty($column)) { // phpstan says always true + $newRange .= $column; + //} if (!empty($row)) { $newRange .= $row; } diff --git a/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php index c33d0d054..aa173435e 100644 --- a/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php +++ b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php @@ -30,8 +30,8 @@ class ConditionalHelper $this->condition = $condition; $this->cellRange = $cellRange; - if (is_int($condition) || is_float($condition)) { - $this->size = ($condition <= 65535 ? 3 : 0x0000); + if (is_int($condition) && $condition >= 0 && $condition <= 65535) { + $this->size = 3; $this->tokens = pack('Cv', 0x1E, $condition); } else { try { diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 9e9b6fe70..422492d4e 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -894,7 +894,11 @@ class Parser */ private function rangeToPackedRange(string $range): array { - preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match); + if (preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match) !== 1) { + // @codeCoverageIgnoreStart + throw new WriterException('Regexp failure in rangeToPackedRange'); + // @codeCoverageIgnoreEnd + } // return absolute rows if there is a $ in the ref $row1_rel = empty($match[1]) ? 1 : 0; $row1 = $match[2]; @@ -933,7 +937,11 @@ class Parser */ private function cellToRowcol(string $cell): array { - preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match); + if (preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match) !== 1) { + // @codeCoverageIgnoreStart + throw new WriterException('Regexp failure in cellToRowcol'); + // @codeCoverageIgnoreEnd + } // return absolute column if there is a $ in the ref $col_rel = empty($match[1]) ? 1 : 0; $col_ref = $match[2]; diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 1254ca2be..94134c691 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -489,6 +489,31 @@ class Worksheet extends BIFFwriter $this->storeEof(); } + public const MAX_XLS_COLUMN = 256; + public const MAX_XLS_COLUMN_STRING = 'IV'; + public const MAX_XLS_ROW = 65536; + + private static function limitRange(string $exploded): string + { + $retVal = ''; + $ranges = Coordinate::getRangeBoundaries($exploded); + $firstCol = Coordinate::columnIndexFromString($ranges[0][0]); + $firstRow = (int) $ranges[0][1]; + if ($firstCol <= self::MAX_XLS_COLUMN && $firstRow <= self::MAX_XLS_ROW) { + $retVal = $exploded; + if (str_contains($exploded, ':')) { + $lastCol = Coordinate::columnIndexFromString($ranges[1][0]); + $ranges[1][1] = min(self::MAX_XLS_ROW, (int) $ranges[1][1]); + if ($lastCol > self::MAX_XLS_COLUMN) { + $ranges[1][0] = self::MAX_XLS_COLUMN_STRING; + } + $retVal = "{$ranges[0][0]}{$ranges[0][1]}:{$ranges[1][0]}{$ranges[1][1]}"; + } + } + + return $retVal; + } + private function writeConditionalFormatting(): void { $conditionalFormulaHelper = new ConditionalHelper($this->parser); @@ -497,7 +522,10 @@ class Worksheet extends BIFFwriter foreach ($this->phpSheet->getConditionalStylesCollection() as $key => $value) { $keyExplode = explode(',', Coordinate::resolveUnionAndIntersection($key)); foreach ($keyExplode as $exploded) { - $arrConditionalStyles[$exploded] = $value; + $range = self::limitRange($exploded); + if ($range !== '') { + $arrConditionalStyles[$range] = $value; + } } } if (!empty($arrConditionalStyles)) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index ca16557a8..e0856a51b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -118,7 +118,7 @@ class Chart extends WriterPart $this->writeColor($objWriter, $fillColor); } $borderLines = $chart->getBorderLines(); - $this->writeLineStyles($objWriter, $borderLines); + $this->writeLineStyles($objWriter, $borderLines, $chart->getNoBorder()); $this->writeEffects($objWriter, $borderLines); $objWriter->endElement(); // c:spPr @@ -1912,19 +1912,23 @@ class Chart extends WriterPart $this->writeEffects($objWriter, $axisText); } if ($labelFont !== null) { - if (!empty($labelFont->getLatin())) { + $defaultFont = ($labelFont->getName() !== Font::DEFAULT_FONT_NAME) ? $labelFont->getName() : ''; + $fontName = $labelFont->getLatin() ?: $defaultFont; + if (!empty($fontName)) { $objWriter->startElement('a:latin'); - $objWriter->writeAttribute('typeface', $labelFont->getLatin()); + $objWriter->writeAttribute('typeface', $fontName); $objWriter->endElement(); } - if (!empty($labelFont->getEastAsian())) { + $fontName = $labelFont->getEastAsian() ?: $defaultFont; + if (!empty($fontName)) { $objWriter->startElement('a:eastAsian'); - $objWriter->writeAttribute('typeface', $labelFont->getEastAsian()); + $objWriter->writeAttribute('typeface', $fontName); $objWriter->endElement(); } - if (!empty($labelFont->getComplexScript())) { + $fontName = $labelFont->getComplexScript() ?: $defaultFont; + if (!empty($fontName)) { $objWriter->startElement('a:complexScript'); - $objWriter->writeAttribute('typeface', $labelFont->getComplexScript()); + $objWriter->writeAttribute('typeface', $fontName); $objWriter->endElement(); } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php index b01746cfc..42224df39 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -196,7 +196,7 @@ class ContentTypes extends WriterPart $bgImage = $spreadsheet->getSheet($i)->getBackgroundImage(); $mimeType = $spreadsheet->getSheet($i)->getBackgroundMime(); $extension = $spreadsheet->getSheet($i)->getBackgroundExtension(); - if ($bgImage !== '' && !isset($aMediaContentTypes[$mimeType])) { + if ($bgImage !== '' && !isset($aMediaContentTypes[$extension])) { $this->writeDefaultContentType($objWriter, $extension, $mimeType); } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index fc84c67e1..8a17eb9f1 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -263,7 +263,13 @@ class Drawing extends WriterPart $objWriter->startElement('a:blip'); $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT); $objWriter->writeAttribute('r:embed', 'rId' . $relationId); - $objWriter->endElement(); + $temp = $drawing->getOpacity(); + if (is_int($temp) && $temp >= 0 && $temp <= 100000) { + $objWriter->startElement('a:alphaModFix'); + $objWriter->writeAttribute('amt', "$temp"); + $objWriter->endElement(); // a:alphaModFix + } + $objWriter->endElement(); // a:blip $srcRect = $drawing->getSrcRect(); if (!empty($srcRect)) { @@ -501,7 +507,11 @@ class Drawing extends WriterPart private function writeVMLHeaderFooterImage(XMLWriter $objWriter, string $reference, HeaderFooterDrawing $image): void { // Calculate object id - preg_match('{(\d+)}', md5($reference), $m); + if (preg_match('{(\d+)}', md5($reference), $m) !== 1) { + // @codeCoverageIgnoreStart + throw new WriterException('Regexp failure in writeVMLHeaderFooterImage'); + // @codeCoverageIgnoreEnd + } $id = 1500 + ((int) substr($m[1], 0, 2) * 1); // Calculate offset diff --git a/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php b/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php index 9ac7893dd..a563bd913 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php @@ -130,7 +130,6 @@ class FunctionPrefix . '|ifs' . '|maxifs' . '|minifs' - . '|sumifs' . '|textjoin' // functions added with Excel 365 . '|anchorarray' diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 28a7c0ee8..28af25829 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -129,6 +129,9 @@ class Worksheet extends WriterPart // Breaks $this->writeBreaks($objWriter, $worksheet); + // IgnoredErrors + $this->writeIgnoredErrors($objWriter); + // Drawings and/or Charts $this->writeDrawings($objWriter, $worksheet, $includeCharts); @@ -141,9 +144,6 @@ class Worksheet extends WriterPart // AlternateContent $this->writeAlternateContent($objWriter, $worksheet); - // IgnoredErrors - $this->writeIgnoredErrors($objWriter); - // BackgroundImage must come after ignored, before table $this->writeBackgroundImage($objWriter, $worksheet); @@ -983,10 +983,10 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('sqref', $dv->getSqref() ?? $coordinate); if ($dv->getFormula1() !== '') { - $objWriter->writeElement('formula1', $dv->getFormula1()); + $objWriter->writeElement('formula1', FunctionPrefix::addFunctionPrefix($dv->getFormula1())); } if ($dv->getFormula2() !== '') { - $objWriter->writeElement('formula2', $dv->getFormula2()); + $objWriter->writeElement('formula2', FunctionPrefix::addFunctionPrefix($dv->getFormula2())); } $objWriter->endElement(); @@ -1542,10 +1542,11 @@ class Worksheet extends WriterPart return $coordinate; } $minRow = (int) $matches[2]; - $maxRow = (int) $matches[5]; + // https://github.com/phpstan/phpstan/issues/11602 + $maxRow = (int) $matches[5]; // @phpstan-ignore-line $rows = $maxRow - $minRow + 1; $minCol = Coordinate::columnIndexFromString($matches[1]); - $maxCol = Coordinate::columnIndexFromString($matches[4]); + $maxCol = Coordinate::columnIndexFromString($matches[4]); // @phpstan-ignore-line $cols = $maxCol - $minCol + 1; $firstCellArray = Coordinate::indexesFromString($coordinate); $lastRow = $firstCellArray[1] + $rows - 1; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php index e82d7ccd9..9e610f234 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php @@ -60,8 +60,8 @@ class TruncTest extends AllSetupTeardown $sheet = $this->getSheet(); $sheet->getCell('E1')->setValue($value); $sheet->getCell('E2')->setValue("=TRUNC(E1,$digits)"); - $result = $sheet->getCell('E2')->getCalculatedValue(); - self::assertSame($expectedResult, (string) $result); + $result = $sheet->getCell('E2')->getCalculatedValueString(); + self::assertSame($expectedResult, $result); } public static function providerTooMuchPrecision(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php index be3cbd1ed..3a5f33cff 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php @@ -30,6 +30,7 @@ class ConcatenateTest extends AllSetupTeardown $finalArg .= 'A3'; $sheet->getCell('A3')->setValue(str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5)); } else { + self::assertTrue($arg === null || is_scalar($arg)); $finalArg .= '"' . (string) $arg . '"'; } } diff --git a/tests/PhpSpreadsheetTests/Chart/Issue4201Test.php b/tests/PhpSpreadsheetTests/Chart/Issue4201Test.php new file mode 100644 index 000000000..d93146c6c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue4201Test.php @@ -0,0 +1,97 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testLabelFont(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + + // Sample data for pie chart + $data = [ + ['Category', 'Value'], + ['Category A', 40], + ['Category B', 30], + ['Category C', 20], + ['Category D', 10], + ]; + $worksheet->fromArray($data, null, 'A1'); + $worksheet->getColumnDimension('A')->setAutoSize(true); + + // Create data series for the pie chart + $categories = [new DataSeriesValues('String', 'Worksheet!$A$2:$A$5', null, 4)]; + $values = [new DataSeriesValues('Number', 'Worksheet!$B$2:$B$5', null, 4)]; + + // Set layout for data labels + $font = new Font(); + $font->setName('Times New Roman'); + $font->setSize(8); + $layout = new Layout(); + $layout->setShowVal(true); // Display values + $layout->setShowCatName(true); // Display category names + $layout->setLabelFont($font); + + $series = new DataSeries( + DataSeries::TYPE_PIECHART, // Chart type: Pie chart + null, + range(0, count($values) - 1), + [], + $categories, + $values + ); + + $plotArea = new PlotArea($layout, [$series]); + $chart = new Chart('Pie Chart', null, null, $plotArea); + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart2 = $charts[0]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); + $layout2 = $plotArea2->getLayout(); + self::assertNotNull($layout2); + $font2 = $layout2->getLabelFont(); + self::assertNotNull($font2); + self::assertSame('Times New Roman', $font2->getLatin()); + self::assertSame(8.0, $font2->getSize()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Issue562Test.php b/tests/PhpSpreadsheetTests/Chart/Issue562Test.php new file mode 100644 index 000000000..21bb903fa --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue562Test.php @@ -0,0 +1,128 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + /** + * @dataProvider providerNoBorder + */ + public function testNoBorder(?bool $noBorder, bool $expectedResult): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_AREACHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + $plotArea = new PlotArea(null, [$series]); + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + if ($noBorder !== null) { + $chart->setNoBorder($noBorder); + } + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + self::assertSame($expectedResult, $chart2->getNoBorder()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public static function providerNoBorder(): array + { + return [ + [true, true], + [false, false], + [null, false], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Helper/HtmlTest.php b/tests/PhpSpreadsheetTests/Helper/HtmlTest.php index 784eb4c55..26f22847a 100644 --- a/tests/PhpSpreadsheetTests/Helper/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Helper/HtmlTest.php @@ -48,4 +48,26 @@ class HtmlTest extends TestCase self::assertSame($expected, $actual->getPlainText()); } + + public function testSTag(): void + { + $html = new Html(); + $input = 'Hello testworld'; + $richText = $html->toRichTextObject($input); + $elements = $richText->getRichTextElements(); + + self::assertSame(count($elements), 3); + + self::assertSame($elements[0]->getText(), 'Hello '); + self::assertNotNull($elements[0]->getFont()); + self::assertFalse($elements[0]->getFont()->getStrikethrough()); + + self::assertSame($elements[1]->getText(), 'test'); + self::assertNotNull($elements[1]->getFont()); + self::assertTrue($elements[1]->getFont()->getStrikethrough()); + + self::assertSame($elements[2]->getText(), 'world'); + self::assertNotNull($elements[2]->getFont()); + self::assertFalse($elements[2]->getFont()->getStrikethrough()); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/BinderTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/BinderTest.php new file mode 100644 index 000000000..95d8d5cb9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Csv/BinderTest.php @@ -0,0 +1,54 @@ +loadSpreadsheetFromString($data); + $sheet1 = $spreadsheet1->getActiveSheet(); + $sheet1->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING); + $sheet1->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC); + $sheet1->setCellValue('C3', 9); + $sheet1->fromArray([10, 11, 12], null, 'A4'); + $expected1 = [ + [1, 2, 3], + [4, 5, 6], + ['7', 8, 9], + [10, 11, 12], + ]; + self::AssertSame($expected1, $sheet1->toArray(null, false, false)); + + $reader2 = new Csv(); + $reader2->setValueBinder(new StringValueBinder()); + $spreadsheet2 = $reader2->loadSpreadsheetFromString($data); + $sheet2 = $spreadsheet2->getActiveSheet(); + $sheet2->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING); + $sheet2->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC); + $sheet2->setCellValue('C3', 9); + $sheet2->fromArray([10, 11, 12], null, 'A4'); + $expected2 = [ + ['1', '2', '3'], + ['4', '5', '6'], + ['7', 8, '9'], + ['10', '11', '12'], + ]; + self::AssertSame($expected2, $sheet2->toArray(null, false, false)); + + $spreadsheet1->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvLineEndingTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvLineEndingTest.php index f58e1809a..28ff9f30f 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvLineEndingTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvLineEndingTest.php @@ -25,6 +25,9 @@ class CsvLineEndingTest extends TestCase */ public function testEndings(string $ending): void { + if ($ending === "\r" && PHP_VERSION_ID >= 90000) { + self::markTestSkipped('Mac line endings not supported for Php9+'); + } $this->tempFile = $filename = File::temporaryFilename(); $data = ['123', '456', '789']; file_put_contents($filename, implode($ending, $data)); diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvTest.php index 6ce2d23d4..e3da06bcc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvTest.php @@ -114,6 +114,10 @@ class CsvTest extends TestCase public function testEscapeCharacters(): void { + if (PHP_VERSION_ID >= 90000) { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Escape character must be null string'); + } $reader = (new Csv())->setEscapeCharacter('"'); $worksheet = $reader->load('tests/data/Reader/CSV/backslash.csv') ->getActiveSheet(); @@ -230,6 +234,10 @@ class CsvTest extends TestCase */ public function testInferSeparator(string $escape, string $delimiter): void { + if (PHP_VERSION_ID >= 90000 && $escape !== '') { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Escape character must be null string'); + } $reader = new Csv(); $reader->setEscapeCharacter($escape); $filename = 'tests/data/Reader/CSV/escape.csv'; diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/Php9Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/Php9Test.php new file mode 100644 index 000000000..131c366ae --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Csv/Php9Test.php @@ -0,0 +1,37 @@ += 90000) { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Php7.4 or Php8'); + } + $dir = 'tests/data/Reader/CSV'; + $files = glob("$dir/*"); + self::assertNotFalse($files); + $affected = []; + foreach ($files as $file) { + $base = basename($file); + $encoding = 'UTF-8'; + if (str_contains($base, 'utf') && !str_contains($base, 'bom')) { + $encoding = 'guess'; + } + $result = Csv::affectedByPhp9($file, $encoding); + if ($result) { + $affected[] = $base; + } + } + $expected = ['backslash.csv', 'escape.csv', 'linend.mac.csv']; + self::assertSame($expected, $affected); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormula2Test.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormula2Test.php index 58e1eebd6..6ea143565 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormula2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormula2Test.php @@ -26,7 +26,7 @@ class ArrayFormula2Test extends TestCase $cell = $worksheet->getCell($cellAddress); self::assertSame(DataType::TYPE_FORMULA, $cell->getDataType()); self::assertSame(['t' => 'array', 'ref' => $expectedRange], $cell->getFormulaAttributes()); - self::assertSame($expectedFormula, strtoupper($cell->getValue())); + self::assertSame($expectedFormula, strtoupper($cell->getValueString())); Calculation::getInstance($spreadsheet)->setInstanceArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); $worksheet->calculateArrays(); $cell = $worksheet->getCell($cellAddress); diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php index 80817633a..f0e9599cc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/ArrayFormulaTest.php @@ -30,7 +30,7 @@ class ArrayFormulaTest extends TestCase } else { self::assertEmpty($cell->getFormulaAttributes()); } - self::assertSame($expectedFormula, strtoupper($cell->getValue())); + self::assertSame($expectedFormula, strtoupper($cell->getValueString())); Calculation::getInstance($spreadsheet)->setInstanceArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); $worksheet->calculateArrays(); $cell = $worksheet->getCell($cellAddress); diff --git a/tests/PhpSpreadsheetTests/Reader/Html/BinderTest.php b/tests/PhpSpreadsheetTests/Reader/Html/BinderTest.php new file mode 100644 index 000000000..cbf1c4fdc --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/BinderTest.php @@ -0,0 +1,58 @@ + + + 123 + 456 + + + EOF; + $reader1 = new Html(); + $spreadsheet1 = $reader1->loadFromString($data); + $sheet1 = $spreadsheet1->getActiveSheet(); + $sheet1->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING); + $sheet1->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC); + $sheet1->setCellValue('C3', 9); + $sheet1->fromArray([10, 11, 12], null, 'A4'); + $expected1 = [ + [1, 2, 3], + [4, 5, 6], + ['7', 8, 9], + [10, 11, 12], + ]; + self::AssertSame($expected1, $sheet1->toArray(null, false, false)); + + $reader2 = new Html(); + $reader2->setValueBinder(new StringValueBinder()); + $spreadsheet2 = $reader2->loadFromString($data); + $sheet2 = $spreadsheet2->getActiveSheet(); + $sheet2->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING); + $sheet2->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC); + $sheet2->setCellValue('C3', 9); + $sheet2->fromArray([10, 11, 12], null, 'A4'); + $expected2 = [ + ['1', '2', '3'], + ['4', '5', '6'], + ['7', 8, '9'], + ['10', '11', '12'], + ]; + self::AssertSame($expected2, $sheet2->toArray(null, false, false)); + + $spreadsheet1->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php new file mode 100644 index 000000000..33dee3108 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php @@ -0,0 +1,65 @@ + + + test image voilà + + '; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + + /** @var Drawing $drawing */ + $drawing = $firstSheet->getDrawingCollection()[0]; + self::assertEquals($imagePath, $drawing->getPath()); + self::assertEquals('A1', $drawing->getCoordinates()); + } + + public function testCantInsertImageNotFound(): void + { + if (getenv('SKIP_URL_IMAGE_TEST') === '1') { + self::markTestSkipped('Skipped due to setting of environment variable'); + } + $imagePath = 'https://phpspreadsheet.readthedocs.io/en/latest/topics/images/xxx01-03-filter-icon-1.png'; + $html = ' + + + +
test image voilà
'; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + $drawingCollection = $firstSheet->getDrawingCollection(); + self::assertCount(0, $drawingCollection); + } + + public function testCannotInsertImageBadProtocol(): void + { + $this->expectException(SpreadsheetException::class); + $this->expectExceptionMessage('Invalid protocol for linked drawing'); + $imagePath = 'httpx://phpspreadsheet.readthedocs.io/en/latest/topics/images/01-03-filter-icon-1.png'; + $html = ' + + + +
test image voilà
'; + $filename = HtmlHelper::createHtml($html); + HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/Issue1107Test.php b/tests/PhpSpreadsheetTests/Reader/Html/Issue1107Test.php new file mode 100644 index 000000000..866850083 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/Issue1107Test.php @@ -0,0 +1,46 @@ +outfile !== '') { + unlink($this->outfile); + $this->outfile = ''; + } + } + + public function testIssue1107(): void + { + // failure due to cached file size + $outstr = str_repeat('a', 1023) . "\n"; + $allout = str_repeat($outstr, 10); + $this->outfile = $outfile = File::temporaryFilename(); + file_put_contents($outfile, $allout); + self::assertSame(10240, filesize($outfile)); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $writer = new HtmlWriter($spreadsheet); + $writer->save($outfile); + $spreadsheet->disconnectWorksheets(); + $reader = new HtmlReader(); + $spreadsheet2 = $reader->load($outfile); + $sheet2 = $spreadsheet2->getActiveSheet(); + self::assertSame(1, $sheet2->getCell('A1')->getValue()); + + $spreadsheet2->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php index 0c4cd1d45..726b42ba3 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/ArrayFormulaTest.php @@ -26,7 +26,7 @@ class ArrayFormulaTest extends TestCase $cell = $worksheet->getCell($cellAddress); self::assertSame(DataType::TYPE_FORMULA, $cell->getDataType()); self::assertSame(['t' => 'array', 'ref' => $expectedRange], $cell->getFormulaAttributes()); - self::assertSame($expectedFormula, strtoupper($cell->getValue())); + self::assertSame($expectedFormula, strtoupper($cell->getValueString())); Calculation::getInstance($spreadsheet)->setInstanceArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); $worksheet->calculateArrays(); $cell = $worksheet->getCell($cellAddress); diff --git a/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php b/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php index aa214b4a0..9299161f0 100644 --- a/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php @@ -30,7 +30,11 @@ class XmlScannerTest extends TestCase self::assertNotFalse($glob); foreach ($glob as $file) { $filename = realpath($file); - $expectedResult = file_get_contents($file); + $expectedResult = (string) file_get_contents($file); + if (preg_match('/UTF-16(LE|BE)?/', $file, $matches) == 1) { + $expectedResult = (string) mb_convert_encoding($expectedResult, 'UTF-8', $matches[0]); + $expectedResult = preg_replace('/encoding\\s*=\\s*[\'"]UTF-\\d+(LE|BE)?[\'"]/', '', $expectedResult) ?? $expectedResult; + } $tests[basename($file)] = [$filename, $expectedResult]; } @@ -132,19 +136,47 @@ class XmlScannerTest extends TestCase self::assertSame($input, $output); } - public function testUtf7Whitespace(): void + /** + * @dataProvider providerInvalidXlsx + */ + public function testInvalidXlsx(string $filename, string $message): void { $this->expectException(ReaderException::class); - $this->expectExceptionMessage('Double-encoded'); + $this->expectExceptionMessage($message); $reader = new Xlsx(); - $reader->load('tests/data/Reader/XLSX/utf7white.dontuse'); + $reader->load("tests/data/Reader/XLSX/$filename"); } - public function testUtf8Entity(): void + public static function providerInvalidXlsx(): array + { + return [ + ['utf7white.dontuse', 'UTF-7 encoding not permitted'], + ['utf7quoteorder.dontuse', 'UTF-7 encoding not permitted'], + ['utf8and16.dontuse', 'Double encoding not permitted'], + ['utf8and16.entity.dontuse', 'Detected use of ENTITY'], + ['utf8entity.dontuse', 'Detected use of ENTITY'], + ['utf16entity.dontuse', 'Detected use of ENTITY'], + ['ebcdic.dontuse', 'EBCDIC encoding not permitted'], + ]; + } + + /** + * @dataProvider providerValidUtf16 + */ + public function testValidUtf16(string $filename): void { - $this->expectException(ReaderException::class); - $this->expectExceptionMessage('Detected use of ENTITY'); $reader = new Xlsx(); - $reader->load('tests/data/Reader/XLSX/utf8entity.dontuse'); + $spreadsheet = $reader->load("tests/data/Reader/XLSX/$filename"); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getCell('A1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } + + public static function providerValidUtf16(): array + { + return [ + ['utf16be.xlsx'], + ['utf16be.bom.xlsx'], + ]; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Slk/BinderTest.php b/tests/PhpSpreadsheetTests/Reader/Slk/BinderTest.php new file mode 100644 index 000000000..4be6753f1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Slk/BinderTest.php @@ -0,0 +1,30 @@ +load($infile); + $sheet = $spreadsheet->getActiveSheet(); + $expected1 = [[1, 2], [3, '']]; + self::assertSame($expected1, $sheet->toArray(null, false, false)); + $reader2 = new Slk(); + $reader2->setValueBinder(new StringValueBinder()); + $spreadsheet2 = $reader2->load($infile); + $sheet2 = $spreadsheet2->getActiveSheet(); + $expected2 = [['1', '2'], ['3', '']]; + self::assertSame($expected2, $sheet2->toArray(null, false, false)); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php index 6ba32aa33..9bc962111 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/ArrayFormulaTest.php @@ -17,7 +17,7 @@ class ArrayFormulaTest extends TestCase self::assertSame(DataType::TYPE_FORMULA, $sheet->getCell('B1')->getDataType()); self::assertSame(['t' => 'array', 'ref' => 'B1:B3'], $sheet->getCell('B1')->getFormulaAttributes()); - self::assertSame('=CONCATENATE(A1:A3,"-",C1:C3)', strtoupper($sheet->getCell('B1')->getValue())); + self::assertSame('=CONCATENATE(A1:A3,"-",C1:C3)', strtoupper($sheet->getCell('B1')->getValueString())); self::assertSame('a-1', $sheet->getCell('B1')->getOldCalculatedValue()); self::assertSame('b-2', $sheet->getCell('B2')->getValue()); self::assertSame('c-3', $sheet->getCell('B3')->getValue()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/HtmlEntitiesLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/HtmlEntitiesLoadTest.php new file mode 100644 index 000000000..197e00841 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/HtmlEntitiesLoadTest.php @@ -0,0 +1,29 @@ +', $contents); + $reader = new XmlReader(); + $spreadsheet = $reader->load($infile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Τέλεια όραση χωρίς γυαλιά', $sheet->getCell('E2')->getValue()); + $g2 = $sheet->getCell('G2')->getValue(); + self::assertStringContainsString('
', $g2); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Csv/HyperlinkTest.php b/tests/PhpSpreadsheetTests/Writer/Csv/HyperlinkTest.php new file mode 100644 index 000000000..b1e1bbfde --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Csv/HyperlinkTest.php @@ -0,0 +1,49 @@ +getActiveSheet(); + $sheet->setCellValue('A1', 3); + $sheet->setCellValue('B1', 4); + $sheet->setCellValue('C1', 5); + $sheet->setCellValue('A2', 6); + $sheet->setCellValue('B2', 'hyperlink'); + $sheet->getCell('B2')->getHyperlink() + ->setUrl('http://www.example.com'); + $sheet->setCellValue('C2', 8); + + $fh = fopen('php://memory', 'r+b'); + self::assertNotFalse($fh); + $writer = new Csv($spreadsheet); + self::assertFalse($writer->getPreferHyperlinkToLabel()); + $writer->setEnclosureRequired(false)->setLineEnding("\n"); + $writer->save($fh); + rewind($fh); + self::assertSame( + "3,4,5\n6,hyperlink,8\n", + stream_get_contents($fh) + ); + + rewind($fh); + $writer->setPreferHyperlinkToLabel(true); + $writer->save($fh); + rewind($fh); + self::assertSame( + "3,4,5\n6,http://www.example.com,8\n", + stream_get_contents($fh) + ); + fclose($fh); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/TransparentDrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Html/TransparentDrawingsTest.php new file mode 100644 index 000000000..faaf7e354 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/TransparentDrawingsTest.php @@ -0,0 +1,58 @@ +getActiveSheet(); + $sheet->setShowGridLines(false); + + $drawing = new Drawing(); + $drawing->setName('Blue Square'); + $drawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertEquals($drawing->getWidth(), 100); + self::assertEquals($drawing->getHeight(), 100); + $drawing->setCoordinates('A1'); + $drawing->setCoordinates2('E8'); + $drawing->setOpacity(25000); + $drawing->setWorksheet($sheet); + + $writer = new HtmlWriter($spreadsheet); + $content = $writer->generateHTMLAll(); + self::assertStringContainsString('opacity:0.25;', $content); + $spreadsheet->disconnectWorksheets(); + } + + public function testHtmlTransparentMemoryDrawing(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setShowGridLines(false); + + $contents = file_get_contents('tests/data/Writer/XLSX/blue_square.png'); + $drawing = MemoryDrawing::fromString("$contents"); + $drawing->setName('Blue Square'); + self::assertEquals($drawing->getWidth(), 100); + self::assertEquals($drawing->getHeight(), 100); + $drawing->setCoordinates('A1'); + $drawing->setCoordinates2('E8'); + $drawing->setOpacity(25000); + $drawing->setWorksheet($sheet); + + $writer = new HtmlWriter($spreadsheet); + $content = $writer->generateHTMLAll(); + self::assertStringContainsString('opacity:0.25;', $content); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/ConditionalLimitsTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/ConditionalLimitsTest.php new file mode 100644 index 000000000..84acb07ab --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/ConditionalLimitsTest.php @@ -0,0 +1,86 @@ +getActiveSheet(); + $sheet->fromArray( + [ + ['Cell', 0, null, null, 'Col Rng', -2, -1], + [null, null, null, null, null, 0, 1], + ['Cell Rng', -2, -1, 0, null, 2, 3], + [null, 1, 2, 3, null, 4, -1], + [], + ['Row Rng'], + [-2, -1, 0], + [1, 2, 3], + ], + strictNullComparison: true + ); + $redStyle = new Style(false, true); + $redStyle->getFont()->setColor(new Color(Color::COLOR_RED)); + + $condition1 = new Conditional(); + $condition1->setConditionType(Conditional::CONDITION_CELLIS) + ->setOperatorType(Conditional::OPERATOR_BETWEEN) + ->addCondition(-1) + ->addCondition(1) + ->setStyle($redStyle); + $conditionalStyles = [$condition1]; + $cellRange = 'B1'; + $sheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + + $condition2 = new Conditional(); + $condition2->setConditionType(Conditional::CONDITION_CELLIS) + ->setOperatorType(Conditional::OPERATOR_BETWEEN) + ->addCondition(-1.5) + ->addCondition(1.5) + ->setStyle($redStyle); + $conditionalStyles = [$condition2]; + $cellRange = 'F:G'; + $sheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + + $condition3 = new Conditional(); + $condition3->setConditionType(Conditional::CONDITION_CELLIS) + ->setOperatorType(Conditional::OPERATOR_BETWEEN) + ->addCondition(-1) + ->addCondition(70000) + ->setStyle($redStyle); + $conditionalStyles = [$condition3]; + $cellRange = '7:8'; + $sheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + + $cellRange = 'B3:D4'; + $sheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + $sheet->setSelectedCells('A1'); + $keys = array_keys($sheet->getConditionalStylesCollection()); + self::assertSame(['B1', 'F1:G1048576', 'A7:XFD8', 'B3:D4'], $keys); + + $robj = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + $sheet0 = $robj->getActiveSheet(); + $conditionals = $sheet0->getConditionalStylesCollection(); + self::assertSame(['B1', 'F1:G65536', 'A7:IV8', 'B3:D4'], array_keys($conditionals)); + $b1 = $conditionals['B1'][0]; + self::assertSame([-1, 1], $b1->getConditions()); + $b1 = $conditionals['F1:G65536'][0]; + self::assertSame([-1.5, 1.5], $b1->getConditions()); + $b1 = $conditionals['A7:IV8'][0]; + self::assertSame([-1, 70000], $b1->getConditions()); + $b1 = $conditionals['B3:D4'][0]; + self::assertSame([-1, 70000], $b1->getConditions()); + $robj->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFormulaPrefixTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFormulaPrefixTest.php index 55eacf68f..0222b5c2c 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFormulaPrefixTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFormulaPrefixTest.php @@ -11,11 +11,6 @@ use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class ArrayFormulaPrefixTest extends AbstractFunctional { - protected function setUp(): void - { - parent::setUp(); - } - /** * Test to ensure that xlfn prefix is being added to functions * included in an array formula, if appropriate. diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFormulaValidationTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFormulaValidationTest.php new file mode 100644 index 000000000..a907b96cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFormulaValidationTest.php @@ -0,0 +1,87 @@ +outputFile !== '') { + unlink($this->outputFile); + $this->outputFile = ''; + } + } + + /** + * @dataProvider validationProvider + */ + public function testWriteArrayFormulaValidation(string $formula): void + { + $spreadsheet = new Spreadsheet(); + Calculation::getInstance($spreadsheet) + ->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('C1', 1); + $sheet->setCellValue('C2', 2); + $sheet->setCellValue('C3', 3); + $sheet->setCellValue('C4', 3); + $sheet->setCellValue('C5', 5); + $sheet->setCellValue('C6', 6); + $sheet->setCellValue('B1', '=UNIQUE(C1:C6)'); + + $validation = $sheet->getCell('A1')->getDataValidation(); + $validation->setType(DataValidation::TYPE_LIST); + $validation->setErrorStyle(DataValidation::STYLE_STOP); + $validation->setAllowBlank(true); + $validation->setShowDropDown(true); + $validation->setShowErrorMessage(true); + $validation->setError('Invalid input'); + $validation->setFormula1($formula); + $sheet->getCell('A1')->setDataValidation($validation); + + $this->outputFile = File::temporaryFilename(); + $writer = new XlsxWriter($spreadsheet); + $writer->save($this->outputFile); + $spreadsheet->disconnectWorksheets(); + + $reader = new XlsxReader(); + $spreadsheet2 = $reader->load($this->outputFile); + Calculation::getInstance($spreadsheet2)->setInstanceArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); + $sheet2 = $spreadsheet2->getActiveSheet(); + $validation2 = $sheet2->getCell('A1')->getDataValidation(); + self::assertSame('ANCHORARRAY($B$1)', $validation2->getFormula1()); + $spreadsheet2->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFile; + $file .= '#xl/worksheets/sheet1.xml'; + $data = file_get_contents($file); + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertStringContainsString('_xlfn.ANCHORARRAY($B$1)', $data); + } + } + + public static function validationProvider(): array + { + return [ + ['ANCHORARRAY($B$1)'], + ['$B$1#'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctions2Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctions2Test.php index ab3a54a4d..c41a176fb 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctions2Test.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctions2Test.php @@ -268,7 +268,9 @@ class ArrayFunctions2Test extends TestCase { $json = file_get_contents('tests/data/Writer/XLSX/ArrayFunctions2.json'); self::assertNotFalse($json); - $this->trn = json_decode($json, true); + $trn = json_decode($json, true); + self::assertIsArray($trn); + $this->trn = $trn; $spreadsheet = new Spreadsheet(); Calculation::getInstance($spreadsheet)->setInstanceArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php index 630993206..2ddd31733 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php @@ -15,6 +15,14 @@ class ArrayFunctionsTest extends TestCase { private string $outputFile = ''; + protected function tearDown(): void + { + if ($this->outputFile !== '') { + unlink($this->outputFile); + $this->outputFile = ''; + } + } + public function testArrayOutput(): void { $spreadsheet = new Spreadsheet(); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/FunctionPrefixTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/FunctionPrefixTest.php index d418bc43b..d4995c8fe 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/FunctionPrefixTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/FunctionPrefixTest.php @@ -37,6 +37,7 @@ class FunctionPrefixTest extends TestCase 'DAYS/NETWORKDAYS 4' => ['ABS(_xlfn.DAYS(DATE(2023,1,1),TODAY()))', 'ABS(_xlfn.DAYS(DATE(2023,1,1),TODAY()))'], 'DAYS/NETWORKDAYS 5' => ['NETWORKDAYS(DATE(2023,1,1),TODAY(), C:C)', 'NETWORKDAYS(DATE(2023,1,1),TODAY(), C:C)'], 'COUNTIFS reclassified as Legacy' => ['COUNTIFS()', 'COUNTIFS()'], + 'SUMIFS reclassified as Legacy' => ['SUMIFS()', 'SUMIFS()'], ]; } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4179Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4179Test.php new file mode 100644 index 000000000..c6f5ea25c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4179Test.php @@ -0,0 +1,62 @@ +outputFile !== '') { + unlink($this->outputFile); + $this->outputFile = ''; + } + } + + public function testIssue4179(): void + { + // duplicate entry in ContentTypes + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $imageFile = 'tests/data/Writer/XLSX/backgroundtest.png'; + $image = (string) file_get_contents($imageFile); + $sheet->setBackgroundImage($image); + $drawing = new Drawing(); + $drawing->setName('Blue Square'); + $drawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + $drawing->setCoordinates('A1'); + $drawing->setWorksheet($sheet); + $writer = new XlsxWriter($spreadsheet); + $this->outputFile = File::temporaryFilename(); + $writer->save($this->outputFile); + $spreadsheet->disconnectWorksheets(); + + $zip = new ZipArchive(); + $open = $zip->open($this->outputFile, ZipArchive::RDONLY); + $pngCount = 0; + if ($open !== true) { + self::fail("zip open failed for {$this->outputFile}"); + } else { + $contents = (string) $zip->getFromName('[Content_Types].xml'); + $subCount = substr_count($contents, '"png"'); + self::assertSame(1, $subCount); + for ($i = 0; $i < $zip->numFiles; ++$i) { + $filename = (string) $zip->getNameIndex($i); + if (preg_match('~^xl/media/\\w+[.]png$~', $filename) === 1) { + ++$pngCount; + } + } + } + self::assertSame(2, $pngCount); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4200Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4200Test.php new file mode 100644 index 000000000..4a64e8c0c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4200Test.php @@ -0,0 +1,90 @@ +outputFile !== '') { + unlink($this->outputFile); + $this->outputFile = ''; + } + } + + public function testIssue4200(): void + { + // ignoredErrors came after legacyDrawing + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValueExplicit('01', DataType::TYPE_STRING); + $sheet->getCell('A1') + ->getIgnoredErrors() + ->setNumberStoredAsText(true); + $richText = new RichText(); + $richText->createText('hello'); + $sheet->getComment('C1') + ->setText($richText); + $writer = new XlsxWriter($spreadsheet); + $this->outputFile = File::temporaryFilename(); + $writer->save($this->outputFile); + $spreadsheet->disconnectWorksheets(); + + $zip = new ZipArchive(); + $open = $zip->open($this->outputFile, ZipArchive::RDONLY); + if ($open !== true) { + self::fail("zip open failed for {$this->outputFile}"); + } else { + $contents = (string) $zip->getFromName('xl/worksheets/sheet1.xml'); + self::assertStringContainsString( + '', + $contents + ); + } + } + + public function testIssue4145(): void + { + // ignoredErrors came after drawing + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValueExplicit('01', DataType::TYPE_STRING); + $sheet->getCell('A1') + ->getIgnoredErrors() + ->setNumberStoredAsText(true); + $drawing = new Drawing(); + $drawing->setName('Blue Square'); + $drawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + $drawing->setCoordinates('C1'); + $drawing->setWorksheet($sheet); + $writer = new XlsxWriter($spreadsheet); + $this->outputFile = File::temporaryFilename(); + $writer->save($this->outputFile); + $spreadsheet->disconnectWorksheets(); + + $zip = new ZipArchive(); + $open = $zip->open($this->outputFile, ZipArchive::RDONLY); + if ($open !== true) { + self::fail("zip open failed for {$this->outputFile}"); + } else { + $contents = (string) $zip->getFromName('xl/worksheets/sheet1.xml'); + self::assertStringContainsString( + '', + $contents + ); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/TransparentDrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/TransparentDrawingsTest.php new file mode 100644 index 000000000..90ba0e4df --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/TransparentDrawingsTest.php @@ -0,0 +1,96 @@ +getActiveSheet(); + + // Add gif image that coordinates is two cell anchor. + $drawing = new Drawing(); + $drawing->setName('Blue Square'); + $drawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertEquals($drawing->getWidth(), 100); + self::assertEquals($drawing->getHeight(), 100); + $drawing->setCoordinates('A1'); + $drawing->setOffsetX(30); + $drawing->setOffsetY(10); + $drawing->setCoordinates2('E8'); + $drawing->setOffsetX2(-50); + $drawing->setOffsetY2(-20); + $drawing->setOpacity(40000); + $drawing->setWorksheet($sheet); + + // Write file + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + + // Check image coordinates. + $drawingCollection = $rsheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection); + $drawing = $drawingCollection[0]; + self::assertNotNull($drawing); + + self::assertSame(100, $drawing->getWidth()); + self::assertSame(100, $drawing->getHeight()); + self::assertSame('A1', $drawing->getCoordinates()); + self::assertSame(30, $drawing->getOffsetX()); + self::assertSame(10, $drawing->getOffsetY()); + self::assertSame('E8', $drawing->getCoordinates2()); + self::assertSame(-50, $drawing->getOffsetX2()); + self::assertSame(-20, $drawing->getOffsetY2()); + self::assertSame(40000, $drawing->getOpacity()); + self::assertSame($rsheet, $drawing->getWorksheet()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + /** + * Save and load XLSX with 1-cell anchor drawing with transparency. + */ + public function testOneCellAnchorTransparent(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // Add gif image that coordinates is two cell anchor. + $drawing = new Drawing(); + $drawing->setName('Blue Square'); + $drawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + //self::assertEquals($drawing->getWidth(), 100); + //self::assertEquals($drawing->getHeight(), 100); + $drawing->setCoordinates('A1'); + $drawing->setOpacity(40000); + $drawing->setWorksheet($sheet); + + // Write file + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + + // Check image coordinates. + $drawingCollection = $rsheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection); + $drawing = $drawingCollection[0]; + self::assertNotNull($drawing); + + self::assertSame(100, $drawing->getWidth()); + self::assertSame(100, $drawing->getHeight()); + self::assertSame('A1', $drawing->getCoordinates()); + self::assertSame(40000, $drawing->getOpacity()); + self::assertSame($rsheet, $drawing->getWorksheet()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Calculation/MathTrig/ROUNDDOWN.php b/tests/data/Calculation/MathTrig/ROUNDDOWN.php index fed45cd44..a68eb0ae2 100644 --- a/tests/data/Calculation/MathTrig/ROUNDDOWN.php +++ b/tests/data/Calculation/MathTrig/ROUNDDOWN.php @@ -33,5 +33,7 @@ return [ [0, 'B1, 0'], ['exception', ''], ['exception', '35.51'], - 'negative number and precision' => [-31400, '-31415.92654, -2'], + 'negative number and precision ROUNDDOWN' => [-31400, '-31415.92654, -2'], + 'issue 4213 #1' => [249.0, '249.9975, 0'], + 'issue 4213 #2' => [-249.0, '-249.9975, 0'], ]; diff --git a/tests/data/Calculation/MathTrig/ROUNDUP.php b/tests/data/Calculation/MathTrig/ROUNDUP.php index 9683d54d4..0f9864dcb 100644 --- a/tests/data/Calculation/MathTrig/ROUNDUP.php +++ b/tests/data/Calculation/MathTrig/ROUNDUP.php @@ -34,4 +34,6 @@ return [ ['exception', ''], ['exception', '35.51'], 'negative number and precision' => [-31500, '-31415.92654, -2'], + 'issue 4213 #1' => [250.0, '249.9975, 0'], + 'issue 4213 #2' => [-250.0, '-249.9975, 0'], ]; diff --git a/tests/data/Reader/CSV/linend.mac.csv b/tests/data/Reader/CSV/linend.mac.csv new file mode 100644 index 000000000..88ee3ed45 --- /dev/null +++ b/tests/data/Reader/CSV/linend.mac.csv @@ -0,0 +1 @@ +A,1 2,3 \ No newline at end of file diff --git a/tests/data/Reader/CSV/linend.unix.csv b/tests/data/Reader/CSV/linend.unix.csv new file mode 100644 index 000000000..54746af89 --- /dev/null +++ b/tests/data/Reader/CSV/linend.unix.csv @@ -0,0 +1,2 @@ +A,1 +2,3 diff --git a/tests/data/Reader/CSV/linend.win.csv b/tests/data/Reader/CSV/linend.win.csv new file mode 100644 index 000000000..badb9dadf --- /dev/null +++ b/tests/data/Reader/CSV/linend.win.csv @@ -0,0 +1,2 @@ +A,1 +2,3 diff --git a/tests/data/Reader/XLSX/ebcdic.dontuse b/tests/data/Reader/XLSX/ebcdic.dontuse new file mode 100644 index 000000000..9d366bd12 Binary files /dev/null and b/tests/data/Reader/XLSX/ebcdic.dontuse differ diff --git a/tests/data/Reader/XLSX/utf16be.bom.xlsx b/tests/data/Reader/XLSX/utf16be.bom.xlsx new file mode 100644 index 000000000..d18c3600e Binary files /dev/null and b/tests/data/Reader/XLSX/utf16be.bom.xlsx differ diff --git a/tests/data/Reader/XLSX/utf16be.xlsx b/tests/data/Reader/XLSX/utf16be.xlsx new file mode 100644 index 000000000..6bcaf0767 Binary files /dev/null and b/tests/data/Reader/XLSX/utf16be.xlsx differ diff --git a/tests/data/Reader/XLSX/utf16entity.dontuse b/tests/data/Reader/XLSX/utf16entity.dontuse new file mode 100644 index 000000000..2462cb870 Binary files /dev/null and b/tests/data/Reader/XLSX/utf16entity.dontuse differ diff --git a/tests/data/Reader/XLSX/utf7quoteorder.dontuse b/tests/data/Reader/XLSX/utf7quoteorder.dontuse new file mode 100644 index 000000000..1ad4adab2 Binary files /dev/null and b/tests/data/Reader/XLSX/utf7quoteorder.dontuse differ diff --git a/tests/data/Reader/XLSX/utf8and16.dontuse b/tests/data/Reader/XLSX/utf8and16.dontuse new file mode 100644 index 000000000..e95fecdc0 Binary files /dev/null and b/tests/data/Reader/XLSX/utf8and16.dontuse differ diff --git a/tests/data/Reader/XLSX/utf8and16.entity.dontuse b/tests/data/Reader/XLSX/utf8and16.entity.dontuse new file mode 100644 index 000000000..b604458d0 Binary files /dev/null and b/tests/data/Reader/XLSX/utf8and16.entity.dontuse differ diff --git a/tests/data/Reader/Xml/issue.2157.small.xml b/tests/data/Reader/Xml/issue.2157.small.xml new file mode 100644 index 000000000..93b831f34 --- /dev/null +++ b/tests/data/Reader/Xml/issue.2157.small.xml @@ -0,0 +1,109 @@ + + + + + +id +isbn +barcode +url +title +short_description +description +category +weight +pages +publication_date +3d_photo +photo +pdf_preview +flipping_book +offer +active +availability +size +country +subject +retail_price +wholesale_price +ebook-isbn +ebook-barcode +ebook-retail_price +ebook-wholesale_price +ebook-urn +ebook-drm +ebook-format +ebook-active +author1-name +author1-url +author1-photo +author1-description +author1-link +author2-name +author2-url +author2-photo +author2-description +author2-link +translator-name +translator-photo +translator-description +editor-name +editor-photo +editor-description + + +2 +978-960-364-004-2 +9789603640042 +https://www.dioptra.gr/vivlio/ygeia-diatrofi/teleia-orasi-xoris-gualia/ +Τέλεια όραση χωρίς γυαλιά +Με τη μέθοδο Bates βελτιώνουμε την όρασή μας ή προλαμβάνουμε τα προβλήματα του πιο ευαίσθητου οργάνου του σώματος – των ματιών. +Φορώντας τα γυαλιά «καταλαβαίνεις αμέσως τη διαφορά». Δεν νοιάζεσαι αν θα χρειαστείς αργότερα πιο δυνατούς φακούς, γιατί νομίζεις ότι δεν υπάρχει θεραπεία για τα ταλαιπωρημένα μάτια.</br> +Κι όμως τα πράγματα δεν είναι έτσι.</br> +Οι περισσότεροι από αυτούς που φορούν γυαλιά θα μπορούσαν να τα είχαν αποφύγει. Ακόμα και τώρα μπορούν να βελτιώσουν την όρασή τους με τη μέθοδο Bates.</br> +Η μέθοδος Bates είναι μια σειρά ασκήσεων και τεχνικών που χαλαρώνουν τους οφθαλμολογικούς μυς και τους επανεκπαιδεύουν να εστιάζουν αποτελεσματικά τις φωτεινές ακτίνες που εισέρχονται στο μάτι, επιτρέποντάς μας να βλέπουμε καλά και καθαρά χωρίς γυαλιά.</br> +Εφαρμόστε σήμερα κιόλας, τη μέθοδο Bates για να βελτιώσετε την όρασή σας ή να προλάβετε τα προβλήματα του πιο ευαίσθητου οργάνου του σώματος – των ματιών σας.</br> +Υγεία - Διατροφή +0.272 +160 +2002-06-03 00:00:00 +https://www.dioptra.gr/Images/Products/004_list.jpg +https://www.dioptra.gr/Images/Products/004.jpg + + +0 +1 +1 +140 x 205 +Ελλάδα +- +13.19 +12.44 +- +- +- +- +- +- +- +- +George Kypreotakis +https://www.dioptra.gr/suggrafeas/171/ + + + +- +- +- +- +- +- +- +- +- +- +- + +
+
+