From b62de98bf70e42652a50dafd2c2aad6441459054 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 7 Jun 2022 20:22:33 +0200 Subject: [PATCH 1/6] New functionality to allow checking whether a row or column is "empty", with flags to allow variation in the definition of "empty" --- CHANGELOG.md | 4 + phpstan-baseline.neon | 10 -- src/PhpSpreadsheet/Worksheet/CellIterator.php | 4 + src/PhpSpreadsheet/Worksheet/Column.php | 57 ++++++- src/PhpSpreadsheet/Worksheet/Row.php | 41 ++++- src/PhpSpreadsheet/Worksheet/Worksheet.php | 60 +++++++ .../Worksheet/ColumnIteratorEmptyTest.php | 154 ++++++++++++++++++ .../Worksheet/RowIteratorEmptyTest.php | 154 ++++++++++++++++++ .../Worksheet/WorksheetTest.php | 98 +++++++++++ 9 files changed, 566 insertions(+), 16 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 290474a1..503f1121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) - Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) - Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) +- Provide new Worksheet methods to identify if a row or column is "empty", making allowance for different definitions of "empty": + - Treat rows/columns containing no cell records as empty (default) + - Treat cells containing a null value as empty + - Treat cells containing an empty string as empty ### Changed diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 33a3ea69..0a131911 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3655,11 +3655,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/CellIterator.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Column\\:\\:\\$parent \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Column.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\:\\:\\$color \\(PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Color\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Color\\|null\\.$#" count: 1 @@ -3685,11 +3680,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Row\\:\\:\\$worksheet \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Row.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\SheetView\\:\\:\\$sheetViewTypes has no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Worksheet/CellIterator.php b/src/PhpSpreadsheet/Worksheet/CellIterator.php index 142639ce..17286f9c 100644 --- a/src/PhpSpreadsheet/Worksheet/CellIterator.php +++ b/src/PhpSpreadsheet/Worksheet/CellIterator.php @@ -12,6 +12,10 @@ use PhpOffice\PhpSpreadsheet\Collection\Cells; */ abstract class CellIterator implements Iterator { + public const TREAT_NULL_VALUE_AS_EMPTY_CELL = 1; + + public const TREAT_EMPTY_STRING_AS_EMPTY_CELL = 2; + /** * Worksheet to iterate. * diff --git a/src/PhpSpreadsheet/Worksheet/Column.php b/src/PhpSpreadsheet/Worksheet/Column.php index b6f30f1f..e6e332ae 100644 --- a/src/PhpSpreadsheet/Worksheet/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Column.php @@ -9,7 +9,7 @@ class Column * * @var Worksheet */ - private $parent; + private $worksheet; /** * Column index. @@ -23,10 +23,10 @@ class Column * * @param string $columnIndex */ - public function __construct(?Worksheet $parent = null, $columnIndex = 'A') + public function __construct(Worksheet $worksheet, $columnIndex = 'A') { // Set parent and column index - $this->parent = $parent; + $this->worksheet = $worksheet; $this->columnIndex = $columnIndex; } @@ -36,7 +36,7 @@ class Column public function __destruct() { // @phpstan-ignore-next-line - $this->parent = null; + $this->worksheet = null; } /** @@ -57,6 +57,53 @@ class Column */ public function getCellIterator($startRow = 1, $endRow = null) { - return new ColumnCellIterator($this->parent, $this->columnIndex, $startRow, $endRow); + return new ColumnCellIterator($this->worksheet, $this->columnIndex, $startRow, $endRow); + } + + /** + * Returns a boolean true if the column contains no cells. By default, this means that no cell records exist in the + * collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + + /** + * Returns bound worksheet. + */ + public function getWorksheet(): Worksheet + { + return $this->worksheet; } } diff --git a/src/PhpSpreadsheet/Worksheet/Row.php b/src/PhpSpreadsheet/Worksheet/Row.php index a5f8f326..5c162752 100644 --- a/src/PhpSpreadsheet/Worksheet/Row.php +++ b/src/PhpSpreadsheet/Worksheet/Row.php @@ -23,7 +23,7 @@ class Row * * @param int $rowIndex */ - public function __construct(?Worksheet $worksheet = null, $rowIndex = 1) + public function __construct(Worksheet $worksheet, $rowIndex = 1) { // Set parent and row index $this->worksheet = $worksheet; @@ -59,6 +59,45 @@ class Row return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn); } + /** + * Returns a boolean true if the row contains no cells. By default, this means that no cell records exist in the + * collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + /** * Returns bound worksheet. */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 4a441a93..54785bfa 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3232,6 +3232,66 @@ class Worksheet implements IComparable return clone $this; } + /** + * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records + * exist in the collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new RowIterator($this, $rowId, $rowId); + $iterator->seek($rowId); + $row = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $row->isEmpty($definitionOfEmptyFlags); + } + + /** + * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records + * exist in the collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new ColumnIterator($this, $columnId, $columnId); + $iterator->seek($columnId); + $column = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $column->isEmpty($definitionOfEmptyFlags); + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php new file mode 100644 index 00000000..beb87028 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php @@ -0,0 +1,154 @@ +getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('D2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('E2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('E3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('F2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('F3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('H3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H4', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyColumnBasic + */ + public function testIteratorEmptyColumn(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnBasic(): array + { + return [ + ['A', false], + ['B', true], + ['C', false], + ['D', false], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnNullAsEmpty + */ + public function testIteratorEmptyColumnWithNull(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnNullAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', false], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnEmptyStringAsEmpty + */ + public function testIteratorEmptyColumnWithEmptyString(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnEmptyStringAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', false], + ['D', true], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnNullAndEmptyStringAsEmpty + */ + public function testIteratorEmptyColumnWithNullAndEmptyString(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty( + CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + ); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnNullAndEmptyStringAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', true], + ['E', true], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php new file mode 100644 index 00000000..9bee6b59 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php @@ -0,0 +1,154 @@ +getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B3', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('B4', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B5', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C5', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B6', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C6', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B7', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C7', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B8', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C8', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('D8', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyRowBasic + */ + public function testIteratorEmptyRow(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowBasic(): array + { + return [ + [1, false], + [2, true], + [3, false], + [4, false], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowNullAsEmpty + */ + public function testIteratorEmptyRowWithNull(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowNullAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, false], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowEmptyStringAsEmpty + */ + public function testIteratorEmptyRowWithEmptyString(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowEmptyStringAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, false], + [4, true], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowNullAndEmptyStringAsEmpty + */ + public function testIteratorEmptyRowWithNullAndEmptyString(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty( + CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + ); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowNullAndEmptyStringAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, true], + [5, true], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index 5377444d..17de5c32 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -3,7 +3,9 @@ namespace PhpOffice\PhpSpreadsheetTests\Worksheet; use Exception; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\CellIterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -405,4 +407,100 @@ class WorksheetTest extends TestCase self::assertSame($expectedData, $worksheet->toArray()); self::assertSame($expectedHighestRow, $worksheet->getHighestRow()); } + + private static function getPopulatedSheetForEmptyRowTest(Spreadsheet $spreadsheet): Worksheet + { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B3', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('B4', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B5', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C5', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B6', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C6', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B7', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C7', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B8', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C8', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('D8', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + private static function getPopulatedSheetForEmptyColumnTest(Spreadsheet $spreadsheet): Worksheet + { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('D2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('E2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('E3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('F2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('F3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('H3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H4', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyRowProvider + */ + public function testIsEmptyRow(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheetForEmptyRowTest($spreadsheet); + + $isEmpty = $sheet->isEmptyRow($rowId, CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowProvider(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, true], + [5, true], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyColumnProvider + */ + public function testIsEmptyColumn(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheetForEmptyColumnTest($spreadsheet); + + $isEmpty = $sheet->isEmptyColumn($columnId, CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnProvider(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', true], + ['E', true], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } } From 00dae1bbdaec33869d7196552ca4bf194e8b3225 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 10 Jun 2022 01:15:57 +0200 Subject: [PATCH 2/6] Relax validation on merge cells to allow input of a single cell --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 48 ++++++++++--------- .../Calculation/MergedCellTest.php | 5 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 4a441a93..f08ddf0c 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1752,31 +1752,35 @@ class Worksheet implements IComparable { $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); - if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) { - $this->mergeCells[$range] = $range; - $firstRow = (int) $matches[2]; - $lastRow = (int) $matches[4]; - $firstColumn = $matches[1]; - $lastColumn = $matches[3]; - $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn); - $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn); - $numberRows = $lastRow - $firstRow; - $numberColumns = $lastColumnIndex - $firstColumnIndex; + if (strpos($range, ':') === false) { + $range .= ":{$range}"; + } - // create upper left cell if it does not already exist - $upperLeft = "{$firstColumn}{$firstRow}"; - if (!$this->cellExists($upperLeft)) { - $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); - } + if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) { + throw new Exception('Merge must be on a valid range of cells.'); + } - // Blank out the rest of the cells in the range (if they exist) - if ($numberRows > $numberColumns) { - $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft); - } else { - $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft); - } + $this->mergeCells[$range] = $range; + $firstRow = (int) $matches[2]; + $lastRow = (int) $matches[4]; + $firstColumn = $matches[1]; + $lastColumn = $matches[3]; + $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn); + $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn); + $numberRows = $lastRow - $firstRow; + $numberColumns = $lastColumnIndex - $firstColumnIndex; + + // create upper left cell if it does not already exist + $upperLeft = "{$firstColumn}{$firstRow}"; + if (!$this->cellExists($upperLeft)) { + $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); + } + + // Blank out the rest of the cells in the range (if they exist) + if ($numberRows > $numberColumns) { + $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft); } else { - throw new Exception('Merge must be set on a range of cells.'); + $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft); } return $this; diff --git a/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php b/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php index 5e5aff6a..e6737b6d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php @@ -100,7 +100,7 @@ class MergedCellTest extends TestCase $sheet->mergeCells($range); self::fail("Expected invalid merge range $range"); } catch (SpreadException $e) { - self::assertSame('Merge must be set on a range of cells.', $e->getMessage()); + self::assertSame('Merge must be on a valid range of cells.', $e->getMessage()); } } @@ -109,7 +109,8 @@ class MergedCellTest extends TestCase $spreadSheet = new Spreadsheet(); $dataSheet = $spreadSheet->getActiveSheet(); - $this->setBadRange($dataSheet, 'B1'); + // TODO - Reinstate full validation and disallow single cell merging for version 2.0 +// $this->setBadRange($dataSheet, 'B1'); $this->setBadRange($dataSheet, 'Invalid'); $this->setBadRange($dataSheet, '1'); $this->setBadRange($dataSheet, 'C'); From 4f22b39b8963a30de162a66962e7adb5154ee8d3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 Jun 2022 18:08:56 -0700 Subject: [PATCH 3/6] Add Support to Chart/Axis and Gridlines for Shadow (#2872) * Add Support to Chart/Axis and Gridlines for Shadow Continuing the work from #2865. Support is added for Shadow properties for Axis and Gridlines, and Glow and SoftEdges are extended to Gridlines. Tests are added. Some chart tests are moved from Reader/Xlsx and Writer/Xlsx so that most chart tests are under a single directory. This is a minor breaking change. Since the support for these properties was just added, it can't really affect much in userland. Some properties had been stored in the form which the XML requires them rather than as the user would enter them to Excel. So, for example, setting the Glow size to 10 points would have caused it to be stored internally as 127,000. This change will store the size internally as 10, obviously making the appropriate conversion when reading from or writing to XML. This makes unit tests much simpler, and I think this is also what a user would expect, especially considering the difficulties in keeping track of the trailing zeros. * More Tests Confirm value change between internal and xml. * Still More Tests Add a little more coverage, and use a neat trick suggested by @MarkBaker in the discussion of PR #2724 to greatly simplify MultiplierTest. --- phpstan-baseline.neon | 49 +- src/PhpSpreadsheet/Chart/Axis.php | 56 +-- src/PhpSpreadsheet/Chart/GridLines.php | 87 ++-- src/PhpSpreadsheet/Chart/Properties.php | 455 ++++++++++-------- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 124 ++++- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 293 +++++------ .../Chart/AxisGlowTest.php | 36 +- .../Chart/AxisShadowTest.php | 184 +++++++ .../Xlsx => Chart}/Charts32CatAxValAxTest.php | 2 +- .../Charts32ColoredAxisLabelTest.php | 2 +- .../Xlsx => Chart}/Charts32ScatterTest.php | 2 +- .../Xlsx => Chart}/Charts32XmlTest.php | 2 +- .../Xlsx => Chart}/ChartsOpenpyxlTest.php | 2 +- .../Xlsx => Chart}/ChartsTitleTest.php | 2 +- .../Chart/GridlinesShadowGlowTest.php | 187 +++++++ .../Chart/MultiplierTest.php | 157 ++++++ .../Chart/ShadowPresetsTest.php | 183 +++++++ 17 files changed, 1285 insertions(+), 538 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php rename tests/PhpSpreadsheetTests/{Writer/Xlsx => Chart}/Charts32CatAxValAxTest.php (99%) rename tests/PhpSpreadsheetTests/{Writer/Xlsx => Chart}/Charts32ColoredAxisLabelTest.php (98%) rename tests/PhpSpreadsheetTests/{Writer/Xlsx => Chart}/Charts32ScatterTest.php (99%) rename tests/PhpSpreadsheetTests/{Writer/Xlsx => Chart}/Charts32XmlTest.php (98%) rename tests/PhpSpreadsheetTests/{Reader/Xlsx => Chart}/ChartsOpenpyxlTest.php (98%) rename tests/PhpSpreadsheetTests/{Reader/Xlsx => Chart}/ChartsTitleTest.php (97%) create mode 100644 tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/MultiplierTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 33a3ea69..c97b7c43 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1170,21 +1170,11 @@ parameters: count: 2 path: src/PhpSpreadsheet/Chart/DataSeries.php - - - message: "#^Parameter \\#1 \\$angle of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setShadowAngle\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - message: "#^Parameter \\#1 \\$color of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects string, string\\|null given\\.$#" count: 1 path: src/PhpSpreadsheet/Chart/GridLines.php - - - message: "#^Parameter \\#1 \\$distance of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setShadowDistance\\(\\) expects float, float\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - message: "#^Parameter \\#2 \\$alpha of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects int, int\\|null given\\.$#" count: 1 @@ -1275,36 +1265,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Chart/Properties.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getTrueAlpha\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getTrueAlpha\\(\\) has parameter \\$alpha with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:setColorProperties\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:setColorProperties\\(\\) has parameter \\$alpha with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:setColorProperties\\(\\) has parameter \\$color with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:setColorProperties\\(\\) has parameter \\$colorType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has no return type specified\\.$#" count: 1 @@ -4477,12 +4437,12 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, array\\|int\\|string given\\.$#" - count: 8 + count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, array\\|int\\|string\\|null given\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - @@ -4525,11 +4485,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Part \\$xAxis\\-\\>getShadowProperty\\('effect'\\) \\(array\\|int\\|string\\|null\\) of encapsed string cannot be cast to string\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Part \\$xAxis\\-\\>getShadowProperty\\(\\['color', 'type'\\]\\) \\(array\\|int\\|string\\|null\\) of encapsed string cannot be cast to string\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 3a72e725..69607216 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -89,25 +89,7 @@ class Axis extends Properties * * @var mixed[] */ - private $shadowProperties = [ - 'presets' => self::SHADOW_PRESETS_NOSHADOW, - 'effect' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - 'size' => [ - 'sx' => null, - 'sy' => null, - 'kx' => null, - ], - 'blur' => null, - 'direction' => null, - 'distance' => null, - 'algn' => null, - 'rotWithShape' => null, - ]; + private $shadowProperties = Properties::PRESETS_OPTIONS[0]; /** * Glow Properties. @@ -340,6 +322,20 @@ class Axis extends Properties return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'len'); } + /** + * @param mixed $value + */ + public function setShadowProperty(string $propertyName, $value): self + { + if ($propertyName === 'color' && is_array($value)) { + $this->setShadowColor($value['value'], $value['alpha'], $value['type']); + } else { + $this->shadowProperties[$propertyName] = $value; + } + + return $this; + } + /** * Set Shadow Properties. * @@ -379,6 +375,8 @@ class Axis extends Properties return $this; } + private const SHADOW_ARRAY_KEYS = ['size', 'color']; + /** * Set Shadow Properties from Mapped Values. * @@ -391,12 +389,10 @@ class Axis extends Properties $base_reference = $reference; foreach ($propertiesMap as $property_key => $property_val) { if (is_array($property_val)) { - if ($reference === null) { + if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { $reference = &$this->shadowProperties[$property_key]; - } else { - $reference = &$reference[$property_key]; + $this->setShadowPropertiesMapValues($property_val, $reference); } - $this->setShadowPropertiesMapValues($property_val, $reference); } else { if ($base_reference === null) { $this->shadowProperties[$property_key] = $property_val; @@ -435,7 +431,7 @@ class Axis extends Properties private function setShadowBlur($blur) { if ($blur !== null) { - $this->shadowProperties['blur'] = (string) $this->getExcelPointsWidth($blur); + $this->shadowProperties['blur'] = $blur; } return $this; @@ -444,14 +440,14 @@ class Axis extends Properties /** * Set Shadow Angle. * - * @param null|int $angle + * @param null|float|int $angle * * @return $this */ private function setShadowAngle($angle) { - if ($angle !== null) { - $this->shadowProperties['direction'] = (string) $this->getExcelPointsAngle($angle); + if (is_numeric($angle)) { + $this->shadowProperties['direction'] = $angle; } return $this; @@ -467,7 +463,7 @@ class Axis extends Properties private function setShadowDistance($distance) { if ($distance !== null) { - $this->shadowProperties['distance'] = (string) $this->getExcelPointsWidth($distance); + $this->shadowProperties['distance'] = $distance; } return $this; @@ -525,7 +521,7 @@ class Axis extends Properties private function setGlowSize($size) { if ($size !== null) { - $this->glowProperties['size'] = $this->getExcelPointsWidth($size); + $this->glowProperties['size'] = $size; } return $this; @@ -555,7 +551,7 @@ class Axis extends Properties public function setSoftEdges($size): void { if ($size !== null) { - $this->softEdges['size'] = (string) $this->getExcelPointsWidth($size); + $this->softEdges['size'] = $size; } } diff --git a/src/PhpSpreadsheet/Chart/GridLines.php b/src/PhpSpreadsheet/Chart/GridLines.php index 84af3ada..ad254c8c 100644 --- a/src/PhpSpreadsheet/Chart/GridLines.php +++ b/src/PhpSpreadsheet/Chart/GridLines.php @@ -45,25 +45,7 @@ class GridLines extends Properties ], ]; - private $shadowProperties = [ - 'presets' => self::SHADOW_PRESETS_NOSHADOW, - 'effect' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 85, - ], - 'size' => [ - 'sx' => null, - 'sy' => null, - 'kx' => null, - ], - 'blur' => null, - 'direction' => null, - 'distance' => null, - 'algn' => null, - 'rotWithShape' => null, - ]; + private $shadowProperties = Properties::PRESETS_OPTIONS[0]; private $glowProperties = [ 'size' => null, @@ -202,6 +184,18 @@ class GridLines extends Properties ->setGlowColor($colorValue, $colorAlpha, $colorType); } + /** + * Get Glow Property. + * + * @param array|string $property + * + * @return null|string + */ + public function getGlowProperty($property) + { + return $this->getArrayElementsValue($this->glowProperties, $property); + } + /** * Get Glow Color Property. * @@ -233,7 +227,7 @@ class GridLines extends Properties */ private function setGlowSize($size) { - $this->glowProperties['size'] = $this->getExcelPointsWidth((float) $size); + $this->glowProperties['size'] = $size; return $this; } @@ -253,7 +247,7 @@ class GridLines extends Properties $this->glowProperties['color']['value'] = (string) $color; } if ($alpha !== null) { - $this->glowProperties['color']['alpha'] = $this->getTrueAlpha((int) $alpha); + $this->glowProperties['color']['alpha'] = (int) $alpha; } if ($colorType !== null) { $this->glowProperties['color']['type'] = (string) $colorType; @@ -275,16 +269,27 @@ class GridLines extends Properties return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); } + /** + * @param mixed $value + */ + public function setShadowProperty(string $propertyName, $value): self + { + $this->activateObject(); + $this->shadowProperties[$propertyName] = $value; + + return $this; + } + /** * Set Shadow Properties. * * @param int $presets * @param string $colorValue * @param string $colorType - * @param string $colorAlpha - * @param string $blur - * @param int $angle - * @param float $distance + * @param null|float|int|string $colorAlpha + * @param null|float $blur + * @param null|int $angle + * @param null|float $distance */ public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void { @@ -292,10 +297,10 @@ class GridLines extends Properties ->setShadowPresetsProperties((int) $presets) ->setShadowColor( $colorValue ?? $this->shadowProperties['color']['value'], - $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : $this->getTrueAlpha($colorAlpha), + $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : (int) $colorAlpha, $colorType ?? $this->shadowProperties['color']['type'] ) - ->setShadowBlur((float) $blur) + ->setShadowBlur($blur) ->setShadowAngle($angle) ->setShadowDistance($distance); } @@ -315,6 +320,8 @@ class GridLines extends Properties return $this; } + private const SHADOW_ARRAY_KEYS = ['size', 'color']; + /** * Set Shadow Properties Values. * @@ -327,12 +334,10 @@ class GridLines extends Properties $base_reference = $reference; foreach ($propertiesMap as $property_key => $property_val) { if (is_array($property_val)) { - if ($reference === null) { + if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { $reference = &$this->shadowProperties[$property_key]; - } else { - $reference = &$reference[$property_key]; + $this->setShadowPropertiesMapValues($property_val, $reference); } - $this->setShadowPropertiesMapValues($property_val, $reference); } else { if ($base_reference === null) { $this->shadowProperties[$property_key] = $property_val; @@ -360,7 +365,7 @@ class GridLines extends Properties $this->shadowProperties['color']['value'] = (string) $color; } if ($alpha !== null) { - $this->shadowProperties['color']['alpha'] = $this->getTrueAlpha((int) $alpha); + $this->shadowProperties['color']['alpha'] = (int) $alpha; } if ($colorType !== null) { $this->shadowProperties['color']['type'] = (string) $colorType; @@ -372,14 +377,14 @@ class GridLines extends Properties /** * Set Shadow Blur. * - * @param float $blur + * @param ?float $blur * * @return $this */ private function setShadowBlur($blur) { if ($blur !== null) { - $this->shadowProperties['blur'] = (string) $this->getExcelPointsWidth($blur); + $this->shadowProperties['blur'] = $blur; } return $this; @@ -388,14 +393,14 @@ class GridLines extends Properties /** * Set Shadow Angle. * - * @param int $angle + * @param null|float|int|string $angle * * @return $this */ private function setShadowAngle($angle) { - if ($angle !== null) { - $this->shadowProperties['direction'] = (string) $this->getExcelPointsAngle($angle); + if (is_numeric($angle)) { + $this->shadowProperties['direction'] = $angle; } return $this; @@ -404,14 +409,14 @@ class GridLines extends Properties /** * Set Shadow Distance. * - * @param float $distance + * @param ?float $distance * * @return $this */ private function setShadowDistance($distance) { if ($distance !== null) { - $this->shadowProperties['distance'] = (string) $this->getExcelPointsWidth($distance); + $this->shadowProperties['distance'] = $distance; } return $this; @@ -434,11 +439,11 @@ class GridLines extends Properties * * @param float $size */ - public function setSoftEdgesSize($size): void + public function setSoftEdges($size): void { if ($size !== null) { $this->activateObject(); - $this->softEdges['size'] = (string) $this->getExcelPointsWidth($size); + $this->softEdges['size'] = $size; } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 800f6aaf..01a83915 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -110,7 +110,10 @@ abstract class Properties const SHADOW_PRESETS_PERSPECTIVE_UPPER_LEFT = 21; const SHADOW_PRESETS_PERSPECTIVE_LOWER_RIGHT = 22; const SHADOW_PRESETS_PERSPECTIVE_LOWER_LEFT = 23; + const POINTS_WIDTH_MULTIPLIER = 12700; + const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky + const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy /** * @param float $width @@ -122,27 +125,58 @@ abstract class Properties return $width * self::POINTS_WIDTH_MULTIPLIER; } - /** - * @param float $angle - * - * @return float - */ - protected function getExcelPointsAngle($angle) + public static function pointsToXml(float $width): string { - return $angle * 60000; + return (string) (int) ($width * self::POINTS_WIDTH_MULTIPLIER); } - protected function getTrueAlpha($alpha) + public static function xmlToPoints(string $width): float + { + return ((float) $width) / self::POINTS_WIDTH_MULTIPLIER; + } + + public static function angleToXml(float $angle): string + { + return (string) (int) ($angle * self::ANGLE_MULTIPLIER); + } + + public static function xmlToAngle(string $angle): float + { + return ((float) $angle) / self::ANGLE_MULTIPLIER; + } + + public static function tenthOfPercentToXml(float $value): string + { + return (string) (int) ($value * self::PERCENTAGE_MULTIPLIER); + } + + public static function xmlToTenthOfPercent(string $value): float + { + return ((float) $value) / self::PERCENTAGE_MULTIPLIER; + } + + public static function alphaToXml(int $alpha): string { return (string) (100 - $alpha) . '000'; } - protected function setColorProperties($color, $alpha, $colorType) + /** + * @param float|int|string $alpha + */ + public static function alphaFromXml($alpha): int + { + return 100 - ((int) $alpha / 1000); + } + + /** + * @param null|float|int|string $alpha + */ + protected function setColorProperties(?string $color, $alpha, ?string $colorType): array { return [ - 'type' => (string) $colorType, - 'value' => (string) $color, - 'alpha' => (string) $this->getTrueAlpha($alpha), + 'type' => $colorType, + 'value' => $color, + 'alpha' => (int) $alpha, ]; } @@ -163,196 +197,217 @@ abstract class Properties return $sizes[$arraySelector][$arrayKaySelector]; } + protected const PRESETS_OPTIONS = [ + //NONE + 0 => [ + 'presets' => self::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + 'color' => [ + 'type' => self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + 'ky' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ], + //OUTER + 1 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + 'algn' => 'tl', + 'rotWithShape' => '0', + ], + 2 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + 'algn' => 't', + 'rotWithShape' => '0', + ], + 3 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + 'algn' => 'tr', + 'rotWithShape' => '0', + ], + 4 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'algn' => 'l', + 'rotWithShape' => '0', + ], + 5 => [ + 'effect' => 'outerShdw', + 'size' => [ + 'sx' => 102000 / self::PERCENTAGE_MULTIPLIER, + 'sy' => 102000 / self::PERCENTAGE_MULTIPLIER, + ], + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'algn' => 'ctr', + 'rotWithShape' => '0', + ], + 6 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 10800000 / self::ANGLE_MULTIPLIER, + 'algn' => 'r', + 'rotWithShape' => '0', + ], + 7 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 8 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 16200000 / self::ANGLE_MULTIPLIER, + 'rotWithShape' => '0', + ], + 9 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + 'algn' => 'br', + 'rotWithShape' => '0', + ], + //INNER + 10 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + ], + 11 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + ], + 12 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + ], + 13 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + ], + 14 => [ + 'effect' => 'innerShdw', + 'blur' => 114300 / self::POINTS_WIDTH_MULTIPLIER, + ], + 15 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 10800000 / self::ANGLE_MULTIPLIER, + ], + 16 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + ], + 17 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 16200000 / self::ANGLE_MULTIPLIER, + ], + 18 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + ], + //perspective + 19 => [ + 'effect' => 'outerShdw', + 'blur' => 152400 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 317500 / self::POINTS_WIDTH_MULTIPLIER, + 'size' => [ + 'sx' => 90000 / self::PERCENTAGE_MULTIPLIER, + 'sy' => -19000 / self::PERCENTAGE_MULTIPLIER, + ], + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + 'rotWithShape' => '0', + ], + 20 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => 23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => -1200000 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 21 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => 23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => 1200000 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'br', + 'rotWithShape' => '0', + ], + 22 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => -23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => -800400 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 23 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => -23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => 800400 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'br', + 'rotWithShape' => '0', + ], + ]; + protected function getShadowPresetsMap($presetsOption) { - $presets_options = [ - //OUTER - 1 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '2700000', - 'algn' => 'tl', - 'rotWithShape' => '0', - ], - 2 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '5400000', - 'algn' => 't', - 'rotWithShape' => '0', - ], - 3 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '8100000', - 'algn' => 'tr', - 'rotWithShape' => '0', - ], - 4 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'algn' => 'l', - 'rotWithShape' => '0', - ], - 5 => [ - 'effect' => 'outerShdw', - 'size' => [ - 'sx' => '102000', - 'sy' => '102000', - ], - 'blur' => '63500', - 'distance' => '38100', - 'algn' => 'ctr', - 'rotWithShape' => '0', - ], - 6 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '10800000', - 'algn' => 'r', - 'rotWithShape' => '0', - ], - 7 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '18900000', - 'algn' => 'bl', - 'rotWithShape' => '0', - ], - 8 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '16200000', - 'rotWithShape' => '0', - ], - 9 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '13500000', - 'algn' => 'br', - 'rotWithShape' => '0', - ], - //INNER - 10 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '2700000', - ], - 11 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '5400000', - ], - 12 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '8100000', - ], - 13 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - ], - 14 => [ - 'effect' => 'innerShdw', - 'blur' => '114300', - ], - 15 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '10800000', - ], - 16 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '18900000', - ], - 17 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '16200000', - ], - 18 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '13500000', - ], - //perspective - 19 => [ - 'effect' => 'outerShdw', - 'blur' => '152400', - 'distance' => '317500', - 'size' => [ - 'sx' => '90000', - 'sy' => '-19000', - ], - 'direction' => '5400000', - 'rotWithShape' => '0', - ], - 20 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'direction' => '18900000', - 'size' => [ - 'sy' => '23000', - 'kx' => '-1200000', - ], - 'algn' => 'bl', - 'rotWithShape' => '0', - ], - 21 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'direction' => '13500000', - 'size' => [ - 'sy' => '23000', - 'kx' => '1200000', - ], - 'algn' => 'br', - 'rotWithShape' => '0', - ], - 22 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'distance' => '12700', - 'direction' => '2700000', - 'size' => [ - 'sy' => '-23000', - 'kx' => '-800400', - ], - 'algn' => 'bl', - 'rotWithShape' => '0', - ], - 23 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'distance' => '12700', - 'direction' => '8100000', - 'size' => [ - 'sy' => '-23000', - 'kx' => '800400', - ], - 'algn' => 'br', - 'rotWithShape' => '0', - ], - ]; - - return $presets_options[$presetsOption]; + return self::PRESETS_OPTIONS[$presetsOption] ?? self::PRESETS_OPTIONS[0]; } protected function getArrayElementsValue($properties, $elements) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 67af04b2..55a150b7 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; @@ -71,6 +72,7 @@ class Chart $rotX = $rotY = $rAngAx = $perspective = null; $xAxis = new Axis(); $yAxis = new Axis(); + $majorGridlines = $minorGridlines = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'chart': @@ -108,26 +110,52 @@ class Chart break; case 'valAx': $whichAxis = null; - if (isset($chartDetail->title, $chartDetail->axPos)) { - $axisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); + $axPos = null; + if (isset($chartDetail->axPos)) { $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string'); switch ($axPos) { case 't': case 'b': - $XaxisLabel = $axisLabel; $whichAxis = $xAxis; break; case 'r': case 'l': - $YaxisLabel = $axisLabel; $whichAxis = $yAxis; break; } } + if (isset($chartDetail->title)) { + $axisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); + + switch ($axPos) { + case 't': + case 'b': + $XaxisLabel = $axisLabel; + + break; + case 'r': + case 'l': + $YaxisLabel = $axisLabel; + + break; + } + } $this->readEffects($chartDetail, $whichAxis); + if (isset($chartDetail->majorGridlines)) { + $majorGridlines = new GridLines(); + if (isset($chartDetail->majorGridlines->spPr)) { + $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + } + } + if (isset($chartDetail->minorGridlines)) { + $minorGridlines = new GridLines(); + if (isset($chartDetail->minorGridlines->spPr)) { + $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + } + } break; case 'barChart': @@ -249,7 +277,7 @@ class Chart } } } - $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis, $majorGridlines, $minorGridlines); if (is_int($rotX)) { $chart->setRotX($rotX); } @@ -893,7 +921,7 @@ class Chart } /** - * @param null|Axis $chartObject may be extended to include other types + * @param null|Axis|GridLines $chartObject may be extended to include other types */ private function readEffects(SimpleXMLElement $chartDetail, $chartObject): void { @@ -905,18 +933,75 @@ class Chart if (isset($sppr->effectLst->glow)) { $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / Properties::POINTS_WIDTH_MULTIPLIER; if ($axisGlowSize != 0.0) { - $srgbClr = $schemeClr = ''; - $colorArray = $this->readColor($sppr->effectLst->glow, $srgbClr, $schemeClr); + $colorArray = $this->readColor($sppr->effectLst->glow); $chartObject->setGlowProperties($axisGlowSize, $colorArray['value'], $colorArray['alpha'], $colorArray['type']); } } if (isset($sppr->effectLst->softEdge)) { - $chartObject->setSoftEdges((float) self::getAttribute($sppr->effectLst->softEdge, 'rad', 'string') / Properties::POINTS_WIDTH_MULTIPLIER); + /** @var string */ + $softEdgeSize = self::getAttribute($sppr->effectLst->softEdge, 'rad', 'string'); + if (is_numeric($softEdgeSize)) { + $chartObject->setSoftEdges((float) Properties::xmlToPoints($softEdgeSize)); + } + } + + $type = ''; + foreach (self::SHADOW_TYPES as $shadowType) { + if (isset($sppr->effectLst->$shadowType)) { + $type = $shadowType; + + break; + } + } + if ($type !== '') { + /** @var string */ + $blur = self::getAttribute($sppr->effectLst->$type, 'blurRad', 'string'); + $blur = is_numeric($blur) ? Properties::xmlToPoints($blur) : null; + /** @var string */ + $dist = self::getAttribute($sppr->effectLst->$type, 'dist', 'string'); + $dist = is_numeric($dist) ? Properties::xmlToPoints($dist) : null; + /** @var string */ + $direction = self::getAttribute($sppr->effectLst->$type, 'dir', 'string'); + $direction = is_numeric($direction) ? Properties::xmlToAngle($direction) : null; + $algn = self::getAttribute($sppr->effectLst->$type, 'algn', 'string'); + $rot = self::getAttribute($sppr->effectLst->$type, 'rotWithShape', 'string'); + $size = []; + foreach (['sx', 'sy'] as $sizeType) { + $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); + if (is_numeric($sizeValue)) { + $size[$sizeType] = Properties::xmlToTenthOfPercent((string) $sizeValue); + } else { + $size[$sizeType] = null; + } + } + foreach (['kx', 'ky'] as $sizeType) { + $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); + if (is_numeric($sizeValue)) { + $size[$sizeType] = Properties::xmlToAngle((string) $sizeValue); + } else { + $size[$sizeType] = null; + } + } + $colorArray = $this->readColor($sppr->effectLst->$type); + $chartObject + ->setShadowProperty('effect', $type) + ->setShadowProperty('blur', $blur) + ->setShadowProperty('direction', $direction) + ->setShadowProperty('distance', $dist) + ->setShadowProperty('algn', $algn) + ->setShadowProperty('rotWithShape', $rot) + ->setShadowProperty('size', $size) + ->setShadowProperty('color', $colorArray); } } - private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr, ?string &$schemeClr): array + private const SHADOW_TYPES = [ + 'outerShdw', + 'innerShdw', + ]; + + private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr = null, ?string &$schemeClr = null): array { $result = [ 'type' => null, @@ -927,16 +1012,27 @@ class Chart $result['type'] = Properties::EXCEL_COLOR_TYPE_ARGB; $result['value'] = $srgbClr = self::getAttribute($colorXml->srgbClr, 'val', 'string'); if (isset($colorXml->srgbClr->alpha)) { - $alpha = (int) self::getAttribute($colorXml->srgbClr->alpha, 'val', 'string'); - $alpha = 100 - (int) ($alpha / 1000); + /** @var string */ + $alpha = self::getAttribute($colorXml->srgbClr->alpha, 'val', 'string'); + $alpha = Properties::alphaFromXml($alpha); $result['alpha'] = $alpha; } } elseif (isset($colorXml->schemeClr)) { $result['type'] = Properties::EXCEL_COLOR_TYPE_SCHEME; $result['value'] = $schemeClr = self::getAttribute($colorXml->schemeClr, 'val', 'string'); if (isset($colorXml->schemeClr->alpha)) { - $alpha = (int) self::getAttribute($colorXml->schemeClr->alpha, 'val', 'string'); - $alpha = 100 - (int) ($alpha / 1000); + /** @var string */ + $alpha = self::getAttribute($colorXml->schemeClr->alpha, 'val', 'string'); + $alpha = Properties::alphaFromXml($alpha); + $result['alpha'] = $alpha; + } + } elseif (isset($colorXml->prstClr)) { + $result['type'] = Properties::EXCEL_COLOR_TYPE_STANDARD; + $result['value'] = self::getAttribute($colorXml->prstClr, 'val', 'string'); + if (isset($colorXml->prstClr->alpha)) { + /** @var string */ + $alpha = self::getAttribute($colorXml->prstClr->alpha, 'val', 'string'); + $alpha = Properties::alphaFromXml($alpha); $result['alpha'] = $alpha; } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 0aae3646..e24afbac 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -499,22 +500,9 @@ class Chart extends WriterPart $objWriter->startElement('c:spPr'); $objWriter->startElement('a:effectLst'); - if ($yAxis->getGlowProperty('size') !== null) { - $objWriter->startElement('a:glow'); - $objWriter->writeAttribute('rad', $yAxis->getGlowProperty('size')); - $objWriter->startElement("a:{$yAxis->getGlowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', (string) $yAxis->getGlowProperty(['color', 'value'])); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', (string) $yAxis->getGlowProperty(['color', 'alpha'])); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - if ($yAxis->getSoftEdgesSize() !== null) { - $objWriter->startElement('a:softEdge'); - $objWriter->writeAttribute('rad', $yAxis->getSoftEdgesSize()); - $objWriter->endElement(); //end softEdge - } + $this->writeGlow($objWriter, $yAxis); + $this->writeShadow($objWriter, $yAxis); + $this->writeSoftEdge($objWriter, $yAxis); $objWriter->endElement(); // effectLst $objWriter->endElement(); // spPr @@ -640,61 +628,9 @@ class Chart extends WriterPart $objWriter->endElement(); //end ln } $objWriter->startElement('a:effectLst'); - - if ($majorGridlines->getGlowSize() !== null) { - $objWriter->startElement('a:glow'); - $objWriter->writeAttribute('rad', $majorGridlines->getGlowSize()); - $objWriter->startElement("a:{$majorGridlines->getGlowColor('type')}"); - $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end schemeClr - $objWriter->endElement(); //end glow - } - - if ($majorGridlines->getShadowProperty('presets') !== null) { - $objWriter->startElement("a:{$majorGridlines->getShadowProperty('effect')}"); - if ($majorGridlines->getShadowProperty('blur') !== null) { - $objWriter->writeAttribute('blurRad', $majorGridlines->getShadowProperty('blur')); - } - if ($majorGridlines->getShadowProperty('distance') !== null) { - $objWriter->writeAttribute('dist', $majorGridlines->getShadowProperty('distance')); - } - if ($majorGridlines->getShadowProperty('direction') !== null) { - $objWriter->writeAttribute('dir', $majorGridlines->getShadowProperty('direction')); - } - if ($majorGridlines->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $majorGridlines->getShadowProperty('algn')); - } - if ($majorGridlines->getShadowProperty(['size', 'sx']) !== null) { - $objWriter->writeAttribute('sx', $majorGridlines->getShadowProperty(['size', 'sx'])); - } - if ($majorGridlines->getShadowProperty(['size', 'sy']) !== null) { - $objWriter->writeAttribute('sy', $majorGridlines->getShadowProperty(['size', 'sy'])); - } - if ($majorGridlines->getShadowProperty(['size', 'kx']) !== null) { - $objWriter->writeAttribute('kx', $majorGridlines->getShadowProperty(['size', 'kx'])); - } - if ($majorGridlines->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $majorGridlines->getShadowProperty('rotWithShape')); - } - $objWriter->startElement("a:{$majorGridlines->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'value'])); - - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'alpha'])); - $objWriter->endElement(); //end alpha - - $objWriter->endElement(); //end color:type - $objWriter->endElement(); //end shadow - } - - if ($majorGridlines->getSoftEdgesSize() !== null) { - $objWriter->startElement('a:softEdge'); - $objWriter->writeAttribute('rad', $majorGridlines->getSoftEdgesSize()); - $objWriter->endElement(); //end softEdge - } + $this->writeGlow($objWriter, $majorGridlines); + $this->writeShadow($objWriter, $majorGridlines); + $this->writeSoftEdge($objWriter, $majorGridlines); $objWriter->endElement(); //end effectLst $objWriter->endElement(); //end spPr @@ -748,61 +684,11 @@ class Chart extends WriterPart } $objWriter->startElement('a:effectLst'); - - if ($minorGridlines->getGlowSize() !== null) { - $objWriter->startElement('a:glow'); - $objWriter->writeAttribute('rad', $minorGridlines->getGlowSize()); - $objWriter->startElement("a:{$minorGridlines->getGlowColor('type')}"); - $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end schemeClr - $objWriter->endElement(); //end glow - } - - if ($minorGridlines->getShadowProperty('presets') !== null) { - $objWriter->startElement("a:{$minorGridlines->getShadowProperty('effect')}"); - if ($minorGridlines->getShadowProperty('blur') !== null) { - $objWriter->writeAttribute('blurRad', $minorGridlines->getShadowProperty('blur')); - } - if ($minorGridlines->getShadowProperty('distance') !== null) { - $objWriter->writeAttribute('dist', $minorGridlines->getShadowProperty('distance')); - } - if ($minorGridlines->getShadowProperty('direction') !== null) { - $objWriter->writeAttribute('dir', $minorGridlines->getShadowProperty('direction')); - } - if ($minorGridlines->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $minorGridlines->getShadowProperty('algn')); - } - if ($minorGridlines->getShadowProperty(['size', 'sx']) !== null) { - $objWriter->writeAttribute('sx', $minorGridlines->getShadowProperty(['size', 'sx'])); - } - if ($minorGridlines->getShadowProperty(['size', 'sy']) !== null) { - $objWriter->writeAttribute('sy', $minorGridlines->getShadowProperty(['size', 'sy'])); - } - if ($minorGridlines->getShadowProperty(['size', 'kx']) !== null) { - $objWriter->writeAttribute('kx', $minorGridlines->getShadowProperty(['size', 'kx'])); - } - if ($minorGridlines->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $minorGridlines->getShadowProperty('rotWithShape')); - } - $objWriter->startElement("a:{$minorGridlines->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'value'])); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'alpha'])); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end color:type - $objWriter->endElement(); //end shadow - } - - if ($minorGridlines->getSoftEdgesSize() !== null) { - $objWriter->startElement('a:softEdge'); - $objWriter->writeAttribute('rad', $minorGridlines->getSoftEdgesSize()); - $objWriter->endElement(); //end softEdge - } - + $this->writeGlow($objWriter, $minorGridlines); + $this->writeShadow($objWriter, $minorGridlines); + $this->writeSoftEdge($objWriter, $minorGridlines); $objWriter->endElement(); //end effectLst + $objWriter->endElement(); //end spPr $objWriter->endElement(); //end minorGridLines } @@ -925,64 +811,11 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:effectLst'); - - if ($xAxis->getGlowProperty('size') !== null) { - $objWriter->startElement('a:glow'); - $objWriter->writeAttribute('rad', $xAxis->getGlowProperty('size')); - $objWriter->startElement("a:{$xAxis->getGlowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', (string) $xAxis->getGlowProperty(['color', 'value'])); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', (string) $xAxis->getGlowProperty(['color', 'alpha'])); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - if ($xAxis->getShadowProperty('presets') !== null) { - $objWriter->startElement("a:{$xAxis->getShadowProperty('effect')}"); - - if ($xAxis->getShadowProperty('blur') !== null) { - $objWriter->writeAttribute('blurRad', $xAxis->getShadowProperty('blur')); - } - if ($xAxis->getShadowProperty('distance') !== null) { - $objWriter->writeAttribute('dist', $xAxis->getShadowProperty('distance')); - } - if ($xAxis->getShadowProperty('direction') !== null) { - $objWriter->writeAttribute('dir', $xAxis->getShadowProperty('direction')); - } - if ($xAxis->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $xAxis->getShadowProperty('algn')); - } - if ($xAxis->getShadowProperty(['size', 'sx']) !== null) { - $objWriter->writeAttribute('sx', $xAxis->getShadowProperty(['size', 'sx'])); - } - if ($xAxis->getShadowProperty(['size', 'sy']) !== null) { - $objWriter->writeAttribute('sy', $xAxis->getShadowProperty(['size', 'sy'])); - } - if ($xAxis->getShadowProperty(['size', 'kx']) !== null) { - $objWriter->writeAttribute('kx', $xAxis->getShadowProperty(['size', 'kx'])); - } - if ($xAxis->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $xAxis->getShadowProperty('rotWithShape')); - } - - $objWriter->startElement("a:{$xAxis->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'value'])); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'alpha'])); - $objWriter->endElement(); - $objWriter->endElement(); - - $objWriter->endElement(); - } - - if ($xAxis->getSoftEdgesSize() !== null) { - $objWriter->startElement('a:softEdge'); - $objWriter->writeAttribute('rad', $xAxis->getSoftEdgesSize()); - $objWriter->endElement(); - } - + $this->writeGlow($objWriter, $xAxis); + $this->writeShadow($objWriter, $xAxis); + $this->writeSoftEdge($objWriter, $xAxis); $objWriter->endElement(); //effectList + $objWriter->endElement(); //end spPr if ($id1 !== '0') { @@ -1658,4 +1491,100 @@ class Chart extends WriterPart $objWriter->endElement(); } + + /** + * Write shadow properties. + * + * @param Axis|GridLines $xAxis + */ + private function writeShadow(XMLWriter $objWriter, $xAxis): void + { + if ($xAxis->getShadowProperty('effect') === null) { + return; + } + /** @var string */ + $effect = $xAxis->getShadowProperty('effect'); + $objWriter->startElement("a:$effect"); + + if (is_numeric($xAxis->getShadowProperty('blur'))) { + $objWriter->writeAttribute('blurRad', Properties::pointsToXml((float) $xAxis->getShadowProperty('blur'))); + } + if (is_numeric($xAxis->getShadowProperty('distance'))) { + $objWriter->writeAttribute('dist', Properties::pointsToXml((float) $xAxis->getShadowProperty('distance'))); + } + if (is_numeric($xAxis->getShadowProperty('direction'))) { + $objWriter->writeAttribute('dir', Properties::angleToXml((float) $xAxis->getShadowProperty('direction'))); + } + if ($xAxis->getShadowProperty('algn') !== null) { + $objWriter->writeAttribute('algn', $xAxis->getShadowProperty('algn')); + } + foreach (['sx', 'sy'] as $sizeType) { + $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]); + if (is_numeric($sizeValue)) { + $objWriter->writeAttribute($sizeType, Properties::tenthOfPercentToXml((float) $sizeValue)); + } + } + foreach (['kx', 'ky'] as $sizeType) { + $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]); + if (is_numeric($sizeValue)) { + $objWriter->writeAttribute($sizeType, Properties::angleToXml((float) $sizeValue)); + } + } + if ($xAxis->getShadowProperty('rotWithShape') !== null) { + $objWriter->writeAttribute('rotWithShape', $xAxis->getShadowProperty('rotWithShape')); + } + + $objWriter->startElement("a:{$xAxis->getShadowProperty(['color', 'type'])}"); + $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'value'])); + $alpha = $xAxis->getShadowProperty(['color', 'alpha']); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write glow properties. + * + * @param Axis|GridLines $yAxis + */ + private function writeGlow(XMLWriter $objWriter, $yAxis): void + { + $size = $yAxis->getGlowProperty('size'); + if (empty($size)) { + return; + } + $objWriter->startElement('a:glow'); + $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $size)); + $objWriter->startElement("a:{$yAxis->getGlowProperty(['color', 'type'])}"); + $objWriter->writeAttribute('val', (string) $yAxis->getGlowProperty(['color', 'value'])); + $alpha = $yAxis->getGlowProperty(['color', 'alpha']); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); // alpha + } + $objWriter->endElement(); // color + $objWriter->endElement(); // glow + } + + /** + * Write soft edge properties. + * + * @param Axis|GridLines $yAxis + */ + private function writeSoftEdge(XMLWriter $objWriter, $yAxis): void + { + $softEdgeSize = $yAxis->getSoftEdgesSize(); + if (empty($softEdgeSize)) { + return; + } + $objWriter->startElement('a:softEdge'); + $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $softEdgeSize)); + $objWriter->endElement(); //end softEdge + } } diff --git a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php index 88afef53..ad7fc776 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php @@ -1,6 +1,6 @@ getChartAxisY(); $xAxis = $chart->getChartAxisX(); - $yAxis->setGlowProperties(10, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); - $expectedSize = 127000.0; + $yGlowSize = 10.0; + $yAxis->setGlowProperties($yGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); $expectedGlowColor = [ 'type' => 'srgbClr', 'value' => 'FFFF00', - 'alpha' => '70000', + 'alpha' => 30, ]; - $yAxis->setSoftEdges(2.5); - $xAxis->setSoftEdges(5); - $expectedSoftEdgesY = '31750'; - $expectedSoftEdgesX = '63500'; - self::assertEquals($expectedSize, $yAxis->getGlowProperty('size')); + $softEdgesY = 2.5; + $yAxis->setSoftEdges($softEdgesY); + $softEdgesX = 5; + $xAxis->setSoftEdges($softEdgesX); + self::assertEquals($yGlowSize, $yAxis->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $yAxis->getGlowProperty('color')); - self::assertEquals($expectedSoftEdgesY, $yAxis->getSoftEdgesSize()); - self::assertEquals($expectedSoftEdgesX, $xAxis->getSoftEdgesSize()); + self::assertEquals($softEdgesY, $yAxis->getSoftEdgesSize()); + self::assertEquals($softEdgesX, $xAxis->getSoftEdgesSize()); // Set the position where the chart should appear in the worksheet $chart->setTopLeftPosition('A7'); @@ -142,9 +142,9 @@ class AxisGlowTest extends AbstractFunctional $chart2 = $charts2[0]; self::assertNotNull($chart2); $yAxis2 = $chart2->getChartAxisY(); - self::assertEquals($expectedSize, $yAxis2->getGlowProperty('size')); + self::assertEquals($yGlowSize, $yAxis2->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $yAxis2->getGlowProperty('color')); - self::assertEquals($expectedSoftEdgesY, $yAxis2->getSoftEdgesSize()); + self::assertEquals($softEdgesY, $yAxis2->getSoftEdgesSize()); $xAxis2 = $chart2->getChartAxisX(); self::assertNull($xAxis2->getGlowProperty('size')); $reloadedSpreadsheet->disconnectWorksheets(); @@ -229,14 +229,14 @@ class AxisGlowTest extends AbstractFunctional $yAxisLabel // yAxisLabel ); $yAxis = $chart->getChartAxisX(); // deliberate - $yAxis->setGlowProperties(20, 'accent1', 20, Properties::EXCEL_COLOR_TYPE_SCHEME); - $expectedSize = 254000.0; + $yGlowSize = 20.0; + $yAxis->setGlowProperties($yGlowSize, 'accent1', 20, Properties::EXCEL_COLOR_TYPE_SCHEME); $expectedGlowColor = [ 'type' => 'schemeClr', 'value' => 'accent1', - 'alpha' => '80000', + 'alpha' => 20, ]; - self::assertEquals($expectedSize, $yAxis->getGlowProperty('size')); + self::assertEquals($yGlowSize, $yAxis->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $yAxis->getGlowProperty('color')); // Set the position where the chart should appear in the worksheet @@ -259,7 +259,7 @@ class AxisGlowTest extends AbstractFunctional $chart2 = $charts2[0]; self::assertNotNull($chart2); $yAxis2 = $chart2->getChartAxisX(); // deliberate - self::assertEquals($expectedSize, $yAxis2->getGlowProperty('size')); + self::assertEquals($yGlowSize, $yAxis2->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $yAxis2->getGlowProperty('color')); $xAxis2 = $chart2->getChartAxisY(); // deliberate self::assertNull($xAxis2->getGlowProperty('size')); diff --git a/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php new file mode 100644 index 00000000..d6f122ef --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php @@ -0,0 +1,184 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testGlowY(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_AREACHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel + ); + $yAxis = $chart->getChartAxisY(); + $expectedY = [ + 'effect' => 'outerShdw', + 'algn' => 'tl', + 'blur' => 5, + 'direction' => 45, + 'distance' => 3, + 'rotWithShape' => 0, + 'color' => [ + 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + ]; + foreach ($expectedY as $key => $value) { + $yAxis->setShadowProperty($key, $value); + } + foreach ($expectedY as $key => $value) { + self::assertEquals($value, $yAxis->getShadowProperty($key), $key); + } + $xAxis = $chart->getChartAxisX(); + $expectedX = [ + 'effect' => 'outerShdw', + 'algn' => 'bl', + 'blur' => 6, + 'direction' => 315, + 'distance' => 3, + 'rotWithShape' => 0, + 'size' => [ + 'sx' => null, + 'sy' => 254, + 'kx' => -94, + 'ky' => null, + ], + 'color' => [ + 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'value' => 'FF0000', + 'alpha' => 20, + ], + ]; + foreach ($expectedX as $key => $value) { + $xAxis->setShadowProperty($key, $value); + } + foreach ($expectedX as $key => $value) { + self::assertEquals($value, $xAxis->getShadowProperty($key), $key); + } + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $yAxis2 = $chart2->getChartAxisY(); + foreach ($expectedY as $key => $value) { + self::assertEquals($value, $yAxis2->getShadowProperty($key), $key); + } + $xAxis2 = $chart2->getChartAxisX(); + foreach ($expectedX as $key => $value) { + self::assertEquals($value, $xAxis2->getShadowProperty($key), $key); + } + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php similarity index 99% rename from tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php rename to tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php index af33baa1..268ee094 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php @@ -1,6 +1,6 @@ setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testGlowY(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + $majorGridlines = new GridLines(); + $majorGlowSize = 10.0; + $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); + $softEdgeSize = 2.5; + $majorGridlines->setSoftEdges($softEdgeSize); + $expectedGlowColor = [ + 'type' => 'srgbClr', + 'value' => 'FFFF00', + 'alpha' => 30, + ]; + self::assertEquals($majorGlowSize, $majorGridlines->getGlowProperty('size')); + self::assertEquals($majorGlowSize, $majorGridlines->getGlowSize()); + self::assertEquals($expectedGlowColor['value'], $majorGridlines->getGlowColor('value')); + self::assertEquals($expectedGlowColor, $majorGridlines->getGlowProperty('color')); + self::assertEquals($softEdgeSize, $majorGridlines->getSoftEdgesSize()); + + $minorGridlines = new GridLines(); + $expectedShadow = [ + 'effect' => 'outerShdw', + 'algn' => 'tl', + 'blur' => 4, + 'direction' => 45, + 'distance' => 3, + 'rotWithShape' => 0, + 'color' => [ + 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + ]; + foreach ($expectedShadow as $key => $value) { + $minorGridlines->setShadowProperty($key, $value); + } + foreach ($expectedShadow as $key => $value) { + self::assertEquals($value, $minorGridlines->getShadowProperty($key), $key); + } + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + null, // xAxis + null, // yAxis + $majorGridlines, + $minorGridlines + ); + $majorGridlines2 = $chart->getMajorGridlines(); + self::assertEquals($majorGlowSize, $majorGridlines2->getGlowProperty('size')); + self::assertEquals($expectedGlowColor, $majorGridlines2->getGlowProperty('color')); + self::assertEquals($softEdgeSize, $majorGridlines2->getSoftEdgesSize()); + $minorGridlines2 = $chart->getMinorGridlines(); + foreach ($expectedShadow as $key => $value) { + self::assertEquals($value, $minorGridlines2->getShadowProperty($key), $key); + } + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $majorGridlines3 = $chart2->getMajorGridlines(); + self::assertEquals($majorGlowSize, $majorGridlines3->getGlowProperty('size')); + self::assertEquals($expectedGlowColor, $majorGridlines3->getGlowProperty('color')); + self::assertEquals($softEdgeSize, $majorGridlines3->getSoftEdgesSize()); + $minorGridlines3 = $chart->getMinorGridlines(); + foreach ($expectedShadow as $key => $value) { + self::assertEquals($value, $minorGridlines3->getShadowProperty($key), $key); + } + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php new file mode 100644 index 00000000..35161ff7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php @@ -0,0 +1,157 @@ +getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_AREACHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel + ); + $xAxis = $chart->getChartAxisX(); + $expectedX = [ + 'effect' => 'outerShdw', + 'algn' => 'bl', + 'blur' => 6, + 'direction' => 315, + 'distance' => 3, + 'rotWithShape' => 0, + 'size' => [ + 'sx' => null, + 'sy' => 254, + 'kx' => -94, + 'ky' => null, + ], + 'color' => [ + 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'value' => 'FF0000', + 'alpha' => 20, + ], + ]; + $expectedXmlX = [ + '', + '', + ]; + $expectedXmlNoX = [ + ' sx=', + ' ky=', + ]; + foreach ($expectedX as $key => $value) { + $xAxis->setShadowProperty($key, $value); + } + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + + // confirm that file contains expected tags + foreach ($expectedXmlX as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php new file mode 100644 index 00000000..58c024c1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php @@ -0,0 +1,183 @@ +setShadowProperties(17); + $expectedShadow = [ + 'effect' => 'innerShdw', + 'distance' => 4, + 'direction' => 270, + 'blur' => 5, + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($gridlines->getShadowProperty($key), $value, $key); + } + } + + public function testGridlineShadowPresetsWithArray(): void + { + $gridlines = new GridLines(); + $gridlines->setShadowProperties(20); + $expectedShadow = [ + 'effect' => 'outerShdw', + 'blur' => 6, + 'direction' => 315, + 'size' => [ + 'sx' => null, + 'sy' => 0.23, + 'kx' => -20, + 'ky' => null, + ], + 'algn' => 'bl', + 'rotWithShape' => '0', + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($gridlines->getShadowProperty($key), $value, $key); + } + } + + public function testAxisShadowPresets(): void + { + $axis = new Axis(); + $axis->setShadowProperties(9); + $expectedShadow = [ + 'effect' => 'outerShdw', + 'blur' => 4, + 'distance' => 3, + 'direction' => 225, + 'algn' => 'br', + 'rotWithShape' => '0', + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($axis->getShadowProperty($key), $value, $key); + } + } + + public function testAxisShadowPresetsWithChanges(): void + { + $axis = new Axis(); + $axis->setShadowProperties( + 9, // preset + 'FF0000', // colorValue + 'srgbClr', // colorType + 20, // alpha + 6, // blur + 30, // direction + 4, // distance + ); + $expectedShadow = [ + 'effect' => 'outerShdw', + 'blur' => 6, + 'distance' => 4, + 'direction' => 30, + 'algn' => 'br', + 'rotWithShape' => '0', + 'color' => [ + 'value' => 'FF0000', + 'type' => 'srgbClr', + 'alpha' => 20, + ], + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($axis->getShadowProperty($key), $value, $key); + } + } + + public function testGridlinesShadowPresetsWithChanges(): void + { + $gridline = new GridLines(); + $gridline->setShadowProperties( + 9, // preset + 'FF0000', // colorValue + 'srgbClr', // colorType + 20, // alpha + 6, // blur + 30, // direction + 4, // distance + ); + $expectedShadow = [ + 'effect' => 'outerShdw', + 'blur' => 6, + 'distance' => 4, + 'direction' => 30, + 'algn' => 'br', + 'rotWithShape' => '0', + 'color' => [ + 'value' => 'FF0000', + 'type' => 'srgbClr', + 'alpha' => 20, + ], + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($gridline->getShadowProperty($key), $value, $key); + } + } + + public function testOutOfRangePresets(): void + { + $axis = new Axis(); + $axis->setShadowProperties(99); + $expectedShadow = [ + 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + 'color' => [ + 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + 'ky' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($value, $axis->getShadowProperty($key), $key); + } + } + + public function testOutOfRangeGridlines(): void + { + $gridline = new GridLines(); + $gridline->setShadowProperties(99); + $expectedShadow = [ + 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + 'color' => [ + 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + 'ky' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($value, $gridline->getShadowProperty($key), $key); + } + } +} From 8434189336c713b19a5fcfa94ddb129c10cee49a Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 10 Jun 2022 13:38:41 +0200 Subject: [PATCH 4/6] Update docblock documentation for setting cell values explicit to indicate that value/datatype matching is the responsibility of the end-user developer making this call; and that values may be changed to reflect the specified datatype. No doubt some developers will complain that it should be the other way round, that datatpe should be modified to match the specified value; but then they should be using setValue() instead; the use of setValueExplicit() is explicit. --- CHANGELOG.md | 2 ++ src/PhpSpreadsheet/Cell/Cell.php | 7 ++++++- src/PhpSpreadsheet/Cell/DataType.php | 4 ++-- src/PhpSpreadsheet/Worksheet/Worksheet.php | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 290474a1..a7082cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Changed +- Better enforcement of value modification to match specified datatype when using setValueExplicit() +- Relax validation of merge cells to allow merge for a single cell reference [Issue #2776](https://github.com/PHPOffice/PhpSpreadsheet/issues/2776) - Memory and speed improvements, particularly for the Cell Collection, and the Writers. See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 76d5e86d..2005694d 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -202,6 +202,11 @@ class Cell * * @param mixed $value Value * @param string $dataType Explicit data type, see DataType::TYPE_* + * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this + * method, then it is your responsibility as an end-user developer to validate that the value and + * the datatype match. + * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype + * that you specify. * * @return Cell */ @@ -210,7 +215,7 @@ class Cell // set the value according to data type switch ($dataType) { case DataType::TYPE_NULL: - $this->value = $value; + $this->value = null; break; case DataType::TYPE_STRING2: diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php index 0f7efe2a..390dcde5 100644 --- a/src/PhpSpreadsheet/Cell/DataType.php +++ b/src/PhpSpreadsheet/Cell/DataType.php @@ -48,7 +48,7 @@ class DataType * * @param null|RichText|string $textValue Value to sanitize to an Excel string * - * @return null|RichText|string Sanitized value + * @return RichText|string Sanitized value */ public static function checkString($textValue) { @@ -58,7 +58,7 @@ class DataType } // string must never be longer than 32,767 characters, truncate if necessary - $textValue = StringHelper::substring($textValue, 0, 32767); + $textValue = StringHelper::substring((string) $textValue, 0, 32767); // we require that newline is represented as "\n" in core, not as "\r\n" or "\r" $textValue = str_replace(["\r\n", "\r"], "\n", $textValue); diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index f08ddf0c..d047380a 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1177,6 +1177,11 @@ class Worksheet implements IComparable * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * @param mixed $value Value of the cell * @param string $dataType Explicit data type, see DataType::TYPE_* + * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this + * method, then it is your responsibility as an end-user developer to validate that the value and + * the datatype match. + * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype + * that you specify. * * @return $this */ @@ -1199,6 +1204,11 @@ class Worksheet implements IComparable * @param int $row Numeric row coordinate of the cell * @param mixed $value Value of the cell * @param string $dataType Explicit data type, see DataType::TYPE_* + * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this + * method, then it is your responsibility as an end-user developer to validate that the value and + * the datatype match. + * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype + * that you specify. * * @return $this */ @@ -1770,6 +1780,10 @@ class Worksheet implements IComparable $numberRows = $lastRow - $firstRow; $numberColumns = $lastColumnIndex - $firstColumnIndex; + if ($numberRows === 1 && $numberColumns === 1) { + return $this; + } + // create upper left cell if it does not already exist $upperLeft = "{$firstColumn}{$firstRow}"; if (!$this->cellExists($upperLeft)) { From 189152ee078e15fcd2f94ad51063402de0c697b2 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 11 Jun 2022 14:32:29 +0200 Subject: [PATCH 5/6] Apply some coercive type-hinting --- phpstan-baseline.neon | 25 ----- .../Calculation/LookupRef/HLookup.php | 2 +- .../Calculation/LookupRef/VLookup.php | 2 +- src/PhpSpreadsheet/Shared/StringHelper.php | 101 ++++++------------ 4 files changed, 37 insertions(+), 93 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3cc183e2..5c1f074e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3115,26 +3115,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/StringHelper.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbIsUpper\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbIsUpper\\(\\) has parameter \\$character with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbStrSplit\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbStrSplit\\(\\) has parameter \\$string with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:sanitizeUTF8\\(\\) should return string but returns string\\|false\\.$#" count: 1 @@ -3160,11 +3140,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/StringHelper.php - - - message: "#^Variable \\$textValue on left side of \\?\\? always exists and is not nullable\\.$#" - count: 3 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - message: "#^Static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\TimeZone\\:\\:validateTimeZone\\(\\) is unused\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index c7e87315..d67718ce 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -73,7 +73,7 @@ class HLookup extends LookupBase // break if we have passed possible keys $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData); - $cellDataLower = StringHelper::strToLower($rowData); + $cellDataLower = StringHelper::strToLower((string) $rowData); if ( $notExactMatch && diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 52d5a191..53a7badc 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -90,7 +90,7 @@ class VLookup extends LookupBase foreach ($lookupArray as $rowKey => $rowData) { $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData[$column]); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData[$column]); - $cellDataLower = StringHelper::strToLower($rowData[$column]); + $cellDataLower = StringHelper::strToLower((string) $rowData[$column]); // break if we have passed possible keys if ( diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 7d6a990f..2ccb2424 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -329,12 +329,8 @@ class StringHelper /** * Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters. - * - * @param string $textValue - * - * @return string */ - public static function sanitizeUTF8($textValue) + public static function sanitizeUTF8(string $textValue): string { if (self::getIsIconvEnabled()) { $textValue = @iconv('UTF-8', 'UTF-8', $textValue); @@ -349,12 +345,8 @@ class StringHelper /** * Check if a string contains UTF8 data. - * - * @param string $textValue - * - * @return bool */ - public static function isUTF8($textValue) + public static function isUTF8(string $textValue): bool { return $textValue === '' || preg_match('/^./su', $textValue) === 1; } @@ -364,10 +356,8 @@ class StringHelper * point as decimal separator in case locale is other than English. * * @param mixed $numericValue - * - * @return string */ - public static function formatNumber($numericValue) + public static function formatNumber($numericValue): string { if (is_float($numericValue)) { return str_replace(',', '.', $numericValue); @@ -385,10 +375,8 @@ class StringHelper * * @param string $textValue UTF-8 encoded string * @param mixed[] $arrcRuns Details of rich text runs in $value - * - * @return string */ - public static function UTF8toBIFF8UnicodeShort($textValue, $arrcRuns = []) + public static function UTF8toBIFF8UnicodeShort(string $textValue, array $arrcRuns = []): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -419,10 +407,8 @@ class StringHelper * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function UTF8toBIFF8UnicodeLong($textValue) + public static function UTF8toBIFF8UnicodeLong(string $textValue): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -436,13 +422,10 @@ class StringHelper /** * Convert string from one encoding to another. * - * @param string $textValue * @param string $to Encoding to convert to, e.g. 'UTF-8' * @param string $from Encoding to convert from, e.g. 'UTF-16LE' - * - * @return string */ - public static function convertEncoding($textValue, $to, $from) + public static function convertEncoding(string $textValue, string $to, string $from): string { if (self::getIsIconvEnabled()) { $result = iconv($from, $to . self::$iconvOptions, $textValue); @@ -457,52 +440,45 @@ class StringHelper /** * Get character count. * - * @param string $textValue * @param string $encoding Encoding * * @return int Character count */ - public static function countCharacters($textValue, $encoding = 'UTF-8') + public static function countCharacters(string $textValue, string $encoding = 'UTF-8'): int { - return mb_strlen($textValue ?? '', $encoding); + return mb_strlen($textValue, $encoding); } /** * Get a substring of a UTF-8 encoded string. * - * @param null|string $textValue UTF-8 encoded string + * @param string $textValue UTF-8 encoded string * @param int $offset Start offset * @param int $length Maximum number of characters in substring - * - * @return string */ - public static function substring($textValue, $offset, $length = 0) + public static function substring(string $textValue, int $offset, int $length = 0): string { - return mb_substr($textValue ?? '', $offset, $length, 'UTF-8'); + return mb_substr($textValue, $offset, $length, 'UTF-8'); } /** * Convert a UTF-8 encoded string to upper case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToUpper($textValue) + public static function strToUpper(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_UPPER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_UPPER, 'UTF-8'); } /** * Convert a UTF-8 encoded string to lower case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToLower($textValue) + public static function strToLower(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_LOWER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_LOWER, 'UTF-8'); } /** @@ -510,24 +486,27 @@ class StringHelper * (uppercase every first character in each word, lower case all other characters). * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToTitle($textValue) + public static function strToTitle(string $textValue): string { return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8'); } - public static function mbIsUpper($character) + public static function mbIsUpper(string $character): bool { - return mb_strtolower($character, 'UTF-8') != $character; + return mb_strtolower($character, 'UTF-8') !== $character; } - public static function mbStrSplit($string) + /** + * Splits a UTF-8 string into an array of individual characters. + */ + public static function mbStrSplit(string $string): array { // Split at all position not after the start: ^ // and not before the end: $ - return preg_split('/(? Date: Sun, 12 Jun 2022 19:21:43 +0200 Subject: [PATCH 6/6] Apply some coercive type-hinting --- .../Calculation/Calculation.php | 2 +- src/PhpSpreadsheet/Calculation/Functions.php | 2 +- .../Calculation/Information/ErrorValue.php | 2 +- .../Calculation/Information/ExcelError.php | 20 +++++++++---------- src/PhpSpreadsheet/Cell/DataType.php | 4 ++-- src/PhpSpreadsheet/ReferenceHelper.php | 6 +++--- src/PhpSpreadsheet/Shared/TimeZone.php | 10 +++++----- src/PhpSpreadsheet/Writer/Html.php | 2 +- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2bba56f6..588841dc 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3088,7 +3088,7 @@ class Calculation } // Test whether we have any language data for this language (any locale) - if (in_array($language, self::$validLocaleLanguages)) { + if (in_array($language, self::$validLocaleLanguages, true)) { // initialise language/locale settings self::$localeFunctions = []; self::$localeArgumentSeparator = ','; diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 1cc980b8..ddd3e200 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -152,7 +152,7 @@ class Functions if ($condition === '') { return '=""'; } - if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='])) { + if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) { $condition = self::operandSpecialHandling($condition); if (is_bool($condition)) { return '=' . ($condition ? 'TRUE' : 'FALSE'); diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index cffad6a6..869350ed 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes) || $value === ExcelError::CALC(); + return in_array($value, ExcelError::$errorCodes, true) || $value === ExcelError::CALC(); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 585dfdc8..88de7e54 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -11,7 +11,7 @@ class ExcelError /** * List of error codes. * - * @var array + * @var array */ public static $errorCodes = [ 'null' => '#NULL!', @@ -60,7 +60,7 @@ class ExcelError * * @return string #NULL! */ - public static function null() + public static function null(): string { return self::$errorCodes['null']; } @@ -72,7 +72,7 @@ class ExcelError * * @return string #NUM! */ - public static function NAN() + public static function NAN(): string { return self::$errorCodes['num']; } @@ -84,7 +84,7 @@ class ExcelError * * @return string #REF! */ - public static function REF() + public static function REF(): string { return self::$errorCodes['reference']; } @@ -100,7 +100,7 @@ class ExcelError * * @return string #N/A! */ - public static function NA() + public static function NA(): string { return self::$errorCodes['na']; } @@ -112,7 +112,7 @@ class ExcelError * * @return string #VALUE! */ - public static function VALUE() + public static function VALUE(): string { return self::$errorCodes['value']; } @@ -124,7 +124,7 @@ class ExcelError * * @return string #NAME? */ - public static function NAME() + public static function NAME(): string { return self::$errorCodes['name']; } @@ -134,7 +134,7 @@ class ExcelError * * @return string #DIV/0! */ - public static function DIV0() + public static function DIV0(): string { return self::$errorCodes['divisionbyzero']; } @@ -142,9 +142,9 @@ class ExcelError /** * CALC. * - * @return string #Not Yet Implemented + * @return string #CALC! */ - public static function CALC() + public static function CALC(): string { return '#CALC!'; } diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php index 390dcde5..16de2a00 100644 --- a/src/PhpSpreadsheet/Cell/DataType.php +++ b/src/PhpSpreadsheet/Cell/DataType.php @@ -21,7 +21,7 @@ class DataType /** * List of error codes. * - * @var array + * @var array */ private static $errorCodes = [ '#NULL!' => 0, @@ -36,7 +36,7 @@ class DataType /** * Get list of error codes. * - * @return array + * @return array */ public static function getErrorCodes() { diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 046c5894..36c85edf 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -399,7 +399,7 @@ class ReferenceHelper return $highestColumn . $row; }, range(1, $highestRow)), function ($coordinate) use ($allCoordinates) { - return !in_array($coordinate, $allCoordinates); + return in_array($coordinate, $allCoordinates, true) === false; } ); @@ -929,7 +929,7 @@ class ReferenceHelper $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; $worksheet->removeConditionalStyles($coordinate); if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); $worksheet->getCell($coordinate)->setXfIndex(0); } } @@ -945,7 +945,7 @@ class ReferenceHelper $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; $worksheet->removeConditionalStyles($coordinate); if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); $worksheet->getCell($coordinate)->setXfIndex(0); } } diff --git a/src/PhpSpreadsheet/Shared/TimeZone.php b/src/PhpSpreadsheet/Shared/TimeZone.php index dabb88f2..734c076d 100644 --- a/src/PhpSpreadsheet/Shared/TimeZone.php +++ b/src/PhpSpreadsheet/Shared/TimeZone.php @@ -21,9 +21,9 @@ class TimeZone * * @return bool Success or failure */ - private static function validateTimeZone($timezoneName) + private static function validateTimeZone(string $timezoneName): bool { - return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)); + return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true); } /** @@ -33,7 +33,7 @@ class TimeZone * * @return bool Success or failure */ - public static function setTimeZone($timezoneName) + public static function setTimeZone(string $timezoneName): bool { if (self::validateTimezone($timezoneName)) { self::$timezone = $timezoneName; @@ -49,7 +49,7 @@ class TimeZone * * @return string Timezone (e.g. 'Europe/London') */ - public static function getTimeZone() + public static function getTimeZone(): string { return self::$timezone; } @@ -63,7 +63,7 @@ class TimeZone * * @return int Number of seconds for timezone adjustment */ - public static function getTimeZoneAdjustment($timezoneName, $timestamp) + public static function getTimeZoneAdjustment(?string $timezoneName, $timestamp): int { $timezoneName = $timezoneName ?? self::$timezone; $dtobj = Date::dateTimeFromTimestamp("$timestamp"); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 3ef65928..da32025a 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1744,7 +1744,7 @@ class Html extends BaseWriter while ($c++ < $e) { $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell']; - if (!in_array($baseCell, $adjustedBaseCells)) { + if (!in_array($baseCell, $adjustedBaseCells, true)) { // subtract rowspan by 1 --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan']; $adjustedBaseCells[] = $baseCell;