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...
```
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?

View File

@ -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.

View File

@ -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

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
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.

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
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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
/**

View File

@ -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;
}

View File

@ -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
{

View File

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

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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<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->workSheet = $worksheet;
}
@ -92,12 +102,19 @@ class AutoFilter
/**
* 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;
// 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 = [];

View File

@ -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
*/

View File

@ -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

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
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<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.
*
@ -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);

View File

@ -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

View File

@ -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');

View File

@ -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');
foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) {
$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();
$this->writeRows($objWriter, $spreadsheet->getSheet($i));
}
$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);
}
}

View File

@ -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');
}
}

View File

@ -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
{
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

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
{
$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
{

Binary file not shown.

Binary file not shown.

View File

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

View File

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