Merge branch 'PHPOffice:master' into Table-for-Xlsx

This commit is contained in:
aswinkumar863 2022-04-23 18:31:13 +05:30 committed by GitHub
commit 1d99dc8d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 678 additions and 303 deletions

View File

@ -26,6 +26,20 @@ $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
// add code that show the issue here... // 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? ### Which versions of PhpSpreadsheet and PHP are affected?

View File

@ -3,14 +3,21 @@ This is:
``` ```
- [ ] a bugfix - [ ] a bugfix
- [ ] a new feature - [ ] a new feature
- [ ] refactoring
- [ ] additional unit tests
``` ```
Checklist: Checklist:
- [ ] Changes are covered by unit tests - [ ] Changes are covered by unit tests
- [ ] Changes are covered by existing unit tests
- [ ] New unit tests have been added
- [ ] Code style is respected - [ ] Code style is respected
- [ ] Commit message explains **why** the change is made (see https://github.com/erlang/otp/wiki/Writing-good-commit-messages) - [ ] 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 - [ ] CHANGELOG.md contains a short summary of the change
- [ ] Documentation is updated as necessary - [ ] Documentation is updated as necessary
### Why this change is needed? ### 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.

View File

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### 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. - 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 FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions.
- Implementation of the ISREF() Information function. - 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. 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: - 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. 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 ### Changed
- Gnumeric Reader now loads number formatting for cells. - Gnumeric Reader now loads number formatting for cells.
@ -66,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed ### 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) - 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) - 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) - 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 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 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) - 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. - 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). 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. 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) - 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) - 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 ## 1.22.0 - 2022-02-18
### Added ### Added

View File

@ -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 other symbols can be used. Because CSV is a text-only format, it doesn't
support any data formatting options. 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 one definition that is commonly used). Rather, in practice the term
"CSV" refers to any file that: "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 international standard (ISO/IEC 15445:2000). HTML 4.01 was published in
late 1999, with further errata published through 2001. In 2004 late 1999, with further errata published through 2001. In 2004
development began on HTML5 in the Web Hypertext Application Technology development began on HTML5 in the Web Hypertext Application Technology
Working Group (WHATWG), which became a joint deliverable with the W3C in Working Group (WHATWG), which became a joint deliverable with the W3C in 2008.
2008.

View File

@ -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 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. 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 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 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 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` See `samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php`
for a working example of this code. 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 ## Spreadsheet Reader Options
Once you have created a reader object for the workbook that you want to 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` See `samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php`
for a working example of this code. 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 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. 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 When setting read data only to true, PhpSpreadsheet doesn't read the

View File

@ -1167,13 +1167,15 @@ that you are setting is measured in.
Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches),
`cm` (centimeters) and `mm` (millimeters). `cm` (centimeters) and `mm` (millimeters).
Setting the column width to `-1` tells MS Excel to display the column using its default width.
```php ```php
$spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(120, 'pt'); $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(120, 'pt');
``` ```
If you want PhpSpreadsheet to perform an automatic width calculation, If you want PhpSpreadsheet to perform an automatic width calculation,
use the following code. PhpSpreadsheet will approximate the column with use the following code. PhpSpreadsheet will approximate the column width
to the width of the widest column value. to the width of the widest value displayed in that column.
```php ```php
$spreadsheet->getActiveSheet()->getColumnDimension('B')->setAutoSize(true); $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'); $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 ## Show/hide a row
To set a worksheet''s row visibility, you can use the following code. To set a worksheet''s row visibility, you can use the following code.

View File

@ -1175,11 +1175,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Cell/Coordinate.php 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\\.$#" message: "#^Parameter \\#4 \\$currentRow of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:validateRange\\(\\) expects int, string given\\.$#"
count: 1 count: 1
@ -3120,11 +3115,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Reader/Xml/Style.php 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\\.$#" message: "#^Elseif condition is always true\\.$#"
count: 1 count: 1
@ -4675,11 +4665,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Writer/Ods/Content.php 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\\.$#" message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<2, max\\> given\\.$#"
count: 3 count: 3
@ -4695,11 +4680,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Writer/Ods/Formula.php 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\\.$#" message: "#^Cannot call method getHashCode\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#"
count: 1 count: 1

View File

@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Functions class Functions
{ {
@ -686,12 +685,13 @@ class Functions
// Uppercase coordinate // Uppercase coordinate
$pCoordinatex = strtoupper($coordinate); $pCoordinatex = strtoupper($coordinate);
// Eliminate leading equal sign // Eliminate leading equal sign
$pCoordinatex = Worksheet::pregReplace('/^=/', '', $pCoordinatex); $pCoordinatex = (string) preg_replace('/^=/', '', $pCoordinatex);
$defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet); $defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet);
if ($defined !== null) { if ($defined !== null) {
$worksheet2 = $defined->getWorkSheet(); $worksheet2 = $defined->getWorkSheet();
if (!$defined->isFormula() && $worksheet2 !== null) { 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 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 public static function trimSheetFromCellReference(string $coordinate): string

View File

@ -383,7 +383,7 @@ abstract class Coordinate
// Sort the result by column and row // Sort the result by column and row
$sortKeys = []; $sortKeys = [];
foreach ($cellList as $coord) { 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; $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
} }
ksort($sortKeys); ksort($sortKeys);

View File

@ -4,8 +4,8 @@ namespace PhpOffice\PhpSpreadsheet\Collection;
use Generator; use Generator;
use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
@ -151,8 +151,6 @@ class Cells
{ {
$sortKeys = []; $sortKeys = [];
foreach ($this->getCoordinates() as $coord) { foreach ($this->getCoordinates() as $coord) {
$column = '';
$row = 0;
sscanf($coord, '%[A-Z]%d', $column, $row); sscanf($coord, '%[A-Z]%d', $column, $row);
$sortKeys[sprintf('%09d%3s', $row, $column)] = $coord; $sortKeys[sprintf('%09d%3s', $row, $column)] = $coord;
} }
@ -172,8 +170,6 @@ class Cells
$col = ['A' => '1A']; $col = ['A' => '1A'];
$row = [1]; $row = [1];
foreach ($this->getCoordinates() as $coord) { foreach ($this->getCoordinates() as $coord) {
$c = '';
$r = 0;
sscanf($coord, '%[A-Z]%d', $c, $r); sscanf($coord, '%[A-Z]%d', $c, $r);
$row[$r] = $r; $row[$r] = $r;
$col[$c] = strlen($c) . $c; $col[$c] = strlen($c) . $c;
@ -206,9 +202,6 @@ class Cells
*/ */
public function getCurrentColumn() public function getCurrentColumn()
{ {
$column = '';
$row = 0;
sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row);
return $column; return $column;
@ -221,9 +214,6 @@ class Cells
*/ */
public function getCurrentRow() public function getCurrentRow()
{ {
$column = '';
$row = 0;
sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row);
return (int) $row; return (int) $row;
@ -240,24 +230,19 @@ class Cells
public function getHighestColumn($row = null) public function getHighestColumn($row = null)
{ {
if ($row === null) { if ($row === null) {
$colRow = $this->getHighestRowAndColumn(); return $this->getHighestRowAndColumn()['column'];
return $colRow['column'];
} }
$columnList = [1]; $maxColumn = '1A';
foreach ($this->getCoordinates() as $coord) { foreach ($this->getCoordinates() as $coord) {
$c = '';
$r = 0;
sscanf($coord, '%[A-Z]%d', $c, $r); sscanf($coord, '%[A-Z]%d', $c, $r);
if ($r != $row) { if ($r != $row) {
continue; 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) public function getHighestRow($column = null)
{ {
if ($column === null) { if ($column === null) {
$colRow = $this->getHighestRowAndColumn(); return $this->getHighestRowAndColumn()['row'];
return $colRow['row'];
} }
$rowList = [0]; $maxRow = 1;
foreach ($this->getCoordinates() as $coord) { foreach ($this->getCoordinates() as $coord) {
$c = '';
$r = 0;
sscanf($coord, '%[A-Z]%d', $c, $r); sscanf($coord, '%[A-Z]%d', $c, $r);
if ($c != $column) { if ($c != $column) {
continue; continue;
} }
$rowList[] = $r; $maxRow = max($maxRow, $r);
} }
return max($rowList); return $maxRow;
} }
/** /**
@ -298,7 +278,9 @@ class Cells
*/ */
private function getUniqueID() 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 public function removeRow($row): void
{ {
foreach ($this->getCoordinates() as $coord) { foreach ($this->getCoordinates() as $coord) {
$c = '';
$r = 0;
sscanf($coord, '%[A-Z]%d', $c, $r); sscanf($coord, '%[A-Z]%d', $c, $r);
if ($r == $row) { if ($r == $row) {
$this->delete($coord); $this->delete($coord);
@ -363,9 +342,6 @@ class Cells
public function removeColumn($column): void public function removeColumn($column): void
{ {
foreach ($this->getCoordinates() as $coord) { foreach ($this->getCoordinates() as $coord) {
$c = '';
$r = 0;
sscanf($coord, '%[A-Z]%d', $c, $r); sscanf($coord, '%[A-Z]%d', $c, $r);
if ($c == $column) { if ($c == $column) {
$this->delete($coord); $this->delete($coord);

View File

@ -115,7 +115,7 @@ class Properties
// Initialise values // Initialise values
$this->lastModifiedBy = $this->creator; $this->lastModifiedBy = $this->creator;
$this->created = self::intOrFloatTimestamp(null); $this->created = self::intOrFloatTimestamp(null);
$this->modified = self::intOrFloatTimestamp(null); $this->modified = $this->created;
} }
/** /**

View File

@ -14,23 +14,39 @@ use PhpOffice\PhpSpreadsheet\Writer\IWriter;
*/ */
abstract class IOFactory 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 = [ private static $readers = [
'Xlsx' => Reader\Xlsx::class, self::READER_XLSX => Reader\Xlsx::class,
'Xls' => Reader\Xls::class, self::READER_XLS => Reader\Xls::class,
'Xml' => Reader\Xml::class, self::READER_XML => Reader\Xml::class,
'Ods' => Reader\Ods::class, self::READER_ODS => Reader\Ods::class,
'Slk' => Reader\Slk::class, self::READER_SLK => Reader\Slk::class,
'Gnumeric' => Reader\Gnumeric::class, self::READER_GNUMERIC => Reader\Gnumeric::class,
'Html' => Reader\Html::class, self::READER_HTML => Reader\Html::class,
'Csv' => Reader\Csv::class, self::READER_CSV => Reader\Csv::class,
]; ];
private static $writers = [ private static $writers = [
'Xls' => Writer\Xls::class, self::WRITER_XLS => Writer\Xls::class,
'Xlsx' => Writer\Xlsx::class, self::WRITER_XLSX => Writer\Xlsx::class,
'Ods' => Writer\Ods::class, self::WRITER_ODS => Writer\Ods::class,
'Csv' => Writer\Csv::class, self::WRITER_CSV => Writer\Csv::class,
'Html' => Writer\Html::class, self::WRITER_HTML => Writer\Html::class,
'Tcpdf' => Writer\Pdf\Tcpdf::class, 'Tcpdf' => Writer\Pdf\Tcpdf::class,
'Dompdf' => Writer\Pdf\Dompdf::class, 'Dompdf' => Writer\Pdf\Dompdf::class,
'Mpdf' => Writer\Pdf\Mpdf::class, 'Mpdf' => Writer\Pdf\Mpdf::class,
@ -70,10 +86,18 @@ abstract class IOFactory
* Loads Spreadsheet from file using automatic Reader\IReader resolution. * Loads Spreadsheet from file using automatic Reader\IReader resolution.
* *
* @param string $filename The name of the spreadsheet file * @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); return $reader->load($filename, $flags);
} }
@ -81,9 +105,9 @@ abstract class IOFactory
/** /**
* Identify file type using automatic IReader resolution. * 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); $className = get_class($reader);
$classType = explode('\\', $className); $classType = explode('\\', $className);
unset($reader); unset($reader);
@ -93,14 +117,32 @@ abstract class IOFactory
/** /**
* Create Reader\IReader for file using automatic IReader resolution. * 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); 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 // First, lucky guess by inspecting file extension
$guessedReader = self::getReaderTypeFromExtension($filename); $guessedReader = self::getReaderTypeFromExtension($filename);
if ($guessedReader !== null) { if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) {
$reader = self::createReader($guessedReader); $reader = self::createReader($guessedReader);
// Let's see if we are lucky // 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 // If we reach here then "lucky guess" didn't give any result
// Try walking through all the options in self::$autoResolveClasses // Try walking through all the options in self::$readers (or the selected subset)
foreach (self::$readers as $type => $class) { foreach ($testReaders as $readerType => $class) {
// Ignore our original guess, we know that won't work // Ignore our original guess, we know that won't work
if ($type !== $guessedReader) { if ($readerType !== $guessedReader) {
$reader = self::createReader($type); $reader = self::createReader($readerType);
if ($reader->canRead($filename)) { if ($reader->canRead($filename)) {
return $reader; return $reader;
} }

View File

@ -153,6 +153,10 @@ abstract class BaseReader implements IReader
/** /**
* Loads Spreadsheet from file. * 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 public function load(string $filename, int $flags = 0): Spreadsheet
{ {

View File

@ -3759,8 +3759,13 @@ class Xls extends BaseReader
} else { } else {
$textRun = $richText->createTextRun($text); $textRun = $richText->createTextRun($text);
if (isset($fmtRuns[$i - 1])) { if (isset($fmtRuns[$i - 1])) {
if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
$fontIndex = $fmtRuns[$i - 1]['fontIndex']; $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) { if (array_key_exists($fontIndex, $this->objFonts) === false) {
$fontIndex = count($this->objFonts) - 1; $fontIndex = count($this->objFonts) - 1;
} }

View File

@ -368,6 +368,9 @@ class Xlsx extends BaseReader
if (strpos($fileName, '//') !== false) { if (strpos($fileName, '//') !== false) {
$fileName = substr($fileName, strpos($fileName, '//') + 1); $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); $fileName = File::realpath($fileName);
// Sadly, some 3rd party xlsx generators don't use consistent case for filenaming // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming

View File

@ -91,8 +91,8 @@ class ReferenceHelper
*/ */
public static function cellSort($a, $b) public static function cellSort($a, $b)
{ {
[$ac, $ar] = sscanf($a, '%[A-Z]%d'); sscanf($a, '%[A-Z]%d', $ac, $ar);
[$bc, $br] = sscanf($b, '%[A-Z]%d'); sscanf($b, '%[A-Z]%d', $bc, $br);
if ($ar === $br) { if ($ar === $br) {
return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
@ -112,8 +112,8 @@ class ReferenceHelper
*/ */
public static function cellReverseSort($a, $b) public static function cellReverseSort($a, $b)
{ {
[$ac, $ar] = sscanf($a, '%[A-Z]%d'); sscanf($a, '%[A-Z]%d', $ac, $ar);
[$bc, $br] = sscanf($b, '%[A-Z]%d'); sscanf($b, '%[A-Z]%d', $bc, $br);
if ($ar === $br) { if ($ar === $br) {
return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);

View File

@ -222,11 +222,15 @@ class Font
* @param RichText|string $cellText Text to calculate width * @param RichText|string $cellText Text to calculate width
* @param int $rotation Rotation angle * @param int $rotation Rotation angle
* @param null|FontStyle $defaultFont Font object * @param null|FontStyle $defaultFont Font object
* * @param bool $filterAdjustment Add space for Autofilter or Table dropdown
* @return int Column width
*/ */
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 it is rich text, use plain text
if ($cellText instanceof RichText) { if ($cellText instanceof RichText) {
$cellText = $cellText->getPlainText(); $cellText = $cellText->getPlainText();
@ -237,7 +241,7 @@ class Font
$lineTexts = explode("\n", $cellText); $lineTexts = explode("\n", $cellText);
$lineWidths = []; $lineWidths = [];
foreach ($lineTexts as $lineText) { 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 return max($lineWidths); // width of longest line in cell
@ -247,7 +251,13 @@ class Font
$approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX; $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX;
$columnWidth = 0; $columnWidth = 0;
if (!$approximate) { 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 { try {
// Width of text in pixels excl. padding // Width of text in pixels excl. padding
@ -259,7 +269,11 @@ class Font
} }
if ($approximate) { 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 // Width of text in pixels excl. padding, approximation
// and addition because Excel adds some padding, just use approx width of 'n' glyph // and addition because Excel adds some padding, just use approx width of 'n' glyph
$columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust; $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust;

View File

@ -7,6 +7,7 @@ use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Date;
@ -50,9 +51,18 @@ class AutoFilter
/** /**
* Create a new AutoFilter. * Create a new AutoFilter.
*
* @param AddressRange|array<int>|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->range = $range;
$this->workSheet = $worksheet; $this->workSheet = $worksheet;
} }
@ -92,12 +102,19 @@ class AutoFilter
/** /**
* Set AutoFilter Cell Range. * Set AutoFilter Cell Range.
*
* @param AddressRange|array<int>|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; $this->evaluated = false;
// extract coordinate // extract coordinate
[$worksheet, $range] = Worksheet::extractSheetTitle($range, true); if ($range !== '') {
[, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
}
if (empty($range)) { if (empty($range)) {
// Discard all column rules // Discard all column rules
$this->columns = []; $this->columns = [];

View File

@ -83,9 +83,10 @@ class ColumnDimension extends Dimension
/** /**
* Get Width. * Get Width.
* *
* Each unit of column width is equal to the width of one character in the default font size. * Each unit of column width is equal to the width of one character in the default font size. A value of -1
* By default, this will be the return value; but this method also accepts a unit of measure argument and will * tells Excel to display this column in its default width.
* return the value converted to the specified UoM using an approximation method. * 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 public function getWidth(?string $unitOfMeasure = null): float
{ {
@ -97,9 +98,11 @@ class ColumnDimension extends Dimension
/** /**
* Set Width. * Set Width.
* *
* Each unit of column width is equal to the width of one character in the default font size. * Each unit of column width is equal to the width of one character in the default font size. A value of -1
* By default, this will be the unit of measure for the passed value; but this method accepts a unit of measure * tells Excel to display this column in its default width.
* argument, and will convert the value from the specified UoM using an approximation method. * 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 * @return $this
*/ */

View File

@ -65,8 +65,9 @@ class RowDimension extends Dimension
/** /**
* Get Row Height. * Get Row Height.
* By default, this will be in points; but this method accepts a unit of measure * By default, this will be in points; but this method also accepts an optional unit of measure
* argument, and will convert the value to the specified UoM. * 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 * @return float
*/ */
@ -80,8 +81,8 @@ class RowDimension extends Dimension
/** /**
* Set Row Height. * Set Row Height.
* *
* @param float $height in points * @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 accepts a unit of measure * 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 * argument, and will convert the passed argument value to points from the specified UoM
* *
* @return $this * @return $this

View File

@ -0,0 +1,100 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
class Validations
{
/**
* Validate a cell address.
*
* @param null|array<int>|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<int>|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<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 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;
}
}

View File

@ -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 // loop through all cells in the worksheet
foreach ($this->getCoordinates(false) as $coordinate) { foreach ($this->getCoordinates(false) as $coordinate) {
$cell = $this->getCellOrNull($coordinate); $cell = $this->getCellOrNull($coordinate);
if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) { if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
//Determine if cell is in merge range //Determine if cell is in merge range
$isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]); $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) { 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 // Calculated value
// To formatted string // To formatted string
$cellValue = NumberFormat::toFormattedString( $cellValue = NumberFormat::toFormattedString(
$cell->getCalculatedValue(), $cell->getCalculatedValue(),
$this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode() $this->getParent()->getCellXfByIndex($cell->getXfIndex())
->getNumberFormat()->getFormatCode()
); );
if ($cellValue !== null && $cellValue !== '') { if ($cellValue !== null && $cellValue !== '') {
@ -777,8 +795,10 @@ class Worksheet implements IComparable
(float) Shared\Font::calculateColumnWidth( (float) Shared\Font::calculateColumnWidth(
$this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(), $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(),
$cellValue, $cellValue,
$this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(), $this->getParent()->getCellXfByIndex($cell->getXfIndex())
$this->getParent()->getDefaultStyle()->getFont() ->getAlignment()->getTextRotation(),
$this->getParent()->getDefaultStyle()->getFont(),
$filterAdjustment
) )
); );
} }
@ -1057,7 +1077,7 @@ class Worksheet implements IComparable
*/ */
public function getHighestColumn($row = null) public function getHighestColumn($row = null)
{ {
if (empty($row)) { if ($row === null) {
return Coordinate::stringFromColumnIndex($this->cachedHighestColumn); return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
} }
@ -1087,7 +1107,7 @@ class Worksheet implements IComparable
*/ */
public function getHighestRow($column = null) public function getHighestRow($column = null)
{ {
if ($column == null) { if ($column === null) {
return $this->cachedHighestRow; return $this->cachedHighestRow;
} }
@ -1117,96 +1137,6 @@ class Worksheet implements IComparable
return $this->cellCollection->getHighestRowAndColumn(); return $this->cellCollection->getHighestRowAndColumn();
} }
/**
* Validate a cell address.
*
* @param null|array<int>|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<int>|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<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 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. * Set a cell value.
* *
@ -1218,7 +1148,7 @@ class Worksheet implements IComparable
*/ */
public function setCellValue($coordinate, $value) public function setCellValue($coordinate, $value)
{ {
$cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
$this->getCell($cellAddress)->setValue($value); $this->getCell($cellAddress)->setValue($value);
return $this; return $this;
@ -1256,7 +1186,7 @@ class Worksheet implements IComparable
*/ */
public function setCellValueExplicit($coordinate, $value, $dataType) public function setCellValueExplicit($coordinate, $value, $dataType)
{ {
$cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
$this->getCell($cellAddress)->setValueExplicit($value, $dataType); $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
return $this; return $this;
@ -1293,7 +1223,7 @@ class Worksheet implements IComparable
*/ */
public function getCell($coordinate): Cell 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 // Shortcut for increased performance for the vast majority of simple cases
if ($this->cellCollection->has($cellAddress)) { if ($this->cellCollection->has($cellAddress)) {
@ -1444,7 +1374,7 @@ class Worksheet implements IComparable
*/ */
public function cellExists($coordinate): bool public function cellExists($coordinate): bool
{ {
$cellAddress = $this->validateCellAddress($coordinate); $cellAddress = Validations::validateCellAddress($coordinate);
/** @var Worksheet $sheet */ /** @var Worksheet $sheet */
[$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
@ -1536,7 +1466,7 @@ class Worksheet implements IComparable
*/ */
public function getStyle($cellCoordinate): Style public function getStyle($cellCoordinate): Style
{ {
$cellCoordinate = $this->validateCellOrCellRange($cellCoordinate); $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
// set this sheet as active // set this sheet as active
$this->parent->setActiveSheetIndex($this->parent->getIndex($this)); $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
@ -1774,7 +1704,7 @@ class Worksheet implements IComparable
*/ */
public function setBreak($coordinate, $break) public function setBreak($coordinate, $break)
{ {
$cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
if ($break === self::BREAK_NONE) { if ($break === self::BREAK_NONE) {
if (isset($this->breaks[$cellAddress])) { if (isset($this->breaks[$cellAddress])) {
@ -1826,7 +1756,7 @@ class Worksheet implements IComparable
*/ */
public function mergeCells($range) 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) { if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) {
$this->mergeCells[$range] = $range; $this->mergeCells[$range] = $range;
@ -1935,7 +1865,7 @@ class Worksheet implements IComparable
*/ */
public function unmergeCells($range) public function unmergeCells($range)
{ {
$range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
if (strpos($range, ':') !== false) { if (strpos($range, ':') !== false) {
if (isset($this->mergeCells[$range])) { if (isset($this->mergeCells[$range])) {
@ -2013,7 +1943,7 @@ class Worksheet implements IComparable
*/ */
public function protectCells($range, $password, $alreadyHashed = false) public function protectCells($range, $password, $alreadyHashed = false)
{ {
$range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
if (!$alreadyHashed) { if (!$alreadyHashed) {
$password = Shared\PasswordHasher::hashPassword($password); $password = Shared\PasswordHasher::hashPassword($password);
@ -2061,7 +1991,7 @@ class Worksheet implements IComparable
*/ */
public function unprotectCells($range) public function unprotectCells($range)
{ {
$range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
if (isset($this->protectedCells[$range])) { if (isset($this->protectedCells[$range])) {
unset($this->protectedCells[$range]); unset($this->protectedCells[$range]);
@ -2132,7 +2062,7 @@ class Worksheet implements IComparable
if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) { if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
$this->autoFilter = $autoFilterOrRange; $this->autoFilter = $autoFilterOrRange;
} else { } else {
$cellRange = Functions::trimSheetFromCellReference($this->validateCellRange($autoFilterOrRange)); $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
$this->autoFilter->setRange($cellRange); $this->autoFilter->setRange($cellRange);
} }
@ -2278,13 +2208,13 @@ class Worksheet implements IComparable
public function freezePane($coordinate, $topLeftCell = null) public function freezePane($coordinate, $topLeftCell = null)
{ {
$cellAddress = ($coordinate !== null) $cellAddress = ($coordinate !== null)
? Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)) ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
: null; : null;
if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) { if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
throw new Exception('Freeze pane can not be set on a range of cells.'); throw new Exception('Freeze pane can not be set on a range of cells.');
} }
$topLeftCell = ($topLeftCell !== null) $topLeftCell = ($topLeftCell !== null)
? Functions::trimSheetFromCellReference($this->validateCellAddress($topLeftCell)) ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
: null; : null;
if ($cellAddress !== null && $topLeftCell === null) { if ($cellAddress !== null && $topLeftCell === null) {
@ -2688,7 +2618,7 @@ class Worksheet implements IComparable
*/ */
public function getComment($cellCoordinate) public function getComment($cellCoordinate)
{ {
$cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($cellCoordinate)); $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
if (Coordinate::coordinateIsRange($cellAddress)) { if (Coordinate::coordinateIsRange($cellAddress)) {
throw new Exception('Cell coordinate string can not be a range of cells.'); 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); 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. * Select a range of cells.
* *
@ -2787,9 +2701,9 @@ class Worksheet implements IComparable
public function setSelectedCells($coordinate) public function setSelectedCells($coordinate)
{ {
if (is_string($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)) { if (Coordinate::coordinateIsRange($coordinate)) {
[$first] = Coordinate::splitRange($coordinate); [$first] = Coordinate::splitRange($coordinate);

View File

@ -132,10 +132,11 @@ class Ods extends BaseWriter
$zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write()); $zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write());
$zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPartthumbnails()->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('content.xml', $this->getWriterPartcontent()->write());
$zip->addFile('meta.xml', $this->getWriterPartmeta()->write()); $zip->addFile('meta.xml', $this->getWriterPartmeta()->write());
$zip->addFile('mimetype', $this->getWriterPartmimetype()->write()); $zip->addFile('mimetype', $this->getWriterPartmimetype()->write());
$zip->addFile('settings.xml', $this->getWriterPartsettings()->write());
$zip->addFile('styles.xml', $this->getWriterPartstyles()->write()); $zip->addFile('styles.xml', $this->getWriterPartstyles()->write());
// Close file // Close file

View File

@ -2,15 +2,20 @@
namespace PhpOffice\PhpSpreadsheet\Writer\Ods\Cell; namespace PhpOffice\PhpSpreadsheet\Writer\Ods\Cell;
use PhpOffice\PhpSpreadsheet\Helper\Dimension;
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle;
use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension;
use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension;
class Style class Style
{ {
public const CELL_STYLE_PREFIX = 'ce'; public const CELL_STYLE_PREFIX = 'ce';
public const COLUMN_STYLE_PREFIX = 'co';
public const ROW_STYLE_PREFIX = 'ro';
private $writer; private $writer;
@ -159,6 +164,63 @@ class Style
$this->writer->endElement(); // Close style:text-properties $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 public function write(CellStyle $style): void
{ {
$this->writer->startElement('style:style'); $this->writer->startElement('style:style');

View File

@ -119,14 +119,21 @@ class Content extends WriterPart
{ {
$spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */ $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */
$sheetCount = $spreadsheet->getSheetCount(); $sheetCount = $spreadsheet->getSheetCount();
for ($i = 0; $i < $sheetCount; ++$i) { for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) {
$objWriter->startElement('table:table'); $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->writeElement('office:forms');
foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) {
$objWriter->startElement('table:table-column'); $objWriter->startElement('table:table-column');
$objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); $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(); $objWriter->endElement();
$this->writeRows($objWriter, $spreadsheet->getSheet($i)); }
$this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex);
$objWriter->endElement(); $objWriter->endElement();
} }
} }
@ -134,7 +141,7 @@ class Content extends WriterPart
/** /**
* Write rows of the specified sheet. * 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; $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
$span_row = 0; $span_row = 0;
@ -148,8 +155,14 @@ class Content extends WriterPart
if ($span_row > 1) { if ($span_row > 1) {
$objWriter->writeAttribute('table:number-rows-repeated', $span_row); $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->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();
$objWriter->endElement(); $objWriter->endElement();
$span_row = 0; $span_row = 0;
@ -275,6 +288,26 @@ class Content extends WriterPart
private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void
{ {
$styleWriter = new Style($writer); $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) { foreach ($spreadsheet->getCellXfCollection() as $style) {
$styleWriter->write($style); $styleWriter->write($style);
} }
@ -296,7 +329,7 @@ class Content extends WriterPart
$columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1; $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1;
$rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1; $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1;
$objWriter->writeAttribute('table:number-columns-spanned', $columnSpan); $objWriter->writeAttribute('table:number-columns-spanned', (string) $columnSpan);
$objWriter->writeAttribute('table:number-rows-spanned', $rowSpan); $objWriter->writeAttribute('table:number-rows-spanned', (string) $rowSpan);
} }
} }

View File

@ -2,8 +2,11 @@
namespace PhpOffice\PhpSpreadsheet\Writer\Ods; namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Settings extends WriterPart class Settings extends WriterPart
{ {
@ -45,28 +48,9 @@ class Settings extends WriterPart
$objWriter->text('view1'); $objWriter->text('view1');
$objWriter->endElement(); // ViewId $objWriter->endElement(); // ViewId
$objWriter->startElement('config:config-item-map-named'); $objWriter->startElement('config:config-item-map-named');
$objWriter->writeAttribute('config:name', 'Tables');
foreach ($spreadsheet->getWorksheetIterator() as $ws) { $this->writeAllWorksheetSettings($objWriter, $spreadsheet);
$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
$wstitle = $spreadsheet->getActiveSheet()->getTitle(); $wstitle = $spreadsheet->getActiveSheet()->getTitle();
$objWriter->startElement('config:config-item'); $objWriter->startElement('config:config-item');
$objWriter->writeAttribute('config:name', 'ActiveTable'); $objWriter->writeAttribute('config:name', 'ActiveTable');
@ -85,4 +69,84 @@ class Settings extends WriterPart
return $objWriter->getData(); 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');
}
} }

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheetTests\Document; namespace PhpOffice\PhpSpreadsheetTests\Document;
use DateTime;
use DateTimeZone; use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Date;
@ -14,20 +15,27 @@ class PropertiesTest extends TestCase
*/ */
private $properties; private $properties;
/** @var float */
private $startTime;
protected function setup(): void protected function setup(): void
{ {
do {
// loop to avoid rare situation where timestamp changes
$this->startTime = (float) (new DateTime())->format('U');
$this->properties = new Properties(); $this->properties = new Properties();
$endTime = (float) (new DateTime())->format('U');
} while ($this->startTime !== $endTime);
} }
public function testNewInstance(): void public function testNewInstance(): void
{ {
$createdTime = $modifiedTime = time();
self::assertSame('Unknown Creator', $this->properties->getCreator()); self::assertSame('Unknown Creator', $this->properties->getCreator());
self::assertSame('Unknown Creator', $this->properties->getLastModifiedBy()); self::assertSame('Unknown Creator', $this->properties->getLastModifiedBy());
self::assertSame('Untitled Spreadsheet', $this->properties->getTitle()); self::assertSame('Untitled Spreadsheet', $this->properties->getTitle());
self::assertSame('', $this->properties->getCompany()); self::assertSame('', $this->properties->getCompany());
self::assertSame($createdTime, $this->properties->getCreated()); self::assertEquals($this->startTime, $this->properties->getCreated());
self::assertSame($modifiedTime, $this->properties->getModified()); self::assertEquals($this->startTime, $this->properties->getModified());
} }
public function testSetCreator(): void public function testSetCreator(): void
@ -46,10 +54,10 @@ class PropertiesTest extends TestCase
*/ */
public function testSetCreated($expectedCreationTime, $created): void public function testSetCreated($expectedCreationTime, $created): void
{ {
$expectedCreationTime = $expectedCreationTime ?? time(); $expectedCreationTime = $expectedCreationTime ?? $this->startTime;
$this->properties->setCreated($created); $this->properties->setCreated($created);
self::assertSame($expectedCreationTime, $this->properties->getCreated()); self::assertEquals($expectedCreationTime, $this->properties->getCreated());
} }
public function providerCreationTime(): array public function providerCreationTime(): array
@ -78,10 +86,10 @@ class PropertiesTest extends TestCase
*/ */
public function testSetModified($expectedModifiedTime, $modified): void public function testSetModified($expectedModifiedTime, $modified): void
{ {
$expectedModifiedTime = $expectedModifiedTime ?? time(); $expectedModifiedTime = $expectedModifiedTime ?? $this->startTime;
$this->properties->setModified($modified); $this->properties->setModified($modified);
self::assertSame($expectedModifiedTime, $this->properties->getModified()); self::assertEquals($expectedModifiedTime, $this->properties->getModified());
} }
public function providerModifiedTime(): array public function providerModifiedTime(): array

View File

@ -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 public function testIdentifyNonExistingFileThrowException(): void
{ {
$this->expectException(ReaderException::class); $this->expectException(ReaderException::class);

View File

@ -0,0 +1,22 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class RichTextSizeTest extends AbstractFunctional
{
public function testRichTextRunSize(): void
{
$filename = 'tests/data/Reader/XLS/RichTextFontSize.xls';
$reader = new Xls();
$spreadsheet = $reader->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();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PHPUnit\Framework\TestCase;
class XlsxRootZipFilesTest extends TestCase
{
/**
* @var string
*/
private static $testbook = 'tests/data/Reader/XLSX/rootZipFiles.xlsx';
public function testXlsxRootZipFiles(): void
{
$filename = self::$testbook;
$reader = new Xlsx();
$spreadsheet = $reader->load($filename);
$sheet = $spreadsheet->getActiveSheet();
$value = $sheet->getCell('A1')->getValue();
self::assertSame('TEST CELL', $value->getPlainText());
}
}

View File

@ -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 private static function verifyA3B2(Calculation $calculation, string $title, ?bool $preCalc, string $type): void
{ {

Binary file not shown.

Binary file not shown.

View File

@ -14,7 +14,6 @@
<table:calculation-settings /> <table:calculation-settings />
<table:table table:name="Worksheet"> <table:table table:name="Worksheet">
<office:forms /> <office:forms />
<table:table-column table:number-columns-repeated="1024" />
<table:table-row> <table:table-row>
<table:table-cell table:style-name="ce0" /> <table:table-cell table:style-name="ce0" />
<table:table-cell table:number-columns-repeated="1023" /> <table:table-cell table:number-columns-repeated="1023" />

View File

@ -64,7 +64,6 @@
<table:calculation-settings/> <table:calculation-settings/>
<table:table table:name="Worksheet"> <table:table table:name="Worksheet">
<office:forms/> <office:forms/>
<table:table-column table:number-columns-repeated="1024"/>
<table:table-row> <table:table-row>
<table:table-cell office:value="1" office:value-type="float" table:style-name="ce2"> <table:table-cell office:value="1" office:value-type="float" table:style-name="ce2">
<text:p>1</text:p> <text:p>1</text:p>
@ -110,7 +109,6 @@
</table:table> </table:table>
<table:table table:name="New Worksheet"> <table:table table:name="New Worksheet">
<office:forms/> <office:forms/>
<table:table-column table:number-columns-repeated="1024"/>
<table:table-row> <table:table-row>
<table:table-cell office:value="2" office:value-type="float" table:style-name="ce0"> <table:table-cell office:value="2" office:value-type="float" table:style-name="ce0">
<text:p>2</text:p> <text:p>2</text:p>