diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 05e3b199..c3a74ede 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -26,6 +26,20 @@ $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); // add code that show the issue here... ``` +If this is an issue with reading a specific spreadsheet file, then it may be appropriate to provide a sample file that demonstrates the problem; but please keep it as small as possible, and sanitize any confidential information before uploading. + +### What features do you think are causing the issue + +- [ ] Reader +- [ ] Writer +- [ ] Styles +- [ ] Data Validations +- [ ] Formula Calulations +- [ ] Charts +- [ ] AutoFilter +- [ ] Form Elements + +### Does an issue affect all spreadsheet file formats? If not, which formats are affected? ### Which versions of PhpSpreadsheet and PHP are affected? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index eba1f1e2..f42df68c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,14 +3,21 @@ This is: ``` - [ ] a bugfix - [ ] a new feature +- [ ] refactoring +- [ ] additional unit tests ``` Checklist: - [ ] Changes are covered by unit tests + - [ ] Changes are covered by existing unit tests + - [ ] New unit tests have been added - [ ] Code style is respected - [ ] Commit message explains **why** the change is made (see https://github.com/erlang/otp/wiki/Writing-good-commit-messages) - [ ] CHANGELOG.md contains a short summary of the change - [ ] Documentation is updated as necessary ### Why this change is needed? + +Provide an explanation of why this change is needed, with links to any Issues (if appropriate). +If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will make it easier to track progress with this PR. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3912c622..3f63dfa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Ods Writer support for Freeze Pane [Issue #2013](https://github.com/PHPOffice/PhpSpreadsheet/issues/2013) [PR #2755](https://github.com/PHPOffice/PhpSpreadsheet/pull/2755) +- Ods Writer support for setting column width/row height (including the use of AutoSize) [Issue #2346](https://github.com/PHPOffice/PhpSpreadsheet/issues/2346) [PR #2753](https://github.com/PHPOffice/PhpSpreadsheet/pull/2753) - Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts. - Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions. - Implementation of the ISREF() Information function. @@ -18,11 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators. -- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) +- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) [#2674](https://github.com/PHPOffice/PhpSpreadsheet/pull/2674) - Limited support for Xls Reader to handle Conditional Formatting: Ranges and Rules are read, but style is currently limited to font size, weight and color; and to fill style and color. +- Add ability to suppress Mac line ending check for CSV [#2623](https://github.com/PHPOffice/PhpSpreadsheet/pull/2623) + ### Changed - Gnumeric Reader now loads number formatting for cells. @@ -66,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Make allowance for the AutoFilter dropdown icon in the first row of an Autofilter range when using Autosize columns. [Issue #2413](https://github.com/PHPOffice/PhpSpreadsheet/issues/2413) [PR #2754](https://github.com/PHPOffice/PhpSpreadsheet/pull/2754) - Support for "chained" ranges (e.g. `A5:C10:C20:F1`) in the Calculation Engine; and also support for using named ranges with the Range operator (e.g. `NamedRange1:NamedRange2`) [Issue #2730](https://github.com/PHPOffice/PhpSpreadsheet/issues/2730) [PR #2746](https://github.com/PHPOffice/PhpSpreadsheet/pull/2746) - Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689) - Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) @@ -73,16 +78,20 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. - Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. - Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669) -- Fixed behaviour of XLSX font style vertical align settings. +- Fixed behaviour of XLSX font style vertical align settings [PR #2619](https://github.com/PHPOffice/PhpSpreadsheet/pull/2619) - Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels. Note that this method is used when translating Excel functions between `en_us` and other locale languages, as well as when converting formulae between different spreadsheet formats (e.g. Ods to Excel). Nor is this a perfect solution, as there may still be issues when function calls have array arguments that themselves contain function calls; but it's still better than the current logic. - Fix for escaping double quotes within a formula [Issue #1971](https://github.com/PHPOffice/PhpSpreadsheet/issues/1971) [PR #2651](https://github.com/PHPOffice/PhpSpreadsheet/pull/2651) +- Change open mode for output from `wb+` to `wb` [Issue #2372](https://github.com/PHPOffice/PhpSpreadsheet/issues/2372) [PR #2657](https://github.com/PHPOffice/PhpSpreadsheet/pull/2657) +- Use color palette if supplied [Issue #2499](https://github.com/PHPOffice/PhpSpreadsheet/issues/2499) [PR #2595](https://github.com/PHPOffice/PhpSpreadsheet/pull/2595) +- Xls reader treat drawing offsets as int rather than float [PR #2648](https://github.com/PHPOffice/PhpSpreadsheet/pull/2648) +- Handle booleans in conditional styles properly [PR #2654](https://github.com/PHPOffice/PhpSpreadsheet/pull/2654) +- Fix for reading files in the root directory of a ZipFile, which should not be prefixed by relative paths ("./") as dirname($filename) does by default. - Fix invalid style of cells in empty columns with columnDimensions and rows with rowDimensions in added external sheet. [PR #2739](https://github.com/PHPOffice/PhpSpreadsheet/pull/2739) - ## 1.22.0 - 2022-02-18 ### Added diff --git a/docs/topics/file-formats.md b/docs/topics/file-formats.md index 6f8783e6..7318b136 100644 --- a/docs/topics/file-formats.md +++ b/docs/topics/file-formats.md @@ -80,7 +80,8 @@ semi-colon (`;`) are used as separators instead of a comma, although other symbols can be used. Because CSV is a text-only format, it doesn't support any data formatting options. -"CSV" is not a single, well-defined format (although see RFC 4180 for +"CSV" is not a single, well-defined format (although see +[RFC 4180](https://www.rfc-editor.org/rfc/rfc4180.html) for one definition that is commonly used). Rather, in practice the term "CSV" refers to any file that: @@ -117,5 +118,5 @@ Wide Web Consortium (W3C). However, in 2000, HTML also became an international standard (ISO/IEC 15445:2000). HTML 4.01 was published in late 1999, with further errata published through 2001. In 2004 development began on HTML5 in the Web Hypertext Application Technology -Working Group (WHATWG), which became a joint deliverable with the W3C in -2008. +Working Group (WHATWG), which became a joint deliverable with the W3C in 2008. + diff --git a/docs/topics/reading-files.md b/docs/topics/reading-files.md index 38428166..76705281 100644 --- a/docs/topics/reading-files.md +++ b/docs/topics/reading-files.md @@ -44,6 +44,22 @@ practise), it will reject the Xls loader that it would normally use for a .xls file; and test the file using the other loaders until it finds the appropriate loader, and then use that to read the file. +If you know that this is an `xls` file, but don't know whether it is a +genuine BIFF-format Excel or Html markup with an xls extension, you can +limit the loader to check only those two possibilities by passing in an +array of Readers to test against. + +```php +$inputFileName = './sampleData/example1.xls'; +$testAgainstFormats = [ + \PhpOffice\PhpSpreadsheet\IOFactory::READER_XLS, + \PhpOffice\PhpSpreadsheet\IOFactory::READER_HTML, +]; + +/** Load $inputFileName to a Spreadsheet Object **/ +$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($inputFileName, 0, $testAgainstFormats); +``` + While easy to implement in your code, and you don't need to worry about the file type; this isn't the most efficient method to load a file; and it lacks the flexibility to configure the loader in any way before @@ -118,6 +134,34 @@ $spreadsheet = $reader->load($inputFileName); See `samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php` for a working example of this code. +As with the IOFactory `load()` method, you can also pass an array of formats +for the `identify()` method to check against if you know that it will only +be in a subset of the possible formats that PhpSpreadsheet supports. + +```php +$inputFileName = './sampleData/example1.xls'; +$testAgainstFormats = [ + \PhpOffice\PhpSpreadsheet\IOFactory::READER_XLS, + \PhpOffice\PhpSpreadsheet\IOFactory::READER_HTML, +]; + +/** Identify the type of $inputFileName **/ +$inputFileType = \PhpOffice\PhpSpreadsheet\IOFactory::identify($inputFileName, $testAgainstFormats); +``` + +You can also use this to confirm that a file is what it claims to be: + +```php +$inputFileName = './sampleData/example1.xls'; + +try { + /** Verify that $inputFileName really is an Xls file **/ + $inputFileType = \PhpOffice\PhpSpreadsheet\IOFactory::identify($inputFileName, [\PhpOffice\PhpSpreadsheet\IOFactory::READER_XLS]); +} catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) { + // File isn't actually an Xls file, even though it has an xls extension +} +``` + ## Spreadsheet Reader Options Once you have created a reader object for the workbook that you want to @@ -146,7 +190,7 @@ $spreadsheet = $reader->load($inputFileName); See `samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php` for a working example of this code. -It is important to note that Workbooks (and PhpSpreadsheet) store dates +It is important to note that most Workbooks (and PhpSpreadsheet) store dates and times as simple numeric values: they can only be distinguished from other numeric values by the format mask that is applied to that cell. When setting read data only to true, PhpSpreadsheet doesn't read the @@ -162,8 +206,8 @@ Reading Only Data from a Spreadsheet File applies to Readers: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | YES | Xls | YES | Xml | YES | -Ods | YES | SYLK | NO | Gnumeric | YES | +Xlsx | YES | Xls | YES | Xml | YES | +Ods | YES | SYLK | NO | Gnumeric | YES | CSV | NO | HTML | NO ### Reading Only Named WorkSheets from a File @@ -233,8 +277,8 @@ Reading Only Named WorkSheets from a File applies to Readers: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | YES | Xls | YES | Xml | YES | -Ods | YES | SYLK | NO | Gnumeric | YES | +Xlsx | YES | Xls | YES | Xml | YES | +Ods | YES | SYLK | NO | Gnumeric | YES | CSV | NO | HTML | NO ### Reading Only Specific Columns and Rows from a File (Read Filters) @@ -381,7 +425,7 @@ Using Read Filters applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | YES | Xls | YES | Xml | YES | +Xlsx | YES | Xls | YES | Xml | YES | Ods | YES | SYLK | NO | Gnumeric | YES | CSV | YES | HTML | NO | | | @@ -439,7 +483,7 @@ Combining Multiple Files into a Single Spreadsheet Object applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | YES | Gnumeric | NO | CSV | YES | HTML | NO @@ -516,7 +560,7 @@ Splitting a single loaded file across multiple worksheets applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | NO | Gnumeric | NO | CSV | YES | HTML | NO @@ -556,7 +600,7 @@ Setting CSV delimiter applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | NO | Gnumeric | NO | CSV | YES | HTML | NO @@ -594,7 +638,7 @@ Applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | NO | Gnumeric | NO | CSV | YES | HTML | NO @@ -646,7 +690,7 @@ Loading using a Value Binder applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N ----------|:---:|--------|:---:|--------------|:---: -Xlsx | NO | Xls | NO | Xml | NO +Xlsx | NO | Xls | NO | Xml | NO Ods | NO | SYLK | NO | Gnumeric | NO CSV | YES | HTML | YES diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 404f8823..f25a9119 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1167,13 +1167,15 @@ that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), `cm` (centimeters) and `mm` (millimeters). +Setting the column width to `-1` tells MS Excel to display the column using its default width. + ```php $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(120, 'pt'); ``` If you want PhpSpreadsheet to perform an automatic width calculation, -use the following code. PhpSpreadsheet will approximate the column with -to the width of the widest column value. +use the following code. PhpSpreadsheet will approximate the column width +to the width of the widest value displayed in that column. ```php $spreadsheet->getActiveSheet()->getColumnDimension('B')->setAutoSize(true); @@ -1266,6 +1268,18 @@ Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), $spreadsheet->getActiveSheet()->getRowDimension('10')->setRowHeight(100, 'pt'); ``` +Setting the row height to `-1` tells MS Excel to display the column using its default height, which is based on the character font size. + +If you have wrapped text in a cell, then the `-1` default will only set the row height to display a single line of that wrapped text. +If you need to calculate the actual height for the row, then count the lines that should be displayed (count the `\n` and add 1); then adjust for the font. +The adjustment for Calibri 11 is approximately 14.5; for Calibri 12 15.9, etc. +```php +$spreadsheet->getActiveSheet()->getRowDimension(1)->setRowHeight( + 14.5 * (substr_count($sheet->getCell('A1')->getValue(), "\n") + 1) +); +``` + + ## Show/hide a row To set a worksheet''s row visibility, you can use the following code. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3b739947..8c1642ff 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1175,11 +1175,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Cell/Coordinate.php - - - message: "#^Cannot use array destructuring on array\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Coordinate.php - - message: "#^Parameter \\#4 \\$currentRow of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:validateRange\\(\\) expects int, string given\\.$#" count: 1 @@ -3120,11 +3115,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xml/Style.php - - - message: "#^Cannot use array destructuring on array\\|null\\.$#" - count: 4 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Elseif condition is always true\\.$#" count: 1 @@ -4675,11 +4665,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Ods/Content.php - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 4 - path: src/PhpSpreadsheet/Writer/Ods/Content.php - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<2, max\\> given\\.$#" count: 3 @@ -4695,11 +4680,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Ods/Formula.php - - - message: "#^Parameter \\#1 \\$content of method XMLWriter\\:\\:text\\(\\) expects string, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Ods/Settings.php - - message: "#^Cannot call method getHashCode\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 00d8a790..dc6ee82e 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Shared\Date; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Functions { @@ -686,12 +685,13 @@ class Functions // Uppercase coordinate $pCoordinatex = strtoupper($coordinate); // Eliminate leading equal sign - $pCoordinatex = Worksheet::pregReplace('/^=/', '', $pCoordinatex); + $pCoordinatex = (string) preg_replace('/^=/', '', $pCoordinatex); $defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet); if ($defined !== null) { $worksheet2 = $defined->getWorkSheet(); if (!$defined->isFormula() && $worksheet2 !== null) { - $coordinate = "'" . $worksheet2->getTitle() . "'!" . Worksheet::pregReplace('/^=/', '', $defined->getValue()); + $coordinate = "'" . $worksheet2->getTitle() . "'!" . + (string) preg_replace('/^=/', '', $defined->getValue()); } } @@ -700,7 +700,7 @@ class Functions public static function trimTrailingRange(string $coordinate): string { - return Worksheet::pregReplace('/:[\\w\$]+$/', '', $coordinate); + return (string) preg_replace('/:[\\w\$]+$/', '', $coordinate); } public static function trimSheetFromCellReference(string $coordinate): string diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 703eac08..ea53919c 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -383,7 +383,7 @@ abstract class Coordinate // Sort the result by column and row $sortKeys = []; foreach ($cellList as $coord) { - [$column, $row] = sscanf($coord, '%[A-Z]%d'); + sscanf($coord, '%[A-Z]%d', $column, $row); $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord; } ksort($sortKeys); diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index e3d81cb4..d49f5457 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -4,8 +4,8 @@ namespace PhpOffice\PhpSpreadsheet\Collection; use Generator; use PhpOffice\PhpSpreadsheet\Cell\Cell; -use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Psr\SimpleCache\CacheInterface; @@ -151,8 +151,6 @@ class Cells { $sortKeys = []; foreach ($this->getCoordinates() as $coord) { - $column = ''; - $row = 0; sscanf($coord, '%[A-Z]%d', $column, $row); $sortKeys[sprintf('%09d%3s', $row, $column)] = $coord; } @@ -172,8 +170,6 @@ class Cells $col = ['A' => '1A']; $row = [1]; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; sscanf($coord, '%[A-Z]%d', $c, $r); $row[$r] = $r; $col[$c] = strlen($c) . $c; @@ -206,9 +202,6 @@ class Cells */ public function getCurrentColumn() { - $column = ''; - $row = 0; - sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); return $column; @@ -221,9 +214,6 @@ class Cells */ public function getCurrentRow() { - $column = ''; - $row = 0; - sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); return (int) $row; @@ -240,24 +230,19 @@ class Cells public function getHighestColumn($row = null) { if ($row === null) { - $colRow = $this->getHighestRowAndColumn(); - - return $colRow['column']; + return $this->getHighestRowAndColumn()['column']; } - $columnList = [1]; + $maxColumn = '1A'; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($r != $row) { continue; } - $columnList[] = Coordinate::columnIndexFromString($c); + $maxColumn = max($maxColumn, strlen($c) . $c); } - return Coordinate::stringFromColumnIndex((int) @max($columnList)); + return substr($maxColumn, 1); } /** @@ -271,24 +256,19 @@ class Cells public function getHighestRow($column = null) { if ($column === null) { - $colRow = $this->getHighestRowAndColumn(); - - return $colRow['row']; + return $this->getHighestRowAndColumn()['row']; } - $rowList = [0]; + $maxRow = 1; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($c != $column) { continue; } - $rowList[] = $r; + $maxRow = max($maxRow, $r); } - return max($rowList); + return $maxRow; } /** @@ -298,7 +278,9 @@ class Cells */ private function getUniqueID() { - return uniqid('phpspreadsheet.', true) . '.'; + return Settings::getCache() instanceof Memory + ? random_bytes(7) . ':' + : uniqid('phpspreadsheet.', true) . '.'; } /** @@ -345,9 +327,6 @@ class Cells public function removeRow($row): void { foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($r == $row) { $this->delete($coord); @@ -363,9 +342,6 @@ class Cells public function removeColumn($column): void { foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($c == $column) { $this->delete($coord); diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 3be5a67a..2d461c59 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -115,7 +115,7 @@ class Properties // Initialise values $this->lastModifiedBy = $this->creator; $this->created = self::intOrFloatTimestamp(null); - $this->modified = self::intOrFloatTimestamp(null); + $this->modified = $this->created; } /** diff --git a/src/PhpSpreadsheet/IOFactory.php b/src/PhpSpreadsheet/IOFactory.php index 91613cb4..e437a220 100644 --- a/src/PhpSpreadsheet/IOFactory.php +++ b/src/PhpSpreadsheet/IOFactory.php @@ -14,23 +14,39 @@ use PhpOffice\PhpSpreadsheet\Writer\IWriter; */ abstract class IOFactory { + public const READER_XLSX = 'Xlsx'; + public const READER_XLS = 'Xls'; + public const READER_XML = 'Xml'; + public const READER_ODS = 'Ods'; + public const READER_SYLK = 'Slk'; + public const READER_SLK = 'Slk'; + public const READER_GNUMERIC = 'Gnumeric'; + public const READER_HTML = 'Html'; + public const READER_CSV = 'Csv'; + + public const WRITER_XLSX = 'Xlsx'; + public const WRITER_XLS = 'Xls'; + public const WRITER_ODS = 'Ods'; + public const WRITER_CSV = 'Csv'; + public const WRITER_HTML = 'Html'; + private static $readers = [ - 'Xlsx' => Reader\Xlsx::class, - 'Xls' => Reader\Xls::class, - 'Xml' => Reader\Xml::class, - 'Ods' => Reader\Ods::class, - 'Slk' => Reader\Slk::class, - 'Gnumeric' => Reader\Gnumeric::class, - 'Html' => Reader\Html::class, - 'Csv' => Reader\Csv::class, + self::READER_XLSX => Reader\Xlsx::class, + self::READER_XLS => Reader\Xls::class, + self::READER_XML => Reader\Xml::class, + self::READER_ODS => Reader\Ods::class, + self::READER_SLK => Reader\Slk::class, + self::READER_GNUMERIC => Reader\Gnumeric::class, + self::READER_HTML => Reader\Html::class, + self::READER_CSV => Reader\Csv::class, ]; private static $writers = [ - 'Xls' => Writer\Xls::class, - 'Xlsx' => Writer\Xlsx::class, - 'Ods' => Writer\Ods::class, - 'Csv' => Writer\Csv::class, - 'Html' => Writer\Html::class, + self::WRITER_XLS => Writer\Xls::class, + self::WRITER_XLSX => Writer\Xlsx::class, + self::WRITER_ODS => Writer\Ods::class, + self::WRITER_CSV => Writer\Csv::class, + self::WRITER_HTML => Writer\Html::class, 'Tcpdf' => Writer\Pdf\Tcpdf::class, 'Dompdf' => Writer\Pdf\Dompdf::class, 'Mpdf' => Writer\Pdf\Mpdf::class, @@ -70,10 +86,18 @@ abstract class IOFactory * Loads Spreadsheet from file using automatic Reader\IReader resolution. * * @param string $filename The name of the spreadsheet file + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. */ - public static function load(string $filename, int $flags = 0): Spreadsheet + public static function load(string $filename, int $flags = 0, ?array $readers = null): Spreadsheet { - $reader = self::createReaderForFile($filename); + $reader = self::createReaderForFile($filename, $readers); return $reader->load($filename, $flags); } @@ -81,9 +105,9 @@ abstract class IOFactory /** * Identify file type using automatic IReader resolution. */ - public static function identify(string $filename): string + public static function identify(string $filename, ?array $readers = null): string { - $reader = self::createReaderForFile($filename); + $reader = self::createReaderForFile($filename, $readers); $className = get_class($reader); $classType = explode('\\', $className); unset($reader); @@ -93,14 +117,32 @@ abstract class IOFactory /** * Create Reader\IReader for file using automatic IReader resolution. + * + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. */ - public static function createReaderForFile(string $filename): IReader + public static function createReaderForFile(string $filename, ?array $readers = null): IReader { File::assertFile($filename); + $testReaders = self::$readers; + if ($readers !== null) { + $readers = array_map('strtoupper', $readers); + $testReaders = array_filter( + self::$readers, + function (string $readerType) use ($readers) { + return in_array(strtoupper($readerType), $readers, true); + }, + ARRAY_FILTER_USE_KEY + ); + } + // First, lucky guess by inspecting file extension $guessedReader = self::getReaderTypeFromExtension($filename); - if ($guessedReader !== null) { + if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) { $reader = self::createReader($guessedReader); // Let's see if we are lucky @@ -110,11 +152,11 @@ abstract class IOFactory } // If we reach here then "lucky guess" didn't give any result - // Try walking through all the options in self::$autoResolveClasses - foreach (self::$readers as $type => $class) { + // Try walking through all the options in self::$readers (or the selected subset) + foreach ($testReaders as $readerType => $class) { // Ignore our original guess, we know that won't work - if ($type !== $guessedReader) { - $reader = self::createReader($type); + if ($readerType !== $guessedReader) { + $reader = self::createReader($readerType); if ($reader->canRead($filename)) { return $reader; } diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php index c215e65b..a137e78c 100644 --- a/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/BaseReader.php @@ -153,6 +153,10 @@ abstract class BaseReader implements IReader /** * Loads Spreadsheet from file. + * + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file */ public function load(string $filename, int $flags = 0): Spreadsheet { diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 402fea9f..d1f14ae1 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -3759,8 +3759,13 @@ class Xls extends BaseReader } else { $textRun = $richText->createTextRun($text); if (isset($fmtRuns[$i - 1])) { - $fontIndex = $fmtRuns[$i - 1]['fontIndex']; - + if ($fmtRuns[$i - 1]['fontIndex'] < 4) { + $fontIndex = $fmtRuns[$i - 1]['fontIndex']; + } else { + // this has to do with that index 4 is omitted in all BIFF versions for some stra nge reason + // check the OpenOffice documentation of the FONT record + $fontIndex = $fmtRuns[$i - 1]['fontIndex'] - 1; + } if (array_key_exists($fontIndex, $this->objFonts) === false) { $fontIndex = count($this->objFonts) - 1; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 18ed7987..a6e7fe03 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -368,6 +368,9 @@ class Xlsx extends BaseReader if (strpos($fileName, '//') !== false) { $fileName = substr($fileName, strpos($fileName, '//') + 1); } + // Relative paths generated by dirname($filename) when $filename + // has no path (i.e.files in root of the zip archive) + $fileName = (string) preg_replace('/^\.\//', '', $fileName); $fileName = File::realpath($fileName); // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 378a9a5a..4a6ee039 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -91,8 +91,8 @@ class ReferenceHelper */ public static function cellSort($a, $b) { - [$ac, $ar] = sscanf($a, '%[A-Z]%d'); - [$bc, $br] = sscanf($b, '%[A-Z]%d'); + sscanf($a, '%[A-Z]%d', $ac, $ar); + sscanf($b, '%[A-Z]%d', $bc, $br); if ($ar === $br) { return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); @@ -112,8 +112,8 @@ class ReferenceHelper */ public static function cellReverseSort($a, $b) { - [$ac, $ar] = sscanf($a, '%[A-Z]%d'); - [$bc, $br] = sscanf($b, '%[A-Z]%d'); + sscanf($a, '%[A-Z]%d', $ac, $ar); + sscanf($b, '%[A-Z]%d', $bc, $br); if ($ar === $br) { return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 9a74befe..3ee3d5a6 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -222,11 +222,15 @@ class Font * @param RichText|string $cellText Text to calculate width * @param int $rotation Rotation angle * @param null|FontStyle $defaultFont Font object - * - * @return int Column width + * @param bool $filterAdjustment Add space for Autofilter or Table dropdown */ - public static function calculateColumnWidth(FontStyle $font, $cellText = '', $rotation = 0, ?FontStyle $defaultFont = null) - { + public static function calculateColumnWidth( + FontStyle $font, + $cellText = '', + $rotation = 0, + ?FontStyle $defaultFont = null, + bool $filterAdjustment = false + ): int { // If it is rich text, use plain text if ($cellText instanceof RichText) { $cellText = $cellText->getPlainText(); @@ -237,7 +241,7 @@ class Font $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { - $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont); + $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont, $filterAdjustment); } return max($lineWidths); // width of longest line in cell @@ -247,7 +251,13 @@ class Font $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX; $columnWidth = 0; if (!$approximate) { - $columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07); + $columnWidthAdjust = ceil( + self::getTextWidthPixelsExact( + str_repeat('n', 1 * ($filterAdjustment ? 3 : 1)), + $font, + 0 + ) * 1.07 + ); try { // Width of text in pixels excl. padding @@ -259,7 +269,11 @@ class Font } if ($approximate) { - $columnWidthAdjust = self::getTextWidthPixelsApprox('n', $font, 0); + $columnWidthAdjust = self::getTextWidthPixelsApprox( + str_repeat('n', 1 * ($filterAdjustment ? 3 : 1)), + $font, + 0 + ); // Width of text in pixels excl. padding, approximation // and addition because Excel adds some padding, just use approx width of 'n' glyph $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust; diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 2e6c8289..dd33d5d5 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -7,6 +7,7 @@ use DateTimeZone; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -50,9 +51,18 @@ class AutoFilter /** * Create a new AutoFilter. + * + * @param AddressRange|array|string $range + * A simple string containing a Cell range like 'A1:E10' is permitted + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. */ - public function __construct(string $range = '', ?Worksheet $worksheet = null) + public function __construct($range = '', ?Worksheet $worksheet = null) { + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } + $this->range = $range; $this->workSheet = $worksheet; } @@ -92,12 +102,19 @@ class AutoFilter /** * Set AutoFilter Cell Range. + * + * @param AddressRange|array|string $range + * A simple string containing a Cell range like 'A1:E10' is permitted + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. */ - public function setRange(string $range): self + public function setRange($range = ''): self { $this->evaluated = false; // extract coordinate - [$worksheet, $range] = Worksheet::extractSheetTitle($range, true); + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } if (empty($range)) { // Discard all column rules $this->columns = []; diff --git a/src/PhpSpreadsheet/Worksheet/ColumnDimension.php b/src/PhpSpreadsheet/Worksheet/ColumnDimension.php index 17dd261b..b64ecec9 100644 --- a/src/PhpSpreadsheet/Worksheet/ColumnDimension.php +++ b/src/PhpSpreadsheet/Worksheet/ColumnDimension.php @@ -83,9 +83,10 @@ class ColumnDimension extends Dimension /** * Get Width. * - * Each unit of column width is equal to the width of one character in the default font size. - * By default, this will be the return value; but this method also accepts a unit of measure argument and will - * return the value converted to the specified UoM using an approximation method. + * Each unit of column width is equal to the width of one character in the default font size. A value of -1 + * tells Excel to display this column in its default width. + * By default, this will be the return value; but this method also accepts an optional unit of measure argument + * and will convert the returned value to the specified UoM.. */ public function getWidth(?string $unitOfMeasure = null): float { @@ -97,9 +98,11 @@ class ColumnDimension extends Dimension /** * Set Width. * - * Each unit of column width is equal to the width of one character in the default font size. - * By default, this will be the unit of measure for the passed value; but this method accepts a unit of measure - * argument, and will convert the value from the specified UoM using an approximation method. + * Each unit of column width is equal to the width of one character in the default font size. A value of -1 + * tells Excel to display this column in its default width. + * By default, this will be the unit of measure for the passed value; but this method also accepts an + * optional unit of measure argument, and will convert the value from the specified UoM using an + * approximation method. * * @return $this */ diff --git a/src/PhpSpreadsheet/Worksheet/RowDimension.php b/src/PhpSpreadsheet/Worksheet/RowDimension.php index 1d8aada8..acaafac5 100644 --- a/src/PhpSpreadsheet/Worksheet/RowDimension.php +++ b/src/PhpSpreadsheet/Worksheet/RowDimension.php @@ -65,8 +65,9 @@ class RowDimension extends Dimension /** * Get Row Height. - * By default, this will be in points; but this method accepts a unit of measure - * argument, and will convert the value to the specified UoM. + * By default, this will be in points; but this method also accepts an optional unit of measure + * argument, and will convert the value from points to the specified UoM. + * A value of -1 tells Excel to display this column in its default height. * * @return float */ @@ -80,8 +81,8 @@ class RowDimension extends Dimension /** * Set Row Height. * - * @param float $height in points - * By default, this will be the passed argument value; but this method accepts a unit of measure + * @param float $height in points. A value of -1 tells Excel to display this column in its default height. + * By default, this will be the passed argument value; but this method also accepts an optional unit of measure * argument, and will convert the passed argument value to points from the specified UoM * * @return $this diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php new file mode 100644 index 00000000..b31b0813 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -0,0 +1,100 @@ +|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + */ + public static function validateCellAddress($cellAddress): string + { + if (is_string($cellAddress)) { + [$worksheet, $address] = Worksheet::extractSheetTitle($cellAddress, true); +// if (!empty($worksheet) && $worksheet !== $this->getTitle()) { +// throw new Exception('Reference is not for this worksheet'); +// } + + return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address); + } + + if (is_array($cellAddress)) { + $cellAddress = CellAddress::fromColumnRowArray($cellAddress); + } + + return (string) $cellAddress; + } + + /** + * Validate a cell address or cell range. + * + * @param AddressRange|array|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as a CellAddress or AddressRange object. + */ + public static function validateCellOrCellRange($cellRange): string + { + if (is_string($cellRange) || is_numeric($cellRange)) { + $cellRange = (string) $cellRange; + // Convert a single column reference like 'A' to 'A:A' + $cellRange = (string) preg_replace('/^([A-Z]+)$/', '${1}:${1}', $cellRange); + // Convert a single row reference like '1' to '1:1' + $cellRange = (string) preg_replace('/^(\d+)$/', '${1}:${1}', $cellRange); + } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { + $cellRange = new CellRange($cellRange, $cellRange); + } + + return self::validateCellRange($cellRange); + } + + /** + * Validate a cell range. + * + * @param AddressRange|array|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as an AddressRange object. + */ + public static function validateCellRange($cellRange): string + { + if (is_string($cellRange)) { + [$worksheet, $addressRange] = Worksheet::extractSheetTitle($cellRange, true); + + // Convert Column ranges like 'A:C' to 'A1:C1048576' + $addressRange = (string) preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); + // Convert Row ranges like '1:3' to 'A1:XFD3' + $addressRange = (string) preg_replace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); + + return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); + } + + if (is_array($cellRange)) { + [$from, $to] = array_chunk($cellRange, 2); + $cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to)); + } + + return (string) $cellRange; + } + + public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet): string + { + // Uppercase coordinate + $testCoordinate = strtoupper($coordinate); + // Eliminate leading equal sign + $testCoordinate = (string) preg_replace('/^=/', '', $coordinate); + $defined = $worksheet->getParent()->getDefinedName($testCoordinate, $worksheet); + if ($defined !== null) { + if ($defined->getWorksheet() === $worksheet && !$defined->isFormula()) { + $coordinate = (string) preg_replace('/^=/', '', $defined->getValue()); + } + } + + return $coordinate; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 6f04d2ba..80f51b44 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -743,9 +743,19 @@ class Worksheet implements IComparable } } + $autoFilterRange = $autoFilterFirstRowRange = $this->autoFilter->getRange(); + if (!empty($autoFilterRange)) { + $autoFilterRangeBoundaries = Coordinate::rangeBoundaries($autoFilterRange); + $autoFilterFirstRowRange = (string) new CellRange( + CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[0][0], $autoFilterRangeBoundaries[0][1]), + CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[1][0], $autoFilterRangeBoundaries[0][1]) + ); + } + // loop through all cells in the worksheet foreach ($this->getCoordinates(false) as $coordinate) { $cell = $this->getCellOrNull($coordinate); + if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) { //Determine if cell is in merge range $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]); @@ -762,13 +772,21 @@ class Worksheet implements IComparable } } - // Determine width if cell does not participate in a merge or does and is a value cell of 1-column wide range + // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range if (!$isMerged || $isMergedButProceed) { + // Determine if we need to make an adjustment for the first row in an AutoFilter range that + // has a column filter dropdown + $filterAdjustment = false; + if (!empty($autoFilterRange) && $cell->isInRange($autoFilterFirstRowRange)) { + $filterAdjustment = true; + } + // Calculated value // To formatted string $cellValue = NumberFormat::toFormattedString( $cell->getCalculatedValue(), - $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode() + $this->getParent()->getCellXfByIndex($cell->getXfIndex()) + ->getNumberFormat()->getFormatCode() ); if ($cellValue !== null && $cellValue !== '') { @@ -777,8 +795,10 @@ class Worksheet implements IComparable (float) Shared\Font::calculateColumnWidth( $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(), $cellValue, - $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(), - $this->getParent()->getDefaultStyle()->getFont() + $this->getParent()->getCellXfByIndex($cell->getXfIndex()) + ->getAlignment()->getTextRotation(), + $this->getParent()->getDefaultStyle()->getFont(), + $filterAdjustment ) ); } @@ -1057,7 +1077,7 @@ class Worksheet implements IComparable */ public function getHighestColumn($row = null) { - if (empty($row)) { + if ($row === null) { return Coordinate::stringFromColumnIndex($this->cachedHighestColumn); } @@ -1087,7 +1107,7 @@ class Worksheet implements IComparable */ public function getHighestRow($column = null) { - if ($column == null) { + if ($column === null) { return $this->cachedHighestRow; } @@ -1117,96 +1137,6 @@ class Worksheet implements IComparable return $this->cellCollection->getHighestRowAndColumn(); } - /** - * Validate a cell address. - * - * @param null|array|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5'; - * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. - */ - protected function validateCellAddress($cellAddress): string - { - if (is_string($cellAddress)) { - [$worksheet, $address] = self::extractSheetTitle($cellAddress, true); -// if (!empty($worksheet) && $worksheet !== $this->getTitle()) { -// throw new Exception('Reference is not for this worksheet'); -// } - - return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address); - } - - if (is_array($cellAddress)) { - $cellAddress = CellAddress::fromColumnRowArray($cellAddress); - } - - return (string) $cellAddress; - } - - private function tryDefinedName(string $coordinate): string - { - // Uppercase coordinate - $coordinate = strtoupper($coordinate); - // Eliminate leading equal sign - $coordinate = self::pregReplace('/^=/', '', $coordinate); - $defined = $this->parent->getDefinedName($coordinate, $this); - if ($defined !== null) { - if ($defined->getWorksheet() === $this && !$defined->isFormula()) { - $coordinate = self::pregReplace('/^=/', '', $defined->getValue()); - } - } - - return $coordinate; - } - - /** - * Validate a cell address or cell range. - * - * @param AddressRange|array|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; - * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), - * or as a CellAddress or AddressRange object. - */ - protected function validateCellOrCellRange($cellRange): string - { - if (is_string($cellRange) || is_numeric($cellRange)) { - $cellRange = (string) $cellRange; - // Convert a single column reference like 'A' to 'A:A' - $cellRange = self::pregReplace('/^([A-Z]+)$/', '${1}:${1}', $cellRange); - // Convert a single row reference like '1' to '1:1' - $cellRange = self::pregReplace('/^(\d+)$/', '${1}:${1}', $cellRange); - } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { - $cellRange = new CellRange($cellRange, $cellRange); - } - - return $this->validateCellRange($cellRange); - } - - /** - * Validate a cell range. - * - * @param AddressRange|array|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; - * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), - * or as an AddressRange object. - */ - protected function validateCellRange($cellRange): string - { - if (is_string($cellRange)) { - [$worksheet, $addressRange] = self::extractSheetTitle($cellRange, true); - - // Convert Column ranges like 'A:C' to 'A1:C1048576' - $addressRange = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); - // Convert Row ranges like '1:3' to 'A1:XFD3' - $addressRange = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); - - return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); - } - - if (is_array($cellRange)) { - [$from, $to] = array_chunk($cellRange, 2); - $cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to)); - } - - return (string) $cellRange; - } - /** * Set a cell value. * @@ -1218,7 +1148,7 @@ class Worksheet implements IComparable */ public function setCellValue($coordinate, $value) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); $this->getCell($cellAddress)->setValue($value); return $this; @@ -1256,7 +1186,7 @@ class Worksheet implements IComparable */ public function setCellValueExplicit($coordinate, $value, $dataType) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); $this->getCell($cellAddress)->setValueExplicit($value, $dataType); return $this; @@ -1293,7 +1223,7 @@ class Worksheet implements IComparable */ public function getCell($coordinate): Cell { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); // Shortcut for increased performance for the vast majority of simple cases if ($this->cellCollection->has($cellAddress)) { @@ -1444,7 +1374,7 @@ class Worksheet implements IComparable */ public function cellExists($coordinate): bool { - $cellAddress = $this->validateCellAddress($coordinate); + $cellAddress = Validations::validateCellAddress($coordinate); /** @var Worksheet $sheet */ [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); @@ -1536,7 +1466,7 @@ class Worksheet implements IComparable */ public function getStyle($cellCoordinate): Style { - $cellCoordinate = $this->validateCellOrCellRange($cellCoordinate); + $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate); // set this sheet as active $this->parent->setActiveSheetIndex($this->parent->getIndex($this)); @@ -1774,7 +1704,7 @@ class Worksheet implements IComparable */ public function setBreak($coordinate, $break) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); if ($break === self::BREAK_NONE) { if (isset($this->breaks[$cellAddress])) { @@ -1826,7 +1756,7 @@ class Worksheet implements IComparable */ public function mergeCells($range) { - $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) { $this->mergeCells[$range] = $range; @@ -1935,7 +1865,7 @@ class Worksheet implements IComparable */ public function unmergeCells($range) { - $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); if (strpos($range, ':') !== false) { if (isset($this->mergeCells[$range])) { @@ -2013,7 +1943,7 @@ class Worksheet implements IComparable */ public function protectCells($range, $password, $alreadyHashed = false) { - $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range)); if (!$alreadyHashed) { $password = Shared\PasswordHasher::hashPassword($password); @@ -2061,7 +1991,7 @@ class Worksheet implements IComparable */ public function unprotectCells($range) { - $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range)); if (isset($this->protectedCells[$range])) { unset($this->protectedCells[$range]); @@ -2132,7 +2062,7 @@ class Worksheet implements IComparable if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) { $this->autoFilter = $autoFilterOrRange; } else { - $cellRange = Functions::trimSheetFromCellReference($this->validateCellRange($autoFilterOrRange)); + $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange)); $this->autoFilter->setRange($cellRange); } @@ -2278,13 +2208,13 @@ class Worksheet implements IComparable public function freezePane($coordinate, $topLeftCell = null) { $cellAddress = ($coordinate !== null) - ? Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)) + ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)) : null; if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Freeze pane can not be set on a range of cells.'); } $topLeftCell = ($topLeftCell !== null) - ? Functions::trimSheetFromCellReference($this->validateCellAddress($topLeftCell)) + ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell)) : null; if ($cellAddress !== null && $topLeftCell === null) { @@ -2688,7 +2618,7 @@ class Worksheet implements IComparable */ public function getComment($cellCoordinate) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($cellCoordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); if (Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Cell coordinate string can not be a range of cells.'); @@ -2759,22 +2689,6 @@ class Worksheet implements IComparable return $this->setSelectedCells($coordinate); } - /** - * Sigh - Phpstan thinks, correctly, that preg_replace can return null. - * But Scrutinizer doesn't. Try to satisfy both. - * - * @param mixed $str - */ - private static function ensureString($str): string - { - return is_string($str) ? $str : ''; - } - - public static function pregReplace(string $pattern, string $replacement, string $subject): string - { - return self::ensureString(preg_replace($pattern, $replacement, $subject)); - } - /** * Select a range of cells. * @@ -2787,9 +2701,9 @@ class Worksheet implements IComparable public function setSelectedCells($coordinate) { if (is_string($coordinate)) { - $coordinate = $this->tryDefinedName($coordinate); + $coordinate = Validations::definedNameToCoordinate($coordinate, $this); } - $coordinate = $this->validateCellOrCellRange($coordinate); + $coordinate = Validations::validateCellOrCellRange($coordinate); if (Coordinate::coordinateIsRange($coordinate)) { [$first] = Coordinate::splitRange($coordinate); diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index decd82bc..827c43b5 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -132,10 +132,11 @@ class Ods extends BaseWriter $zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write()); $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPartthumbnails()->write()); + // Settings always need to be written before Content; Styles after Content + $zip->addFile('settings.xml', $this->getWriterPartsettings()->write()); $zip->addFile('content.xml', $this->getWriterPartcontent()->write()); $zip->addFile('meta.xml', $this->getWriterPartmeta()->write()); $zip->addFile('mimetype', $this->getWriterPartmimetype()->write()); - $zip->addFile('settings.xml', $this->getWriterPartsettings()->write()); $zip->addFile('styles.xml', $this->getWriterPartstyles()->write()); // Close file diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index f8aae20c..a3629bd6 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -2,15 +2,20 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods\Cell; +use PhpOffice\PhpSpreadsheet\Helper\Dimension; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; +use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; +use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; class Style { public const CELL_STYLE_PREFIX = 'ce'; + public const COLUMN_STYLE_PREFIX = 'co'; + public const ROW_STYLE_PREFIX = 'ro'; private $writer; @@ -159,6 +164,63 @@ class Style $this->writer->endElement(); // Close style:text-properties } + protected function writeColumnProperties(ColumnDimension $columnDimension): void + { + $this->writer->startElement('style:table-column-properties'); + $this->writer->writeAttribute( + 'style:column-width', + round($columnDimension->getWidth(Dimension::UOM_CENTIMETERS), 3) . 'cm' + ); + $this->writer->writeAttribute('fo:break-before', 'auto'); + + // End + $this->writer->endElement(); // Close style:table-column-properties + } + + public function writeColumnStyles(ColumnDimension $columnDimension, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table-column'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s_%d_%d', self::COLUMN_STYLE_PREFIX, $sheetId, $columnDimension->getColumnNumeric()) + ); + + $this->writeColumnProperties($columnDimension); + + // End + $this->writer->endElement(); // Close style:style + } + + protected function writeRowProperties(RowDimension $rowDimension): void + { + $this->writer->startElement('style:table-row-properties'); + $this->writer->writeAttribute( + 'style:row-height', + round($rowDimension->getRowHeight(Dimension::UOM_CENTIMETERS), 3) . 'cm' + ); + $this->writer->writeAttribute('style:use-optimal-row-height', 'true'); + $this->writer->writeAttribute('fo:break-before', 'auto'); + + // End + $this->writer->endElement(); // Close style:table-row-properties + } + + public function writeRowStyles(RowDimension $rowDimension, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table-row'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s_%d_%d', self::ROW_STYLE_PREFIX, $sheetId, $rowDimension->getRowIndex()) + ); + + $this->writeRowProperties($rowDimension); + + // End + $this->writer->endElement(); // Close style:style + } + public function write(CellStyle $style): void { $this->writer->startElement('style:style'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index a589e549..5d227c84 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -119,14 +119,21 @@ class Content extends WriterPart { $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */ $sheetCount = $spreadsheet->getSheetCount(); - for ($i = 0; $i < $sheetCount; ++$i) { + for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) { $objWriter->startElement('table:table'); - $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle()); + $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); $objWriter->writeElement('office:forms'); - $objWriter->startElement('table:table-column'); - $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); - $objWriter->endElement(); - $this->writeRows($objWriter, $spreadsheet->getSheet($i)); + foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { + $objWriter->startElement('table:table-column'); + $objWriter->writeAttribute( + 'table:style-name', + sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric()) + ); + $objWriter->writeAttribute('table:default-cell-style-name', 'ce0'); +// $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); + $objWriter->endElement(); + } + $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex); $objWriter->endElement(); } } @@ -134,7 +141,7 @@ class Content extends WriterPart /** * Write rows of the specified sheet. */ - private function writeRows(XMLWriter $objWriter, Worksheet $sheet): void + private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void { $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX; $span_row = 0; @@ -148,8 +155,14 @@ class Content extends WriterPart if ($span_row > 1) { $objWriter->writeAttribute('table:number-rows-repeated', $span_row); } + if ($sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) { + $objWriter->writeAttribute( + 'table:style_name', + sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex()) + ); + } $objWriter->startElement('table:table-cell'); - $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); + $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX); $objWriter->endElement(); $objWriter->endElement(); $span_row = 0; @@ -275,6 +288,26 @@ class Content extends WriterPart private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void { $styleWriter = new Style($writer); + + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $spreadsheet->getSheet($i); + $worksheet->calculateColumnWidths(); + foreach ($worksheet->getColumnDimensions() as $columnDimension) { + if ($columnDimension->getWidth() !== -1.0) { + $styleWriter->writeColumnStyles($columnDimension, $i); + } + } + } + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $spreadsheet->getSheet($i); + foreach ($worksheet->getRowDimensions() as $rowDimension) { + if ($rowDimension->getRowHeight() > 0.0) { + $styleWriter->writeRowStyles($rowDimension, $i); + } + } + } + foreach ($spreadsheet->getCellXfCollection() as $style) { $styleWriter->write($style); } @@ -296,7 +329,7 @@ class Content extends WriterPart $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1; $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1; - $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan); - $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan); + $objWriter->writeAttribute('table:number-columns-spanned', (string) $columnSpan); + $objWriter->writeAttribute('table:number-rows-spanned', (string) $rowSpan); } } diff --git a/src/PhpSpreadsheet/Writer/Ods/Settings.php b/src/PhpSpreadsheet/Writer/Ods/Settings.php index 047bd410..06445591 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Settings.php +++ b/src/PhpSpreadsheet/Writer/Ods/Settings.php @@ -2,8 +2,11 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Cell\CellAddress; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Settings extends WriterPart { @@ -45,28 +48,9 @@ class Settings extends WriterPart $objWriter->text('view1'); $objWriter->endElement(); // ViewId $objWriter->startElement('config:config-item-map-named'); - $objWriter->writeAttribute('config:name', 'Tables'); - foreach ($spreadsheet->getWorksheetIterator() as $ws) { - $objWriter->startElement('config:config-item-map-entry'); - $objWriter->writeAttribute('config:name', $ws->getTitle()); - $selected = $ws->getSelectedCells(); - if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) { - $colSel = Coordinate::columnIndexFromString($matches[1]) - 1; - $rowSel = (int) $matches[2] - 1; - $objWriter->startElement('config:config-item'); - $objWriter->writeAttribute('config:name', 'CursorPositionX'); - $objWriter->writeAttribute('config:type', 'int'); - $objWriter->text($colSel); - $objWriter->endElement(); - $objWriter->startElement('config:config-item'); - $objWriter->writeAttribute('config:name', 'CursorPositionY'); - $objWriter->writeAttribute('config:type', 'int'); - $objWriter->text($rowSel); - $objWriter->endElement(); - } - $objWriter->endElement(); // config:config-item-map-entry - } - $objWriter->endElement(); // config:config-item-map-named + + $this->writeAllWorksheetSettings($objWriter, $spreadsheet); + $wstitle = $spreadsheet->getActiveSheet()->getTitle(); $objWriter->startElement('config:config-item'); $objWriter->writeAttribute('config:name', 'ActiveTable'); @@ -85,4 +69,84 @@ class Settings extends WriterPart return $objWriter->getData(); } + + private function writeAllWorksheetSettings(XMLWriter $objWriter, Spreadsheet $spreadsheet): void + { + $objWriter->writeAttribute('config:name', 'Tables'); + + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $this->writeWorksheetSettings($objWriter, $worksheet); + } + + $objWriter->endElement(); // config:config-item-map-entry Tables + } + + private function writeWorksheetSettings(XMLWriter $objWriter, Worksheet $worksheet): void + { + $objWriter->startElement('config:config-item-map-entry'); + $objWriter->writeAttribute('config:name', $worksheet->getTitle()); + + $this->writeSelectedCells($objWriter, $worksheet); + if ($worksheet->getFreezePane() !== null) { + $this->writeFreezePane($objWriter, $worksheet); + } + + $objWriter->endElement(); // config:config-item-map-entry Worksheet + } + + private function writeSelectedCells(XMLWriter $objWriter, Worksheet $worksheet): void + { + $selected = $worksheet->getSelectedCells(); + if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) { + $colSel = Coordinate::columnIndexFromString($matches[1]) - 1; + $rowSel = (int) $matches[2] - 1; + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'CursorPositionX'); + $objWriter->writeAttribute('config:type', 'int'); + $objWriter->text((string) $colSel); + $objWriter->endElement(); + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'CursorPositionY'); + $objWriter->writeAttribute('config:type', 'int'); + $objWriter->text((string) $rowSel); + $objWriter->endElement(); + } + } + + private function writeSplitValue(XMLWriter $objWriter, string $splitMode, string $type, string $value): void + { + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', $splitMode); + $objWriter->writeAttribute('config:type', $type); + $objWriter->text($value); + $objWriter->endElement(); + } + + private function writeFreezePane(XMLWriter $objWriter, Worksheet $worksheet): void + { + $freezePane = CellAddress::fromCellAddress($worksheet->getFreezePane()); + if ($freezePane->cellAddress() === 'A1') { + return; + } + + $columnId = $freezePane->columnId(); + $columnName = $freezePane->columnName(); + $row = $freezePane->rowId(); + + $this->writeSplitValue($objWriter, 'HorizontalSplitMode', 'short', '2'); + $this->writeSplitValue($objWriter, 'HorizontalSplitPosition', 'int', (string) ($columnId - 1)); + $this->writeSplitValue($objWriter, 'PositionLeft', 'short', '0'); + $this->writeSplitValue($objWriter, 'PositionRight', 'short', (string) ($columnId - 1)); + + for ($column = 'A'; $column !== $columnName; ++$column) { + $worksheet->getColumnDimension($column)->setAutoSize(true); + } + + $this->writeSplitValue($objWriter, 'VerticalSplitMode', 'short', '2'); + $this->writeSplitValue($objWriter, 'VerticalSplitPosition', 'int', (string) ($row - 1)); + $this->writeSplitValue($objWriter, 'PositionTop', 'short', '0'); + $this->writeSplitValue($objWriter, 'PositionBottom', 'short', (string) ($row - 1)); + + $this->writeSplitValue($objWriter, 'ActiveSplitRange', 'short', '3'); + } } diff --git a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php index e6c95cd4..0dfb256b 100644 --- a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Document; +use DateTime; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -14,20 +15,27 @@ class PropertiesTest extends TestCase */ private $properties; + /** @var float */ + private $startTime; + protected function setup(): void { - $this->properties = new Properties(); + do { + // loop to avoid rare situation where timestamp changes + $this->startTime = (float) (new DateTime())->format('U'); + $this->properties = new Properties(); + $endTime = (float) (new DateTime())->format('U'); + } while ($this->startTime !== $endTime); } public function testNewInstance(): void { - $createdTime = $modifiedTime = time(); self::assertSame('Unknown Creator', $this->properties->getCreator()); self::assertSame('Unknown Creator', $this->properties->getLastModifiedBy()); self::assertSame('Untitled Spreadsheet', $this->properties->getTitle()); self::assertSame('', $this->properties->getCompany()); - self::assertSame($createdTime, $this->properties->getCreated()); - self::assertSame($modifiedTime, $this->properties->getModified()); + self::assertEquals($this->startTime, $this->properties->getCreated()); + self::assertEquals($this->startTime, $this->properties->getModified()); } public function testSetCreator(): void @@ -46,10 +54,10 @@ class PropertiesTest extends TestCase */ public function testSetCreated($expectedCreationTime, $created): void { - $expectedCreationTime = $expectedCreationTime ?? time(); + $expectedCreationTime = $expectedCreationTime ?? $this->startTime; $this->properties->setCreated($created); - self::assertSame($expectedCreationTime, $this->properties->getCreated()); + self::assertEquals($expectedCreationTime, $this->properties->getCreated()); } public function providerCreationTime(): array @@ -78,10 +86,10 @@ class PropertiesTest extends TestCase */ public function testSetModified($expectedModifiedTime, $modified): void { - $expectedModifiedTime = $expectedModifiedTime ?? time(); + $expectedModifiedTime = $expectedModifiedTime ?? $this->startTime; $this->properties->setModified($modified); - self::assertSame($expectedModifiedTime, $this->properties->getModified()); + self::assertEquals($expectedModifiedTime, $this->properties->getModified()); } public function providerModifiedTime(): array diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index b516a9f4..19722dc7 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -108,6 +108,22 @@ class IOFactoryTest extends TestCase ]; } + public function testFormatAsExpected(): void + { + $fileName = 'samples/templates/30template.xls'; + + $actual = IOFactory::identify($fileName, [IOFactory::READER_XLS]); + self::assertSame('Xls', $actual); + } + + public function testFormatNotAsExpectedThrowsException(): void + { + $fileName = 'samples/templates/30template.xls'; + + $this->expectException(ReaderException::class); + IOFactory::identify($fileName, [IOFactory::READER_ODS]); + } + public function testIdentifyNonExistingFileThrowException(): void { $this->expectException(ReaderException::class); diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php new file mode 100644 index 00000000..54274e8c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php @@ -0,0 +1,22 @@ +load($filename); + $sheet = $spreadsheet->getSheetByName('橱柜门板'); + self::assertNotNull($sheet); + $text = $sheet->getCell('L15')->getValue(); + $elements = $text->getRichTextElements(); + self::assertEquals(10, $elements[2]->getFont()->getSize()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxRootZipFilesTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxRootZipFilesTest.php new file mode 100644 index 00000000..110c70b0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxRootZipFilesTest.php @@ -0,0 +1,24 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $value = $sheet->getCell('A1')->getValue(); + self::assertSame('TEST CELL', $value->getPlainText()); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php index 2db372c4..73374e21 100644 --- a/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php +++ b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php @@ -64,7 +64,7 @@ class PreCalcTest extends AbstractFunctional } } - private const AUTOSIZE_TYPES = ['Xlsx', 'Xls', 'Html']; + private const AUTOSIZE_TYPES = ['Xlsx', 'Xls', 'Html', 'Ods']; private static function verifyA3B2(Calculation $calculation, string $title, ?bool $preCalc, string $type): void { diff --git a/tests/data/Reader/XLS/RichTextFontSize.xls b/tests/data/Reader/XLS/RichTextFontSize.xls new file mode 100644 index 00000000..3edf4737 Binary files /dev/null and b/tests/data/Reader/XLS/RichTextFontSize.xls differ diff --git a/tests/data/Reader/XLSX/rootZipFiles.xlsx b/tests/data/Reader/XLSX/rootZipFiles.xlsx new file mode 100644 index 00000000..3de12790 Binary files /dev/null and b/tests/data/Reader/XLSX/rootZipFiles.xlsx differ diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index c9620060..867cfa3e 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -14,7 +14,6 @@ - diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index a707d197..fff47f65 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -64,7 +64,6 @@ - 1 @@ -110,7 +109,6 @@ - 2