From b62de98bf70e42652a50dafd2c2aad6441459054 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 7 Jun 2022 20:22:33 +0200 Subject: [PATCH 01/25] 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 391e6308d7956d3c76a10aa5ae5a02879de795df Mon Sep 17 00:00:00 2001 From: dgeppo <107109388+dgeppo@users.noreply.github.com> Date: Wed, 8 Jun 2022 11:24:56 +0200 Subject: [PATCH 02/25] Add removeCommentByColumnAndRow function --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 4a441a93..d7288aa4 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2581,6 +2581,22 @@ class Worksheet implements IComparable return $this; } + + + /** + * Remove comment. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * + */ + public function removeCommentByColumnAndRow($columnIndex, $row) + { + $pCellCoordinate = strtoupper(Coordinate::stringFromColumnIndex($columnIndex) . $row); + if(isset($this->comments[$pCellCoordinate])) { + unset($this->comments[$pCellCoordinate]); + } + } /** * Get comment for cell. From 13437384afd58acd64833cbd28fdf13070f82d2d Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:08:02 +0200 Subject: [PATCH 03/25] Change removeComment to use CellCoordinate --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d7288aa4..14d24265 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2581,22 +2581,31 @@ class Worksheet implements IComparable return $this; } - /** - * Remove comment. + * Remove comment from cell. * - * @param int $columnIndex Numeric column coordinate of the cell - * @param int $row Numeric row coordinate of the cell + * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * + * @return Comment */ - public function removeCommentByColumnAndRow($columnIndex, $row) + public function removeComment($cellCoordinate) { - $pCellCoordinate = strtoupper(Coordinate::stringFromColumnIndex($columnIndex) . $row); - if(isset($this->comments[$pCellCoordinate])) { - unset($this->comments[$pCellCoordinate]); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); + + if (Coordinate::coordinateIsRange($cellAddress)) { + throw new Exception('Cell coordinate string can not be a range of cells.'); + } elseif (strpos($cellAddress, '$') !== false) { + throw new Exception('Cell coordinate string must not be absolute.'); + } elseif ($cellAddress == '') { + throw new Exception('Cell coordinate can not be zero-length string.'); } - } + // Check if we have a comment for this cell and delete it + if (isset($this->comments[$cellAddress])) { + unset($this->comments[$cellAddress]); + } + } /** * Get comment for cell. From 56ddbc4dd50533ebed3d2a224385f0a1ba80c0a2 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:08:52 +0200 Subject: [PATCH 04/25] Add testRemoveComment --- tests/PhpSpreadsheetTests/CommentTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index e58afad4..1d7825f4 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -83,4 +83,12 @@ class CommentTest extends TestCase $comment->setText($test); self::assertEquals('This is a test comment', (string) $comment); } + + public function testRemoveComment(): void { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getComment('A2')->getText()->createText('Comment to delete'); + $sheet->removeComment('A2'); + self::assertEmpty($sheet->getComments()); + } } From 52da0dc0fcc846a2bd53137f298a2f646b692b77 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:18:48 +0200 Subject: [PATCH 05/25] Add use PhpOffice\PhpSpreadsheet\Spreadsheet; --- tests/PhpSpreadsheetTests/CommentTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index 1d7825f4..7d06162a 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\TextElement; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Color; use PHPUnit\Framework\TestCase; From 801f5296e9fb2e4d8a30effc58abc0250831818a Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:21:18 +0200 Subject: [PATCH 06/25] Update Worksheet.php --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 14d24265..b3a0bf2a 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2590,7 +2590,7 @@ class Worksheet implements IComparable * * @return Comment */ - public function removeComment($cellCoordinate) + public function removeComment($cellCoordinate):void { $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); From 00dae1bbdaec33869d7196552ca4bf194e8b3225 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 10 Jun 2022 01:15:57 +0200 Subject: [PATCH 07/25] 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 08/25] 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 09/25] 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 10/25] 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 11/25] 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; From 861b955b3b39cee6b7f4a373d8d6359b56f3dd84 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:12:08 +0200 Subject: [PATCH 12/25] Add feature removeComment Use $cellCoordinate as argument Return $this to support the fluent interface --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index b3a0bf2a..b9a168c2 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2588,9 +2588,9 @@ class Worksheet implements IComparable * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * - * @return Comment + * @return $this */ - public function removeComment($cellCoordinate):void + public function removeComment($cellCoordinate) { $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); @@ -2605,6 +2605,8 @@ class Worksheet implements IComparable if (isset($this->comments[$cellAddress])) { unset($this->comments[$cellAddress]); } + + return $this; } /** From 7f62fba7ef95891967a99e745e71bfac689a2fc4 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:44:42 +0200 Subject: [PATCH 13/25] Update the method testRemoveComment Adding test to check if comment exists before delete it --- tests/PhpSpreadsheetTests/CommentTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index 7d06162a..e55fe9cc 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -89,6 +89,7 @@ class CommentTest extends TestCase $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->getComment('A2')->getText()->createText('Comment to delete'); + self::assertArrayHasKey('A2',$sheet->getComments()); $sheet->removeComment('A2'); self::assertEmpty($sheet->getComments()); } From 11a94dabec2ed0085dc3c033fa615b6b388bc363 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 14 Jun 2022 12:01:08 +0200 Subject: [PATCH 14/25] Potential improvements for insert/delete column/row for performance testing --- src/PhpSpreadsheet/ReferenceHelper.php | 38 ++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 36c85edf..2a349607 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -367,7 +367,6 @@ class ReferenceHelper Worksheet $worksheet ): void { $remove = ($numberOfColumns < 0 || $numberOfRows < 0); - $allCoordinates = $worksheet->getCoordinates(); if ( $this->cellReferenceHelper === null || @@ -394,12 +393,13 @@ class ReferenceHelper } // Find missing coordinates. This is important when inserting column before the last column + $cellCollection = $worksheet->getCellCollection(); $missingCoordinates = array_filter( array_map(function ($row) use ($highestColumn) { return $highestColumn . $row; }, range(1, $highestRow)), - function ($coordinate) use ($allCoordinates) { - return in_array($coordinate, $allCoordinates, true) === false; + function ($coordinate) use ($cellCollection) { + return $cellCollection->has($coordinate) === false; } ); @@ -408,16 +408,15 @@ class ReferenceHelper foreach ($missingCoordinates as $coordinate) { $worksheet->createNewCell($coordinate); } - - // Refresh all coordinates - $allCoordinates = $worksheet->getCoordinates(); } - // Loop through cells, bottom-up, and change cell coordinate + $allCoordinates = $worksheet->getCoordinates(); if ($remove) { // It's faster to reverse and pop than to use unshift, especially with large cell collections $allCoordinates = array_reverse($allCoordinates); } + + // Loop through cells, bottom-up, and change cell coordinate while ($coordinate = array_pop($allCoordinates)) { $cell = $worksheet->getCell($coordinate); $cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); @@ -927,11 +926,7 @@ class ReferenceHelper for ($i = 1; $i <= $highestRow - 1; ++$i) { for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } + $this->clearStripCell($worksheet, $coordinate); } } } @@ -943,15 +938,24 @@ class ReferenceHelper for ($i = $beforeColumn - 1; $i <= $lastColumnIndex; ++$i) { for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } + $this->clearStripCell($worksheet, $coordinate); } } } + private function clearStripCell(Worksheet $worksheet, string $coordinate) + { + // TODO - Should also clear down comments, but wait until after comment removal PR-2875 is merged + $worksheet->removeConditionalStyles($coordinate); + $worksheet->setHyperlink($coordinate); + $worksheet->setDataValidation($coordinate); + + if ($worksheet->cellExists($coordinate)) { + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void { $autoFilter = $worksheet->getAutoFilter(); From 4d2b00dafc9d0015084354e9a064d4a1515ed88f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 14 Jun 2022 12:19:54 +0200 Subject: [PATCH 15/25] phpcs fxes --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- tests/PhpSpreadsheetTests/CommentTest.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index b9a168c2..c8236993 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2605,7 +2605,7 @@ class Worksheet implements IComparable if (isset($this->comments[$cellAddress])) { unset($this->comments[$cellAddress]); } - + return $this; } diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index e55fe9cc..aacd538f 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -84,13 +84,14 @@ class CommentTest extends TestCase $comment->setText($test); self::assertEquals('This is a test comment', (string) $comment); } - - public function testRemoveComment(): void { + + public function testRemoveComment(): void + { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->getComment('A2')->getText()->createText('Comment to delete'); - self::assertArrayHasKey('A2',$sheet->getComments()); + self::assertArrayHasKey('A2', $sheet->getComments()); $sheet->removeComment('A2'); self::assertEmpty($sheet->getComments()); - } + } } From 8e31dbaabe2c69aefa6d7cee4db14f56e4688209 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 14 Jun 2022 12:21:44 +0200 Subject: [PATCH 16/25] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 290474a1..2c65f4ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files) - Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) - Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) From 90bdc7c12e709f5603e4003a07605aa4b8143cce Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 14 Jun 2022 08:33:36 -0700 Subject: [PATCH 17/25] Test For Excel File Saved With Ribbon Data (#2883) File from https://www.rondebruin.nl/win/s2/win003.htm. I have been in conversation with the author, who has no objection to its use. I have not actually opened the file in Excel (at least not with macros enabled); I am using it merely to demonstrate that the ribbon data is read and written correctly. Test added; no source code changed. This should slightly increase coverage for Reader/Xlsx (moderate), Writer/Xlsx (slight), and Spreadsheet (substantial). Note that this file has no Ribbon Bin objects, so some coverage is still lacking. --- .../Reader/Xlsx/RibbonTest.php | 47 ++++++++++++++++++ tests/data/Reader/XLSX/ribbon.donotopen.zip | Bin 0 -> 15340 bytes 2 files changed, 47 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php create mode 100644 tests/data/Reader/XLSX/ribbon.donotopen.zip diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php new file mode 100644 index 00000000..197ad47f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -0,0 +1,47 @@ +load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen($data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + self::assertNull($spreadsheet->getRibbonBinObjects()); + self::assertNull($spreadsheet->getRibbonBinObjects('names')); + self::assertNull($spreadsheet->getRibbonBinObjects('data')); + self::assertEmpty($spreadsheet->getRibbonBinObjects('types')); + self::assertNull($spreadsheet->getRibbonBinObjects('xxxxx')); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertSame($vbaCode, $reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/ribbon.donotopen.zip b/tests/data/Reader/XLSX/ribbon.donotopen.zip new file mode 100644 index 0000000000000000000000000000000000000000..97d58bdcf63e278ecdd03bdcd454e39fc95f14e6 GIT binary patch literal 15340 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 literal 0 HcmV?d00001 From 04f46676584abcc144a441aa516a8e2a2035eb1b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 14 Jun 2022 08:45:12 -0700 Subject: [PATCH 18/25] Expand Chart Support for schemeClr and prstClr (#2879) * Expand Chart Support for schemeClr and prstClr Fix #2219. Address some, but not all, issues mentioned in #2863. For Pie Charts, fill color is stored in XML as part of dPt node, which had been ignored by Reader/Xlsx/Chart. Add support for it, including when specified as schemeClr or prstClr rather than srgbClr. Add support for prstClr in other cases where schemeClr is supported. * Update Change Log Add this PR. --- CHANGELOG.md | 2 +- samples/templates/32readwriteAreaChart4.xlsx | Bin 0 -> 12474 bytes src/PhpSpreadsheet/Chart/DataSeriesValues.php | 25 ++- src/PhpSpreadsheet/Chart/Properties.php | 5 + src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 90 ++++++---- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 34 +++- .../Chart/Charts32XmlTest.php | 95 ++++++----- .../PhpSpreadsheetTests/Chart/PieFillTest.php | 160 ++++++++++++++++++ 8 files changed, 325 insertions(+), 86 deletions(-) create mode 100644 samples/templates/32readwriteAreaChart4.xlsx create mode 100644 tests/PhpSpreadsheetTests/Chart/PieFillTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a12675..4664f049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) -- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) ## 1.23.0 - 2022-04-24 diff --git a/samples/templates/32readwriteAreaChart4.xlsx b/samples/templates/32readwriteAreaChart4.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4548bf2e0dec273a37ceaf11b27e1a914c8978c5 GIT binary patch literal 12474 zcmeHt^J5;{_IIqtwr$(C+o);t#I|kQb{gAm8ryD+ri~k``9AHr_x7Be^S=MUz4OD& zvoo_lnZ0JMz4TF*1BXBdfdYX60RbTasqCeO%K!xdafJc_K?8vS(-E_=b~Lhfe6Q|i zYviEI=xSw2oDTs;oeKg6eExsOf3XBAl13GKnUN)Kq#wn08O+~m5(lO`tLVW>e}u;N zSkZK{9A&#azmflpNurMXdYn>m`!?I-DK(PvZ~#M2+BBfL&xr>e(v=|c>O$7DLkQtS z+^?`hh0(R74@y~nAO)-7MgfA}wP8?pPW=3x>>LL>+$eI7!z6v7B#O|7)<{{VIsDym zTUR=#Gl*=cIrGr-15o2{qblbu`-?hMISUc`ZBJDauSNS0D&F)-?#*Nf#fqekS3sd% zYV^)f3_y(NAqYp&8Pv(k+X(mdC=UgUK_DsxoTw)8v*PuxNOo)=JR`4UKdT)Nbe`Fh zTjaXF4$FSsyS%qD{8~C<5*c&PE>5kOQb}c*`tYVJ=ImmS+QDJ1K92J}=E<2z$Dr{3 z;T>k3JT$V|4v2?hJH|2TBCVer*?Q;2I>O_^$r&JCyAgBuWdG4^1J=ow!-G7r^=s7d z9`P&Z+N9ap^cacBSJgLdgST%7-l>J+3y^cKee9xdEP##+B905n`Q&)u`;6Q=mW}wR z=9yQy12qQPH1-^I&IMG*=Vx#b<-gc;<7;NJE8rb@pvk@h+Vp#SBTEM+#-Hc^+46s} z0sm>~k@1s2TZSJvmF^JRefVZA4U<>YE-&3qqE7gc{Di16s(@Vh<~k7EU)ErC`ufYH zkk0zazL`1^zCM#;ZyU8pEt|f_qPc5j&Ksm$eNq!M^g-Nce1OKy#fOr* zhzQ)a#78oHb^;~zSLa9eMRCk3BLLIs1e_A1&xv~i!QF()4E*yw<}lD6&}D-6{C9QD z>(I|v^h`Z4C1mfm(w@NX9fZYdbv0($z9)ZhQ3mBd3Q9zg&-pAa|*2rdW= zsH-K@ABN&$V{f5vV`K5t_x;;wK!FYr`0Ri8Q4v3C-p33taTD|-V7kkW)k`_p&Ujii z)hff=*EW$`ULShYYxmSbm3R1KtaTmBTi6W`@`hC(m&#l6$*6j`I&w&nH|=$`js7>= zd;2I5&6j*w=Sg^CwB#@aJe5A52hm>FJnBE_G>OEha=|jocxMC3YnX zd?Q;R=Q6t|e)X?NvVZed=R(b9tHMZ2c?|!Lluhs6F-yw=eb7B0Box zH*!}04*YH{Ho?BesYF3po~~t7u_nsilnW_Nalkfrl4f+4MCtX2qh7KSWPFVKKHEfVkvpbq_6dm>d7Y(6o= zcPg#;gxV9CfjfdvlPXx^rkFtsPH64SgtC{$I|lg(HNOT7X5p_+GI>vyUlq;lLT73$ z6tZWFC;ARos_!SxJe@p!4MHUfj1$!Xr|I-BK0G=H%xk;+Xu6h<{dk+0Ofn}o;Q3CJ z;<0Z*uSQK%Bh0sykL5id@=OXy-b|#F$|u@9cmnL?6CwO2YIQ`s3@(gR=2-42(#`uW z^~U1mGG$!E08-L#_iE}bHxg?3K}a^Lkg+sb2Ie{~SjLGhAH1y+2Tt^H?MCV{hM1(t z(etMPA>SpPFC!+om3R$J`b9(zPsMa)8KsO0t@x<5JqYS0L}gZ8qGf|wi#$iZG_OFH zsNvWe;|kpz9U*zy5PQ!`>AO4Op+Kyc);mWW4we{|o^3U;o-`TkC^I@n>$gpTn_L7U zg`Q_GGyS9353(Ys7Jzr`fucwML+lQw0DB`tHAj0hYZHf`mTNgRDGR{@4|UeQ$(Ojk zHc_=L#H;%TFhO?!p2#_&zT(&6%&)kzU?(}C3(Oy?t1yoZBZUYKTsZs9w{v5NxLT}tU>PB$paVGqV(d%V#SQ${@j z1(3}A6{odISQ>vB(Bn_&I>;l)Z^Nx`4L;Zo-HQL@)0F(%$J0cI5;~Wie?d^I)3z$zNAk1{ocqwM>#w!jd82M zMNj=+{7>6+Ff}rA{P*&I4Sp}aB5vFq==FS01G)mHJDq2Gu?rNL45kiO1bj{C!n8>7 zPM+CYPL)k4`dT?GWIiG*vp;jCo?F(-y< z4&S(}Vr6E|kq{afzozNKl#F{cg?~UX8RRu8T=}Y&mNnM^Ps#Uu0>z&3p_OZo3%m_d z?KmK)4hc_r(4i{h*|@kXhA=1oIW=x~01hpjZ+?_^CJD6Pe<20;z5g2xBj#qSmFI?G zig-PqTzB}Y>uD`Gh5P4fwep4Q0jzKg5P6YOUu?_kIKzJ;VsOEh?DwwK5y)hIbE#;gyxBnlJD8A zmg`ozhxQ2)&2pQ1xCB4R;St8e4H}HO%CD?vN}sMQJe=zGXV8+Ph|~xsXS5s#$x>t_ zg0ZvL@jWMLj|T^@_2 zQQr!nyYV>V^ws~|ar_LuhuTE@a8XCY+5R5t;~Z<%;-QmAV`es}-UhTnCA~!Y)ep(a zglT%;qaD^`>qeEz_FJ&0D~dFoEXCq!S)*EUgdxF&lry|}V=s^K9=zCouH>_;3Fqcj z$1^Cy^KlB#XW4Qsmvbm|R$Qf&sg=AZZ&`s5dG1w)AR7?Rg7i;V)Sbi`glDyXkMF!#iwymGJ3 z_Idtxr*`@6E@3BRGMeazxo8NVQ-iz3i|~QunTg-Z9>2( zi(o5uL96M?9T8x^um7fxK+jWipgqE1u(4AM>^0jf*}?0zSZ@6jHhen#*i`DmFSm)Y zsK+NN-7m3pGqHapo>UtN5B~nhHGCfKvv&Fso-JQQLio`) zx+SA^4>Tqtl#Vl0Wxlj%dPYjBh-sBo3yuX`!tZu7*Oa#E!GqMG)pBd{`j{WR5~>3c z9@G++4evf>RLZk(!J6AX0FJu~Yt*1oN#+rg^AHf&l7c}ZIH>5JwZX^SRp1iU3+iQR zyFh!+s%1c(9d+d>(YDL;>tB(rIA3!x6nTCK#SYvdW7lW1^X+-!RTs%&NwVDm(E5A( z9r9RNvF%}gVgDMQq7P*<$Ra12nXHqQuSKcTLL=P@zh6mAc<_d{UFGDlEH$y4_;ono zQAkbVl*$&eR&asvY|B|wuaK?i*e*{L@ijGU=?2ymcj^l0G?+#QGlB@D%iMl9amao5 zIFDR!Ax9?pJSakJZVgeXGIAY~(XjXiO`rMKbt9R&#MrPB{}$F-{g@#VfS4|LhP%y; zGEAOznT$3R{E6XX>!}%36xiA>`o(yu;oZ80!)DHjrzLBEsx<*8f`ak$+rAC~&7(5s zPOa?YP6>(2zWnhr6D`m&I2{o>g@;A-txTQDN=-T3Y7W+|j}fD|y?X1&6t;1Jmqbg? zC1+_*n|WMwBtpI`;hPUZlu!V06|Z4VLTM~%K5qYkVZ;@^nZgRQ*##LLhSdtDmf7*U zZ04rqBl;hL#kCjvV@6+gwm2;Pj3<^BnME_(5lE4Y~>KTWH-7aqO5p2 z`BcqOgryTU3mWHnVdR|){ZmWJN&+)P%|z(&op4fR)#CBKkh?O!>KzqErmU6Hj5uN- z_CE+|OE;8N@2dmf8EYcIlqA*JNK`em-;6a!8<%nZbb`pV2|3!pT2`ax`$}DQ14EGnZRRZn!Q=%;HJ|+*z$k_ z^O2h<=@7#VzCovKkI-6$X`>lqjshU+>1}%@b*P%ana&`ID1wD7tI8V^#*cE$eO1Q3 z-%z6*ZM--AR9Uv^p}ZjaFvD$9s2*>N9FQQzB+P@e_c)<&+`NtBtVXOA(OJXw5p%5D z;D%h3r1RkXv1co0n*SQ+8cg+vhvpnQ_>a>LvZ2$fLMR*YvhZeW;s=!n8%@GHGLs_o zx!z(H`D&s3f|(=OPi?i6o7wkfR^i!<#HMpWCIZ+?;?}BYkIPmFh)fkG5CrA}3<=;u zCO<&gY$ga0)`Yd1LEGLTspQu3Fi4LdKiQ^j)<1^rS74U+e0$}g-V`8xrq$&XfTi1p z-B8;Ml}+-fQmN(d^VUYN!g=vw*$~G%IpGdcSN=`JxOH8s4Ojb`H^#)L2!`w*wO zN`=%~(=JZ@w>X64+QvRzduN%5B7jW#l5OOPto?F{`mJlw7_(J3<28k->9 zA$v$lxh_7h?uO5iIPc`QV`R`98j16YXC>lR#L03U8A%>;gK|O4mre`p?w_W)@UPbl zn5o}sw;q(yieJX)&CRt9?Ij7jl^0J*G^rz{xlNN9>gK0#)2c5*31T$rzmfCC3S);3 zAKc9x(oithF=N*xVpFLZWUF}Sx2P}KU+{syXAgr@2@kLY%~GQjxdH3LXj8>7Oh|N_CKW$eqtH||@=V?5veyEY@G7#%SvitE zei$w1`-Ph!6Zle(k0X3bAjkyAr(-7qt30lw$xq>Te!3IiBV6B|@4Ch#H?5hA!Kzb0 zogYS~pjd(iTAQs%XZnGASF)?9`oq?MKRR5@K#IgSq*-q_I(O+HCgJd8fkd=6X_%xEVe)K>}n;CT3f>9JBR? zr!xi{u6@%@hv4z{mufx+Q*c()0HP4^ESL0q5%I3MprFm|7+@-*-^P0ERhVlsxv1iJ z>f#+pzRkXOJ{IZ?=Ha9_4=CegzRrMrztC(kfWcW&3sA$Om7KI(RkVi}$T)7l> zvjqNiZ~^$^=;x31OV`PQ+E!cnX)X;OF1lT=a(*+h2>i=8Ihs8N?Yy;AR!;glD6^Za z(;;UeOgA|u33rP&7k6KfT=E`5h}wvJZrnMz zP*d(fD4T{pD=E71v$wtmr9CI_^U^D8m=(#T0o}c^!BiFqhb>_|$>#yxby=nEfrK=2 zoR0VX4&PcyO-&Txs2~gpS~U>t``ur#Y#`Yej=w{f4{oHt_8KCQXuZ$ewEe=Tp)xaL z<$4Ndgzlaj&sl5AiB{#dd-msbQK8YygIk}1X`)lW8q|FD^bAl=?0reP|w4(YuK^Z zWu>2zUq`FM{S%zf~$B<&HR{g8! z%JM?Hl1UfeaI7Gs4&YpMqrrD@%ahGTLXg60J-h9AZGMl};Td&1k9~aA^Uy>Q4zW+` zp3hO|pfco7fx3+l?`nv3s98bz041g6IV50u(@+%`9*OZHyWk&8IJ0}wDOHxSqAXA?&QN$mp2av!*{QSAk zYMj@(hM;M0kP9rRA zElu%61EE12)N6_Ck%N+T3obMz9=^1K%ItCWSW5x-t{x1_ff4&-3)TDmFKlHsdb#Ek zXXBd(OedPEN@Qvl<>YG1DtxZNYPhn ztUdb{9!>+RmUvNf64Mw$Us_)ld%<2iW;YRi5~j$QbYYYSgE4ELMTH>jYaJka<+W>& z{S>=YB0c1JR08&uoxK|$m1({o058W&B(UvK{%fbsE#+rw=dIvwf)mB2X;BgM$9*`P zBqYiruz^Y8vxZ<+wLpC;dP{WkHeqx*1(jrLV*P#!)PrLrDXQf**a`U`PL7PeoJMEX zWn8xX@Vw3)oUh-Jz9G9W%d%l|-AaeTvasYzT(jz{Ebpu5#vL@{MNBZorWj@(|4h2* zZequZ8$!0!Hh^-LP>f{BMYxK1y|!%&^6^NoI-%+wdr^4_(Qq9h75i%keht`ofKol} z(xHhuop%wH5_3Ns7PKVo$h^GXT#V{qtEhfdlCC25QKR>n9I~-V0e2vBz9o8)3Tm|2 z#~}s_VZrT-e>kKft;XF1~+6GyTWjX zXb2Pxdw<-zC0MM8bR1wX`CdhnEGKShoM%szaR1e-Cz7C7vq60leiC#`Ry9+iHs`W2 zZ)T~a^E_@$ZqSaWN9?}9OduqXafF~vfD*NJ&>+E^B@GmOrPn~lq7SL{*5GVYRG89q zw3=35k|}@lsiClu($?;Cf-XU@3U5o%;<#Oi4u(12ow0(hsDkE7iN&?z+et-TH~&Jp zGQb^GzcriaRml7vN@2_?)t4J4{3^x5{&~VH2kN=64)A-H=Z7W5OH$1NNKdYzo>Zv3 zai-{3BjSpb#~N&w^avgj`c$YN0M%n9=affWN!)3Gxl_B9CEV)=HtV;F9ujqu3fBlA z(3O=U-O8^nZkM#D_Bzs*C~aS(HALxjXsep7==a*Z+v32dXR>Ri;L5xS&7Rtq?v$hu z%wwu)N%9VpK)k6&yWeqdEp!#>I%0yoek-<$RTVT~hFN5X9?-`kKuA&59?jW^U%Gs) zV{RBtZRzg3O1hcXnxHcrU-PPg!yF0Xl|xwzlvzjXuC>iu^Kg+Ttix zLXMKXI_q6Hhm-x>4~4mCHk-AniONo%Z`THpphgnUy{85@*}= z!R{pNwvY=45jZcWglD@d`6O>Hjo>Tna=b_#N4NvUfau;kq|cRYItc{bRNoN_+QBPc zk(LkI7Ih`Feffs_TDM$in&Anm$}6_Lpv-io5jC(B@(moSd7DoF%4dpfJ4B(KBay%b zY+qe8%Fd!rOoZzk^dEdRi$y&>3F2K;?gwR3F&tHCy}c4*nhjt%_o|GDy0uM0dr^h^kX^T$COx?O9!GS}Vh--eUE%omtDghkwsg!CaL<(8CM=vG~4 zymYG74#i^L_3ZAt580*NBI2OgVO!R+3}JSZ{Y+vy%ns9lq2$H#kRvrQIEQ;)<0Jkd zJ=|w!O!-Z}4WIBNTy{~hU?#b!TgAm_xKOlzaaFdY)T6^ytpqN7Vw2Ufrro_?^%glR z_1-~RsxV7IoVlW`4NUFuXMgBx|8J2<fR|{!DnCcfgXM zRI55FQ3@ByI4^iYd)4yfekFs^him~uqhAy){cWMk$7F%WDRr3)0~MuImBFETCjD)M zM{dxNN+RW+Kuv>NvD&O_$jxx)5ldYc<7f;nQe^GGeVGS>y4rTxmuG~^1iae=Gmj&P zrSwT%tPb#GwUyKLJ@L}5?a>|pvpXYh-fFJLaCS@+yZd^2RDiDwswEywpkxm8J}5wQ z1=o|(!u{RV0r#H&qm9;TBds^+makeVVO_|OKgZGs;|ejU%;U+#oKguxR&DrMTlz2S zZpSu9_UzJjX82%~$S8~`&Dr2HF~$gK&mtX}Xfg0;r-2Sg@HPhn;G;f2WPj}Mun9P? zk`ry2Ii!fyn~@kWj0%4J;^!7yw8tFyxe?h#Gl$z_^6zbh?iE`^vYP-#B{Ic`*cB;B^a#-O8-y_%7u8wq&5Sf>J~3_l0Ao;4Q4&Oir$14icW}<8P@jWwT??#1 zt!8QKsMlDl!|7Ny;6pI(#MG`!ly1eDPCw#8jeK{Q)!|}|M7JU^(y_J+;>B(8>oWwu zK-~2|(bZ$Nu7!azC^2TeoY20N8gMNOf}GQOAMdJOnv$fQ+xn#sy1hH<^Q<_EM8idv zzi08(!~-*N1blfG^~gW~C=nBp$MG;#fxuaNM>Qn^=ci$caC3CI?b_`EWY>49fqglx zwUZyRstLDC2G!_9cg+qp5E1r2Tz`vm&El$W^7k1b4@O{Qfm2=;B8-z7oQBstQo z1TH-Cnq|lu$!4zU2Z(WM7nVJf)e3xLf1Q7E`!PbMOyQA~EPE!bd`h6Aqs>DT*HoYW z;cMHEz2$roZD)6PvD;N z_mAi2u3-POr~V#z7v;}8j)9H6(f_s_pgRBS@QhcH>t#j{+JJl(8}`U3#P}k*+LI{2 zgPcj-6|`$jXE7Tq-u~n<>$rf}Kls_<#AULR{5^gK?>&S;`5TN-N(hNuaXPw2x~WlP zjE%e@-YW8H9Y{!CKZ%I%_q15)VGAD<%s8+${G#|}v?v|@t|!cbYcV`bTNFCP-0W7< zgZZA>`6^nu&j>aXV|70J?!@y&d+1e2k@AuiVaMI~&Vdp`2IJx?Pw1yBKkhi`;Xas% zDN{~-u%|k9;T_DXE5h!Z_G*|nJ!)h8T8G^GrTEtB=@$P@6y5xWEC{UwO)B$8U5gP8 zt`O5=PD0v7$IU2NkK%sPd;?j%TK+=K_iLEs0r*J z3=>qH*$f$etT zte4*a-9jY*UE(JL24kf+ld6t~2&$LeuXXP_&Y>vsh<5`cV^L8>1EpA5?ymrUWi0-A_8jmd{E5H#a^hb}Tz{T;4-9Ai z-;}PGI4_AjzmWuC|3>S1Ir$|W<@e-9xL=e1MoxK&@RG>!8=>>nFNFVu)qlw1&%yOC zirP!Sm*j}wfNk*qS4&>)u5Fqgn8)QQh$sopa z33M+N<<~VT`isoZ@ymC8nG*d1_;nH>{qBab0F!U1iuWtD_BnU`6^`Bnp-%k4F^p~6JZvZs9KcxTXEmm0$ U5@^0Zw_jus05G`}qyIVjKiJn^X8-^I literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 6747934a..0a2f5a85 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -76,6 +76,9 @@ class DataSeriesValues /** @var string */ private $schemeClr = ''; + /** @var string */ + private $prstClr = ''; + /** * Line Width. * @@ -262,7 +265,7 @@ class DataSeriesValues /** * Set fill color for series. * - * @param null|string|string[] $color HEX color or array with HEX colors + * @param string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ @@ -270,10 +273,14 @@ class DataSeriesValues { if (is_array($color)) { foreach ($color as $colorValue) { - $this->validateColor($colorValue); + if (substr($colorValue, 0, 1) !== '*' && substr($colorValue, 0, 1) !== '/') { + $this->validateColor($colorValue); + } } } else { - $this->validateColor("$color"); + if (substr($color, 0, 1) !== '*' && substr($color, 0, 1) !== '/') { + $this->validateColor("$color"); + } } $this->fillColor = $color; @@ -470,4 +477,16 @@ class DataSeriesValues return $this; } + + public function getPrstClr(): string + { + return $this->prstClr; + } + + public function setPrstClr(string $prstClr): self + { + $this->prstClr = $prstClr; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 01a83915..6db04809 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -14,6 +14,11 @@ abstract class Properties EXCEL_COLOR_TYPE_STANDARD = 'prstClr'; const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr'; const EXCEL_COLOR_TYPE_ARGB = 'srgbClr'; + const EXCEL_COLOR_TYPES = [ + self::EXCEL_COLOR_TYPE_ARGB, + self::EXCEL_COLOR_TYPE_SCHEME, + self::EXCEL_COLOR_TYPE_STANDARD, + ]; const AXIS_LABELS_LOW = 'low'; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 55a150b7..8e3d6386 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -359,7 +359,9 @@ class Chart $pointSize = null; $noFill = false; $schemeClr = ''; + $prstClr = ''; $bubble3D = false; + $dPtColors = []; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -383,7 +385,24 @@ class Chart $noFill = true; } if (isset($children->solidFill)) { - $this->readColor($children->solidFill, $srgbClr, $schemeClr); + $this->readColor($children->solidFill, $srgbClr, $schemeClr, $prstClr); + } + + break; + case 'dPt': + $dptIdx = (int) self::getAttribute($seriesDetail->idx, 'val', 'string'); + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $arrayColors = $this->readColor($children->solidFill); + if ($arrayColors['type'] === 'srgbClr') { + $dptColors[$dptIdx] = $arrayColors['value']; + } elseif ($arrayColors['type'] === 'prstClr') { + $dptColors[$dptIdx] = '/' . $arrayColors['value']; + } else { + $dptColors[$dptIdx] = '*' . $arrayColors['value']; + } + } } break; @@ -394,7 +413,7 @@ class Chart if (count($seriesDetail->spPr) === 1) { $ln = $seriesDetail->spPr->children($this->aNamespace); if (isset($ln->solidFill)) { - $this->readColor($ln->solidFill, $srgbClr, $schemeClr); + $this->readColor($ln->solidFill, $srgbClr, $schemeClr, $prstClr); } } @@ -461,6 +480,16 @@ class Chart if (isset($seriesValues[$seriesIndex])) { $seriesValues[$seriesIndex]->setSchemeClr($schemeClr); } + } elseif ($prstClr) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setPrstClr($prstClr); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setPrstClr($prstClr); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setPrstClr($prstClr); + } } if ($bubble3D) { if (isset($seriesLabel[$seriesIndex])) { @@ -473,6 +502,17 @@ class Chart $seriesValues[$seriesIndex]->setBubble3D($bubble3D); } } + if (!empty($dptColors)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setFillColor($dptColors); + } + } } } /** @phpstan-ignore-next-line */ @@ -1001,39 +1041,31 @@ class Chart 'innerShdw', ]; - private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr = null, ?string &$schemeClr = null): array + private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr = null, ?string &$schemeClr = null, ?string &$prstClr = null): array { $result = [ 'type' => null, 'value' => null, 'alpha' => null, ]; - if (isset($colorXml->srgbClr)) { - $result['type'] = Properties::EXCEL_COLOR_TYPE_ARGB; - $result['value'] = $srgbClr = self::getAttribute($colorXml->srgbClr, 'val', 'string'); - if (isset($colorXml->srgbClr->alpha)) { - /** @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)) { - /** @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; + foreach (Properties::EXCEL_COLOR_TYPES as $type) { + if (isset($colorXml->$type)) { + $result['type'] = $type; + $result['value'] = self::getAttribute($colorXml->$type, 'val', 'string'); + if ($type === Properties::EXCEL_COLOR_TYPE_ARGB) { + $srgbClr = $result['value']; + } elseif ($type === Properties::EXCEL_COLOR_TYPE_SCHEME) { + $schemeClr = $result['value']; + } elseif ($type === Properties::EXCEL_COLOR_TYPE_STANDARD) { + $prstClr = $result['value']; + } + if (isset($colorXml->$type->alpha)) { + $alpha = (int) self::getAttribute($colorXml->$type->alpha, 'val', 'string'); + $alpha = 100 - (int) ($alpha / 1000); + $result['alpha'] = $alpha; + } + + break; } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index e24afbac..dda395ac 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -942,6 +942,9 @@ class Chart extends WriterPart */ private function writePlotSeriesValuesElement(XMLWriter $objWriter, $val = 3, $fillColor = 'FF9900'): void { + if ($fillColor === '') { + return; + } $objWriter->startElement('c:dPt'); $objWriter->startElement('c:idx'); $objWriter->writeAttribute('val', $val); @@ -953,8 +956,16 @@ class Chart extends WriterPart $objWriter->startElement('c:spPr'); $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); + if (substr($fillColor, 0, 1) === '*') { + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', substr($fillColor, 1)); + } elseif (substr($fillColor, 0, 1) === '/') { + $objWriter->startElement('a:prstClr'); + $objWriter->writeAttribute('val', substr($fillColor, 1)); + } else { + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $fillColor); + } $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); @@ -1039,7 +1050,7 @@ class Chart extends WriterPart $fillColorValues = $plotSeriesValues->getFillColor(); if ($fillColorValues !== null && is_array($fillColorValues)) { foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { - $this->writePlotSeriesValuesElement($objWriter, $dataKey, ($fillColorValues[$dataKey] ?? 'FF9900')); + $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? ''); } } else { $this->writePlotSeriesValuesElement($objWriter); @@ -1061,7 +1072,7 @@ class Chart extends WriterPart $groupType == DataSeries::TYPE_LINECHART || $groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()) - || ($plotSeriesValues !== false && $plotSeriesValues->getSchemeClr()) + || ($plotSeriesValues !== false && ($plotSeriesValues->getSchemeClr() || $plotSeriesValues->getPrstClr())) ) { $plotLineWidth = 12700; if ($plotSeriesValues) { @@ -1069,10 +1080,21 @@ class Chart extends WriterPart } $objWriter->startElement('c:spPr'); - $schemeClr = $plotLabel ? $plotLabel->getSchemeClr() : null; + $schemeClr = $typeClr = ''; + if ($plotLabel) { + $schemeClr = $plotLabel->getSchemeClr(); + if ($schemeClr) { + $typeClr = 'schemeClr'; + } else { + $schemeClr = $plotLabel->getPrstClr(); + if ($schemeClr) { + $typeClr = 'prstClr'; + } + } + } if ($schemeClr) { $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:schemeClr'); + $objWriter->startElement("a:$typeClr"); $objWriter->writeAttribute('val', $schemeClr); $objWriter->endElement(); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php index a193ca7d..3123278f 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; -use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; use PHPUnit\Framework\TestCase; @@ -13,17 +12,6 @@ class Charts32XmlTest extends TestCase // These tests can only be performed by examining xml. private const DIRECTORY = 'samples' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR; - /** @var string */ - private $outputFileName = ''; - - protected function tearDown(): void - { - if ($this->outputFileName !== '') { - unlink($this->outputFileName); - $this->outputFileName = ''; - } - } - /** * @dataProvider providerScatterCharts */ @@ -33,25 +21,21 @@ class Charts32XmlTest extends TestCase $reader = new XlsxReader(); $reader->setIncludeCharts(true); $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart2.xml'; - $data = file_get_contents($file); - // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } else { - self::assertSame(1, substr_count($data, '')); - self::assertSame($expectedCount, substr_count($data, '')); - } + self::assertSame(1, substr_count($data, '')); + self::assertSame($expectedCount, substr_count($data, '')); } public function providerScatterCharts(): array @@ -69,23 +53,20 @@ class Charts32XmlTest extends TestCase $reader = new XlsxReader(); $reader->setIncludeCharts(true); $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart1.xml'; - $data = file_get_contents($file); // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } else { - self::assertSame(0, substr_count($data, '')); - } + self::assertSame(0, substr_count($data, '')); } /** @@ -116,18 +97,11 @@ class Charts32XmlTest extends TestCase $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart2.xml'; - $data = file_get_contents($file); - // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } elseif ($numeric === true) { + if ($numeric === true) { self::assertSame(0, substr_count($data, '')); self::assertSame(2, substr_count($data, '')); } else { @@ -144,4 +118,31 @@ class Charts32XmlTest extends TestCase [null], ]; } + + public function testAreaPrstClr(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + } } diff --git a/tests/PhpSpreadsheetTests/Chart/PieFillTest.php b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php new file mode 100644 index 00000000..452fcb93 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php @@ -0,0 +1,160 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testPieFill(): 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], + ] + ); + // Custom colors for dataSeries (gray, blue, red, orange) + $colors = [ + 'cccccc', + '*accent1', // use schemeClr, was '00abb8', + '/green', // use prstClr, was 'b8292f', + 'eb8500', + ]; + + // 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 + $dataSeriesLabels1 = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + 'Worksheet!$C$1', + null, + 1 + ), // 2011 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues1 = [ + 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 + // Custom Colors + $dataSeriesValues1Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4); + $dataSeriesValues1Element->setFillColor($colors); + $dataSeriesValues1 = [$dataSeriesValues1Element]; + + // Build the dataseries + $series1 = new DataSeries( + DataSeries::TYPE_PIECHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues + ); + + // Set up a layout object for the Pie chart + $layout1 = new Layout(); + $layout1->setShowVal(true); + $layout1->setShowPercent(true); + + // Set the series in the plot area + $plotArea1 = new PlotArea($layout1, [$series1]); + // Set the chart legend + $legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title1 = new Title('Test Pie Chart'); + + // Create the chart + $chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // no Y-Axis for Pie Chart + ); + + // Set the position where the chart should appear in the worksheet + $chart1->setTopLeftPosition('A7'); + $chart1->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart1); + + /** @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); + $plotArea2 = $chart2->getPlotArea(); + $dataSeries2 = $plotArea2->getPlotGroup(); + self::assertCount(1, $dataSeries2); + $plotValues = $dataSeries2[0]->getPlotValues(); + self::assertCount(1, $plotValues); + $fillColors = $plotValues[0]->getFillColor(); + self::assertSame($colors, $fillColors); + + $writer = new XlsxWriter($reloadedSpreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart2); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(4, substr_count($data, '')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} From 09c66ab3029dd072ef8de8c8b809716706efc4d0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 11:19:02 +0200 Subject: [PATCH 19/25] Use column address rather than index for remove column loop to eliminate extra math and repeated calls to stringFromColumnIndex() --- src/PhpSpreadsheet/ReferenceHelper.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 2a349607..8caaab18 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -923,9 +923,12 @@ class ReferenceHelper private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void { - for ($i = 1; $i <= $highestRow - 1; ++$i) { - for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns); + $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + + for ($row = 1; $row <= $highestRow - 1; ++$row) { + for ($column = $startColumnId; $column !== $endColumnId; ++$column) { + $coordinate = $column . $row; $this->clearStripCell($worksheet, $coordinate); } } @@ -933,22 +936,23 @@ class ReferenceHelper private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void { - $lastColumnIndex = Coordinate::columnIndexFromString($highestColumn) - 1; + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + ++$highestColumn; - for ($i = $beforeColumn - 1; $i <= $lastColumnIndex; ++$i) { - for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; + for ($column = $startColumnId; $column !== $highestColumn; ++$column) { + for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) { + $coordinate = $column . $row; $this->clearStripCell($worksheet, $coordinate); } } } - private function clearStripCell(Worksheet $worksheet, string $coordinate) + private function clearStripCell(Worksheet $worksheet, string $coordinate): void { - // TODO - Should also clear down comments, but wait until after comment removal PR-2875 is merged $worksheet->removeConditionalStyles($coordinate); $worksheet->setHyperlink($coordinate); $worksheet->setDataValidation($coordinate); + $worksheet->removeComment($coordinate); if ($worksheet->cellExists($coordinate)) { $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); From f51f4bc0ea37124f16055591b7287ee7ce9ce1da Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 12:29:47 +0200 Subject: [PATCH 20/25] Update change log and documentation --- CHANGELOG.md | 1 + docs/topics/recipes.md | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a12675..67359728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - 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 +- Improved performance for removing rows/columns from a worksheet ### Deprecated diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index f25a9119..e4313382 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1348,7 +1348,7 @@ Removing a merge can be done using the unmergeCells method: $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); ``` -## Inserting rows/columns +## Inserting or Removing rows/columns You can insert/remove rows/columns at a specific position. The following code inserts 2 new rows, right before row 7: @@ -1356,6 +1356,23 @@ code inserts 2 new rows, right before row 7: ```php $spreadsheet->getActiveSheet()->insertNewRowBefore(7, 2); ``` +while +```php +$spreadsheet->getActiveSheet()->removeRow(7, 2); +``` +will remove 2 rows starting at row number 7 (ie. rows 7 and 8). + +Equivalent methods exist for inserting/removing columns: + +```php +$spreadsheet->getActiveSheet()->removeColumn('C', 2); +``` + +All subsequent rows (or columns) will be moved to allow the insertion (or removal) with all formulae referencing thise cells adjusted accordingly. + +Note that this is a fairly intensive process, particularly with large worksheets, and especially if you are inserting/removing rows/columns from near beginning of the worksheet. + +If you need to insert/remove several consecutive rows/columns, always use the second argument rather than making multiple calls to insert/remove a single row/column if possible. ## Add a drawing to a worksheet From 3b55689ec17f27ce3fdb2fceb3126b6e6506eeb0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 13:33:42 +0200 Subject: [PATCH 21/25] phpcs update to version 3.7.0 to ensure it catches all the PHP 8.1 updates --- composer.json | 2 +- composer.lock | 176 +++----------------------------------------------- 2 files changed, 10 insertions(+), 168 deletions(-) diff --git a/composer.json b/composer.json index 4cf5e77d..f0c40cf8 100644 --- a/composer.json +++ b/composer.json @@ -87,7 +87,7 @@ "phpstan/phpstan": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.6", + "squizlabs/php_codesniffer": "^3.7", "tecnickcom/tcpdf": "^6.4" }, "suggest": { diff --git a/composer.lock b/composer.lock index ee0d79d1..50011b16 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e09b120ad90fc0fbbc055e1f341910e", + "content-hash": "fa9fa4814df8320d600551ca8ec11883", "packages": [ { "name": "ezyang/htmlpurifier", @@ -279,10 +279,6 @@ "keywords": [ "enum" ], - "support": { - "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.3" - }, "funding": [ { "url": "https://github.com/mnapoli", @@ -811,12 +807,12 @@ "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", "shasum": "" }, @@ -830,7 +826,6 @@ "php-parallel-lint/php-parallel-lint": "^1.3.1", "phpcompatibility/php-compatibility": "^9.0" }, - "default-branch": true, "type": "composer-plugin", "extra": { "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" @@ -999,10 +994,6 @@ "constructor", "instantiate" ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -1162,10 +1153,6 @@ ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", - "support": { - "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v1.2.2" - }, "time": "2022-04-27T13:50:54+00:00" }, { @@ -1366,11 +1353,6 @@ "php", "utf-8" ], - "support": { - "docs": "http://mpdf.github.io", - "issues": "https://github.com/mpdf/mpdf/issues", - "source": "https://github.com/mpdf/mpdf" - }, "funding": [ { "url": "https://www.paypal.me/mpdf", @@ -1426,10 +1408,6 @@ "object", "object graph" ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" - }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", @@ -1488,10 +1466,6 @@ "parser", "php" ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" - }, "time": "2021-11-30T19:35:32+00:00" }, { @@ -1649,10 +1623,6 @@ } ], "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, "time": "2022-02-21T01:04:05+00:00" }, { @@ -1693,10 +1663,6 @@ ], "description": "A library to read, parse, export and make subsets of different types of font files.", "homepage": "https://github.com/PhenX/php-font-lib", - "support": { - "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/0.5.4" - }, "time": "2021-12-17T19:44:54+00:00" }, { @@ -1739,10 +1705,6 @@ ], "description": "A library to read, parse and export to PDF SVG files.", "homepage": "https://github.com/PhenX/php-svg-lib", - "support": { - "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.4.1" - }, "time": "2022-03-07T12:52:04+00:00" }, { @@ -1845,10 +1807,6 @@ "stream", "uri" ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" - }, "time": "2015-12-19T14:08:53+00:00" }, { @@ -2067,10 +2025,6 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" - }, "time": "2022-03-15T21:29:03+00:00" }, { @@ -2134,10 +2088,6 @@ "spy", "stub" ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, "time": "2021-12-08T12:19:24+00:00" }, { @@ -2245,10 +2195,6 @@ "MIT" ], "description": "PHPUnit extensions and rules for PHPStan", - "support": { - "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.1" - }, "time": "2022-04-20T15:24:25+00:00" }, { @@ -2316,10 +2262,6 @@ "testing", "xunit" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2376,10 +2318,6 @@ "filesystem", "iterator" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2439,10 +2377,6 @@ "keywords": [ "process" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2498,10 +2432,6 @@ "keywords": [ "template" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2557,10 +2487,6 @@ "keywords": [ "timer" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2656,10 +2582,6 @@ "testing", "xunit" ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" - }, "funding": [ { "url": "https://phpunit.de/sponsors.html", @@ -2916,10 +2838,6 @@ "parser", "stylesheet" ], - "support": { - "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues", - "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0" - }, "time": "2021-12-11T13:40:54+00:00" }, { @@ -2966,10 +2884,6 @@ ], "description": "Library for parsing CLI options", "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3022,10 +2936,6 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3077,10 +2987,6 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3151,10 +3057,6 @@ "compare", "equality" ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3208,10 +3110,6 @@ ], "description": "Library for calculating the complexity of PHP code units", "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3274,10 +3172,6 @@ "unidiff", "unified diff" ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3337,10 +3231,6 @@ "environment", "hhvm" ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3414,10 +3304,6 @@ "export", "exporter" ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3478,10 +3364,6 @@ "keywords": [ "global state" ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3535,10 +3417,6 @@ ], "description": "Library for counting the lines of code in PHP source code", "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3592,10 +3470,6 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3647,10 +3521,6 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3710,10 +3580,6 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3765,10 +3631,6 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3821,10 +3683,6 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3874,10 +3732,6 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3960,16 +3814,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563", + "reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563", "shasum": "" }, "require": { @@ -4007,12 +3861,7 @@ "phpcs", "standards" ], - "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" - }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2022-06-13T06:31:38+00:00" }, { "name": "symfony/console", @@ -4603,9 +4452,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -5381,10 +5227,6 @@ "pdf417", "qrcode" ], - "support": { - "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.4.4" - }, "funding": [ { "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", @@ -5526,5 +5368,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "1.1.0" } From 0a8c97cf8ae3c7a1c60aea03703b68c2fecb4aa7 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 13:59:58 +0200 Subject: [PATCH 22/25] Add PHP 8.2 with allow fail --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b36bf6c8..29a55f44 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,10 @@ jobs: - '8.0' - '8.1' + include: + - php-version: '8.2' + experimental: true + name: PHP ${{ matrix.php-version }} steps: @@ -39,7 +43,7 @@ jobs: - name: Delete composer lock file id: composer-lock - if: ${{ matrix.php-version == '8.1' }} + if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' }} run: | rm composer.lock echo "::set-output name=flags::--ignore-platform-reqs" From 1829dea91e55e0dcf96f98c81cbd2ecac108366c Mon Sep 17 00:00:00 2001 From: FlameStorm Date: Thu, 16 Jun 2022 00:55:17 +0300 Subject: [PATCH 23/25] Ignore square-$-brackets prefix in format string (#2886) * Ignore square-$-brackets prefix in format string * Test for square-$-brackets prefix in format string issue fixed * Fix for phpstan compliance * Additional assert for checking number format of tested source cell --- .../Style/NumberFormat/Formatter.php | 3 ++ .../Reader/Xlsx/Issue2885Test.php | 30 ++++++++++++++++++ tests/data/Reader/XLSX/issue.2885.xlsx | Bin 0 -> 5436 bytes 3 files changed, 33 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php create mode 100644 tests/data/Reader/XLSX/issue.2885.xlsx diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 3e4bdc46..be195a88 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -112,6 +112,9 @@ class Formatter return $value; } + // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc + $format = (string) preg_replace('/^\[\$-[^\]]*\]/', '', $format); + $format = (string) preg_replace_callback( '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', function ($matches) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php new file mode 100644 index 00000000..82727ef8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php @@ -0,0 +1,30 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('[$-809]0%', $sheet->getStyle('A1')->getNumberFormat()->getFormatCode()); + + $finishColumns = $sheet->getHighestColumn(); + $rowsCount = $sheet->getHighestRow(); + $rows = $sheet->rangeToArray("A1:{$finishColumns}{$rowsCount}"); + self::assertSame('8%', $rows[0][0]); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.2885.xlsx b/tests/data/Reader/XLSX/issue.2885.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d7f4d48901ded48529578065895e60651d79015d GIT binary patch literal 5436 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 F{{w(ZFa7`k literal 0 HcmV?d00001 From 481cef2def54226368b35ea9de3867fac8101f33 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 15 Jun 2022 18:47:35 -0700 Subject: [PATCH 24/25] Php8.2 Deprecation in Reader/Xlsx (#2894) * Php8.2 Deprecation in Reader/Xlsx Using `${var}` will be deprecated, with the suggested resolution being to use `{$var}`. This appears to be the only place in PhpSpreadsheet which does this. Some vendor packages will need to change for 8.2 for this and other reasons. * mb_convert_encoding and HTML_ENTITIES Also scheduled to be deprecated with 8.2. It appears to have not been needed in PhpSpreadsheet in the first place. --- src/PhpSpreadsheet/Reader/Html.php | 18 ++++-------------- src/PhpSpreadsheet/Reader/Xlsx.php | 7 +------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 4edf3cf8..3d859e15 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -632,16 +632,6 @@ class Html extends BaseReader } } - /** - * Make sure mb_convert_encoding returns string. - * - * @param mixed $result - */ - private static function ensureString($result): string - { - return is_string($result) ? $result : ''; - } - /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. * @@ -660,8 +650,8 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scanFile($filename), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scanFile($filename); + $loaded = $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } @@ -683,8 +673,8 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scan($content); + $loaded = $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 8562339b..52df94e4 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -414,7 +414,7 @@ class Xlsx extends BaseReader [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; - $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS); + $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS); $theme = null; $this->styleReader = new Styles(); foreach ($wbRels->Relationship as $relx) { @@ -1849,11 +1849,6 @@ class Xlsx extends BaseReader private static function dirAdd($base, $add): string { - $add = "$add"; - if (substr($add, 0, 4) === '/xl/') { - $add = substr($add, 4); - } - return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } From 97381d43071677afdc032d94cfb5098ff8dab281 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 15 Jun 2022 19:00:33 -0700 Subject: [PATCH 25/25] Complete Support for Chart/Axis and Gridlines (#2881) Unit testing now results in 100% coverage for Axis and Properties. All the properties in methods in Gridlines were more or less duplicated in Axis, and these duplications are moved to the common ancestor Properties. So, there isn't anything left in Gridlines. PhpSpreadsheet Chart is now over 85% covered (it was below 35% until recently). Properties are in many cases set to default to null/null-string, rather than the default values they receive from Excel, and are not written to Xml if unchanged. This is consistent with how Excel behaves. A new property `crossBetween` is added to Axis, and, with support for that added to Xlsx Reader and Writer, some minor Sample peculiarities are corrected, in particular, the charts were sometimes slightly truncated on the left and right edges. --- phpstan-baseline.neon | 65 --- src/PhpSpreadsheet/Chart/Axis.php | 454 ++------------- src/PhpSpreadsheet/Chart/GridLines.php | 446 --------------- src/PhpSpreadsheet/Chart/Properties.php | 532 +++++++++++++++++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 128 +++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 348 ++++++------ .../Chart/AxisPropertiesTest.php | 226 ++++++++ .../Chart/GridlinesLineStyleTest.php | 215 +++++++ 8 files changed, 1302 insertions(+), 1112 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5c1f074e..1491a665 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1170,46 +1170,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Chart/DataSeries.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 \\#2 \\$alpha of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Parameter \\#3 \\$colorType of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$glowProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$lineProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$objectState has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$shadowProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$softEdges has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" count: 1 @@ -4385,11 +4345,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#1 \\$plotSeriesValues of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeBubbles\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|null, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|false given\\.$#" count: 1 @@ -4400,16 +4355,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, array\\|int\\|string given\\.$#" - 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: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float given\\.$#" count: 6 @@ -4425,11 +4370,6 @@ parameters: count: 42 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#6 \\$yAxis of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null given\\.$#" count: 1 @@ -4450,11 +4390,6 @@ parameters: count: 2 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 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:\\$calculateCellValues has no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 69607216..09a4febc 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -50,70 +50,6 @@ class Axis extends Properties 'alpha' => 0, ]; - /** - * Line Properties. - * - * @var mixed[] - */ - private $lineProperties = [ - 'type' => self::EXCEL_COLOR_TYPE_ARGB, - 'value' => null, - 'alpha' => 0, - ]; - - /** - * Line Style Properties. - * - * @var mixed[] - */ - private $lineStyleProperties = [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ]; - - /** - * Shadow Properties. - * - * @var mixed[] - */ - private $shadowProperties = Properties::PRESETS_OPTIONS[0]; - - /** - * Glow Properties. - * - * @var mixed[] - */ - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - /** - * Soft Edge Properties. - * - * @var mixed[] - */ - private $softEdges = [ - 'size' => null, - ]; - private const NUMERIC_FORMAT = [ Properties::FORMAT_CODE_NUMBER, Properties::FORMAT_CODE_DATE, @@ -161,33 +97,39 @@ class Axis extends Properties return (bool) $this->axisNumber['numeric']; } + public function setAxisOption(string $key, ?string $value): void + { + if (!empty($value)) { + $this->axisOptions[$key] = $value; + } + } + /** * Set Axis Options Properties. - * - * @param string $axisLabels - * @param string $horizontalCrossesValue - * @param string $horizontalCrosses - * @param string $axisOrientation - * @param string $majorTmt - * @param string $minorTmt - * @param string $minimum - * @param string $maximum - * @param string $majorUnit - * @param string $minorUnit */ - public function setAxisOptionsProperties($axisLabels, $horizontalCrossesValue = null, $horizontalCrosses = null, $axisOrientation = null, $majorTmt = null, $minorTmt = null, $minimum = null, $maximum = null, $majorUnit = null, $minorUnit = null): void - { - $this->axisOptions['axis_labels'] = (string) $axisLabels; - ($horizontalCrossesValue !== null) ? $this->axisOptions['horizontal_crosses_value'] = (string) $horizontalCrossesValue : null; - ($horizontalCrosses !== null) ? $this->axisOptions['horizontal_crosses'] = (string) $horizontalCrosses : null; - ($axisOrientation !== null) ? $this->axisOptions['orientation'] = (string) $axisOrientation : null; - ($majorTmt !== null) ? $this->axisOptions['major_tick_mark'] = (string) $majorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minimum !== null) ? $this->axisOptions['minimum'] = (string) $minimum : null; - ($maximum !== null) ? $this->axisOptions['maximum'] = (string) $maximum : null; - ($majorUnit !== null) ? $this->axisOptions['major_unit'] = (string) $majorUnit : null; - ($minorUnit !== null) ? $this->axisOptions['minor_unit'] = (string) $minorUnit : null; + public function setAxisOptionsProperties( + string $axisLabels, + ?string $horizontalCrossesValue = null, + ?string $horizontalCrosses = null, + ?string $axisOrientation = null, + ?string $majorTmt = null, + ?string $minorTmt = null, + ?string $minimum = null, + ?string $maximum = null, + ?string $majorUnit = null, + ?string $minorUnit = null + ): void { + $this->axisOptions['axis_labels'] = $axisLabels; + $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); + $this->setAxisOption('horizontal_crosses', $horizontalCrosses); + $this->setAxisOption('orientation', $axisOrientation); + $this->setAxisOption('major_tick_mark', $majorTmt); + $this->setAxisOption('minor_tick_mark', $minorTmt); + $this->setAxisOption('minor_tick_mark', $minorTmt); + $this->setAxisOption('minimum', $minimum); + $this->setAxisOption('maximum', $maximum); + $this->setAxisOption('major_unit', $majorUnit); + $this->setAxisOption('minor_unit', $minorUnit); } /** @@ -195,7 +137,7 @@ class Axis extends Properties * * @param string $property * - * @return string + * @return ?string */ public function getAxisOptionsProperty($property) { @@ -215,27 +157,15 @@ class Axis extends Properties /** * Set Fill Property. * - * @param string $color - * @param int $alpha - * @param string $AlphaType + * @param ?string $color + * @param ?int $alpha + * @param ?string $AlphaType */ - public function setFillParameters($color, $alpha = 0, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function setFillParameters($color, $alpha = null, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void { $this->fillProperties = $this->setColorProperties($color, $alpha, $AlphaType); } - /** - * Set Line Property. - * - * @param string $color - * @param int $alpha - * @param string $alphaType - */ - public function setLineParameters($color, $alpha = 0, $alphaType = self::EXCEL_COLOR_TYPE_ARGB): void - { - $this->lineProperties = $this->setColorProperties($color, $alpha, $alphaType); - } - /** * Get Fill Property. * @@ -245,323 +175,33 @@ class Axis extends Properties */ public function getFillProperty($property) { - return $this->fillProperties[$property]; + return (string) $this->fillProperties[$property]; } /** - * Get Line Property. + * Get Line Color Property. * - * @param string $property + * @param string $propertyName * - * @return string + * @return null|int|string */ - public function getLineProperty($property) + public function getLineProperty($propertyName) { - return $this->lineProperties[$property]; + return $this->lineProperties['color'][$propertyName]; } - /** - * Set Line Style Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - ($lineWidth !== null) ? $this->lineStyleProperties['width'] = $this->getExcelPointsWidth((float) $lineWidth) : null; - ($compoundType !== null) ? $this->lineStyleProperties['compound'] = (string) $compoundType : null; - ($dashType !== null) ? $this->lineStyleProperties['dash'] = (string) $dashType : null; - ($capType !== null) ? $this->lineStyleProperties['cap'] = (string) $capType : null; - ($joinType !== null) ? $this->lineStyleProperties['join'] = (string) $joinType : null; - ($headArrowType !== null) ? $this->lineStyleProperties['arrow']['head']['type'] = (string) $headArrowType : null; - ($headArrowSize !== null) ? $this->lineStyleProperties['arrow']['head']['size'] = (string) $headArrowSize : null; - ($endArrowType !== null) ? $this->lineStyleProperties['arrow']['end']['type'] = (string) $endArrowType : null; - ($endArrowSize !== null) ? $this->lineStyleProperties['arrow']['end']['size'] = (string) $endArrowSize : null; - } + /** @var string */ + private $crossBetween = ''; // 'between' or 'midCat' might be better - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) + public function setCrossBetween(string $crossBetween): self { - return $this->getArrayElementsValue($this->lineStyleProperties, $elements); - } - - /** - * Get Line Style Arrow Excel Width. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowWidth($arrow) - { - return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'w'); - } - - /** - * Get Line Style Arrow Excel Length. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowLength($arrow) - { - 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; - } + $this->crossBetween = $crossBetween; return $this; } - /** - * Set Shadow Properties. - * - * @param int $shadowPresets - * @param string $colorValue - * @param string $colorType - * @param null|int|string $colorAlpha - * @param null|float $blur - * @param null|int $angle - * @param null|float $distance - */ - public function setShadowProperties($shadowPresets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void + public function getCrossBetween(): string { - $this->setShadowPresetsProperties((int) $shadowPresets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - (int) ($colorAlpha ?? $this->shadowProperties['color']['alpha']), - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur($blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); - } - - /** - * Set Shadow Color. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) - { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); - - return $this; - } - - private const SHADOW_ARRAY_KEYS = ['size', 'color']; - - /** - * Set Shadow Properties from Mapped Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) - { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { - $reference = &$this->shadowProperties[$property_key]; - $this->setShadowPropertiesMapValues($property_val, $reference); - } - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } - - return $this; - } - - /** - * Set Shadow Color. - * - * @param null|string $color - * @param null|int $alpha - * @param null|string $alphaType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $alphaType) - { - $this->shadowProperties['color'] = $this->setColorProperties($color, $alpha, $alphaType); - - return $this; - } - - /** - * Set Shadow Blur. - * - * @param null|float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = $blur; - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param null|float|int $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if (is_numeric($angle)) { - $this->shadowProperties['direction'] = $angle; - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param null|float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = $distance; - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return null|array|int|string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param null|string $colorValue - * @param null|int $colorAlpha - * @param null|string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this->setGlowSize($size) - ->setGlowColor( - $colorValue ?? $this->glowProperties['color']['value'], - $colorAlpha ?? (int) $this->glowProperties['color']['alpha'], - $colorType ?? $this->glowProperties['color']['type'] - ); - } - - /** - * Get Glow Property. - * - * @param array|string $property - * - * @return null|string - */ - public function getGlowProperty($property) - { - return $this->getArrayElementsValue($this->glowProperties, $property); - } - - /** - * Set Glow Color. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - if ($size !== null) { - $this->glowProperties['size'] = $size; - } - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - $this->glowProperties['color'] = $this->setColorProperties($color, $alpha, $colorType); - - return $this; - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdges($size): void - { - if ($size !== null) { - $this->softEdges['size'] = $size; - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; + return $this->crossBetween; } } diff --git a/src/PhpSpreadsheet/Chart/GridLines.php b/src/PhpSpreadsheet/Chart/GridLines.php index ad254c8c..8b86ccbd 100644 --- a/src/PhpSpreadsheet/Chart/GridLines.php +++ b/src/PhpSpreadsheet/Chart/GridLines.php @@ -10,450 +10,4 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ class GridLines extends Properties { - /** - * Properties of Class: - * Object State (State for Minor Tick Mark) @var bool - * Line Properties @var array of mixed - * Shadow Properties @var array of mixed - * Glow Properties @var array of mixed - * Soft Properties @var array of mixed. - */ - private $objectState = false; - - private $lineProperties = [ - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => null, - 'alpha' => 0, - ], - 'style' => [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ], - ]; - - private $shadowProperties = Properties::PRESETS_OPTIONS[0]; - - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - private $softEdges = [ - 'size' => null, - ]; - - /** - * Get Object State. - * - * @return bool - */ - public function getObjectState() - { - return $this->objectState; - } - - /** - * Change Object State to True. - * - * @return $this - */ - private function activateObject() - { - $this->objectState = true; - - return $this; - } - - /** - * Set Line Color Properties. - * - * @param string $value - * @param int $alpha - * @param string $colorType - */ - public function setLineColorProperties($value, $alpha = 0, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void - { - $this->activateObject() - ->lineProperties['color'] = $this->setColorProperties( - $value, - $alpha, - $colorType - ); - } - - /** - * Set Line Color Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - $this->activateObject(); - ($lineWidth !== null) - ? $this->lineProperties['style']['width'] = $this->getExcelPointsWidth((float) $lineWidth) - : null; - ($compoundType !== null) - ? $this->lineProperties['style']['compound'] = (string) $compoundType - : null; - ($dashType !== null) - ? $this->lineProperties['style']['dash'] = (string) $dashType - : null; - ($capType !== null) - ? $this->lineProperties['style']['cap'] = (string) $capType - : null; - ($joinType !== null) - ? $this->lineProperties['style']['join'] = (string) $joinType - : null; - ($headArrowType !== null) - ? $this->lineProperties['style']['arrow']['head']['type'] = (string) $headArrowType - : null; - ($headArrowSize !== null) - ? $this->lineProperties['style']['arrow']['head']['size'] = (string) $headArrowSize - : null; - ($endArrowType !== null) - ? $this->lineProperties['style']['arrow']['end']['type'] = (string) $endArrowType - : null; - ($endArrowSize !== null) - ? $this->lineProperties['style']['arrow']['end']['size'] = (string) $endArrowSize - : null; - } - - /** - * Get Line Color Property. - * - * @param string $propertyName - * - * @return string - */ - public function getLineColorProperty($propertyName) - { - return $this->lineProperties['color'][$propertyName]; - } - - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) - { - return $this->getArrayElementsValue($this->lineProperties['style'], $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param string $colorValue - * @param int $colorAlpha - * @param string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this - ->activateObject() - ->setGlowSize($size) - ->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. - * - * @param string $propertyName - * - * @return string - */ - public function getGlowColor($propertyName) - { - return $this->glowProperties['color'][$propertyName]; - } - - /** - * Get Glow Size. - * - * @return string - */ - public function getGlowSize() - { - return $this->glowProperties['size']; - } - - /** - * Set Glow Size. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - $this->glowProperties['size'] = $size; - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->glowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->glowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->glowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Get Line Style Arrow Parameters. - * - * @param string $arrowSelector - * @param string $propertySelector - * - * @return string - */ - public function getLineStyleArrowParameters($arrowSelector, $propertySelector) - { - 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 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 - { - $this->activateObject() - ->setShadowPresetsProperties((int) $presets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : (int) $colorAlpha, - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur($blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); - } - - /** - * Set Shadow Presets Properties. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) - { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); - - return $this; - } - - private const SHADOW_ARRAY_KEYS = ['size', 'color']; - - /** - * Set Shadow Properties Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) - { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { - $reference = &$this->shadowProperties[$property_key]; - $this->setShadowPropertiesMapValues($property_val, $reference); - } - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } - - return $this; - } - - /** - * Set Shadow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->shadowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->shadowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->shadowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Set Shadow Blur. - * - * @param ?float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = $blur; - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param null|float|int|string $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if (is_numeric($angle)) { - $this->shadowProperties['direction'] = $angle; - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param ?float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = $distance; - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdges($size): void - { - if ($size !== null) { - $this->activateObject(); - $this->softEdges['size'] = $size; - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; - } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 6db04809..c6b2d15b 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -120,14 +120,47 @@ abstract class Properties const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy + /** @var bool */ + protected $objectState = false; // used only for minor gridlines + + /** @var array */ + protected $glowProperties = [ + 'size' => null, + 'color' => [ + 'type' => self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + ]; + + /** @var array */ + protected $softEdges = [ + 'size' => null, + ]; + + /** @var array */ + protected $shadowProperties = self::PRESETS_OPTIONS[0]; + /** - * @param float $width + * Get Object State. * - * @return float + * @return bool */ - protected function getExcelPointsWidth($width) + public function getObjectState() { - return $width * self::POINTS_WIDTH_MULTIPLIER; + return $this->objectState; + } + + /** + * Change Object State to True. + * + * @return $this + */ + protected function activateObject() + { + $this->objectState = true; + + return $this; } public static function pointsToXml(float $width): string @@ -181,27 +214,10 @@ abstract class Properties return [ 'type' => $colorType, 'value' => $color, - 'alpha' => (int) $alpha, + 'alpha' => ($alpha === null) ? null : (int) $alpha, ]; } - protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) - { - $sizes = [ - 1 => ['w' => 'sm', 'len' => 'sm'], - 2 => ['w' => 'sm', 'len' => 'med'], - 3 => ['w' => 'sm', 'len' => 'lg'], - 4 => ['w' => 'med', 'len' => 'sm'], - 5 => ['w' => 'med', 'len' => 'med'], - 6 => ['w' => 'med', 'len' => 'lg'], - 7 => ['w' => 'lg', 'len' => 'sm'], - 8 => ['w' => 'lg', 'len' => 'med'], - 9 => ['w' => 'lg', 'len' => 'lg'], - ]; - - return $sizes[$arraySelector][$arrayKaySelector]; - } - protected const PRESETS_OPTIONS = [ //NONE 0 => [ @@ -428,4 +444,476 @@ abstract class Properties return $reference; } + + /** + * Set Glow Properties. + * + * @param float $size + * @param ?string $colorValue + * @param ?int $colorAlpha + * @param ?string $colorType + */ + public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void + { + $this + ->activateObject() + ->setGlowSize($size) + ->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. + * + * @param string $propertyName + * + * @return string + */ + public function getGlowColor($propertyName) + { + return $this->glowProperties['color'][$propertyName]; + } + + /** + * Get Glow Size. + * + * @return string + */ + public function getGlowSize() + { + return $this->glowProperties['size']; + } + + /** + * Set Glow Size. + * + * @param float $size + * + * @return $this + */ + protected function setGlowSize($size) + { + $this->glowProperties['size'] = $size; + + return $this; + } + + /** + * Set Glow Color. + * + * @param ?string $color + * @param ?int $alpha + * @param ?string $colorType + * + * @return $this + */ + protected function setGlowColor($color, $alpha, $colorType) + { + if ($color !== null) { + $this->glowProperties['color']['value'] = (string) $color; + } + if ($alpha !== null) { + $this->glowProperties['color']['alpha'] = (int) $alpha; + } + if ($colorType !== null) { + $this->glowProperties['color']['type'] = (string) $colorType; + } + + return $this; + } + + /** + * Set Soft Edges Size. + * + * @param float $size + */ + public function setSoftEdges($size): void + { + if ($size !== null) { + $this->activateObject(); + $this->softEdges['size'] = $size; + } + } + + /** + * Get Soft Edges Size. + * + * @return string + */ + public function getSoftEdgesSize() + { + return $this->softEdges['size']; + } + + /** + * @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 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 + { + $this->activateObject() + ->setShadowPresetsProperties((int) $presets) + ->setShadowColor( + $colorValue ?? $this->shadowProperties['color']['value'], + $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : (int) $colorAlpha, + $colorType ?? $this->shadowProperties['color']['type'] + ) + ->setShadowBlur($blur) + ->setShadowAngle($angle) + ->setShadowDistance($distance); + } + + /** + * Set Shadow Presets Properties. + * + * @param int $presets + * + * @return $this + */ + protected function setShadowPresetsProperties($presets) + { + $this->shadowProperties['presets'] = $presets; + $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + + return $this; + } + + protected const SHADOW_ARRAY_KEYS = ['size', 'color']; + + /** + * Set Shadow Properties Values. + * + * @param mixed $reference + * + * @return $this + */ + protected function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) + { + $base_reference = $reference; + foreach ($propertiesMap as $property_key => $property_val) { + if (is_array($property_val)) { + if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { + $reference = &$this->shadowProperties[$property_key]; + $this->setShadowPropertiesMapValues($property_val, $reference); + } + } else { + if ($base_reference === null) { + $this->shadowProperties[$property_key] = $property_val; + } else { + $reference[$property_key] = $property_val; + } + } + } + + return $this; + } + + /** + * Set Shadow Color. + * + * @param string $color + * @param int $alpha + * @param string $colorType + * + * @return $this + */ + protected function setShadowColor($color, $alpha, $colorType) + { + if ($color !== null) { + $this->shadowProperties['color']['value'] = (string) $color; + } + if ($alpha !== null) { + $this->shadowProperties['color']['alpha'] = (int) $alpha; + } + if ($colorType !== null) { + $this->shadowProperties['color']['type'] = (string) $colorType; + } + + return $this; + } + + /** + * Set Shadow Blur. + * + * @param ?float $blur + * + * @return $this + */ + protected function setShadowBlur($blur) + { + if ($blur !== null) { + $this->shadowProperties['blur'] = $blur; + } + + return $this; + } + + /** + * Set Shadow Angle. + * + * @param null|float|int|string $angle + * + * @return $this + */ + protected function setShadowAngle($angle) + { + if (is_numeric($angle)) { + $this->shadowProperties['direction'] = $angle; + } + + return $this; + } + + /** + * Set Shadow Distance. + * + * @param ?float $distance + * + * @return $this + */ + protected function setShadowDistance($distance) + { + if ($distance !== null) { + $this->shadowProperties['distance'] = $distance; + } + + return $this; + } + + /** + * Get Shadow Property. + * + * @param string|string[] $elements + * + * @return string + */ + public function getShadowProperty($elements) + { + return $this->getArrayElementsValue($this->shadowProperties, $elements); + } + + /** @var array */ + protected $lineProperties = [ + 'color' => [ + 'type' => '', //self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => '', //null, + 'alpha' => null, + ], + 'style' => [ + 'width' => null, //'9525', + 'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE, + 'dash' => '', //self::LINE_STYLE_DASH_SOLID, + 'cap' => '', //self::LINE_STYLE_CAP_FLAT, + 'join' => '', //self::LINE_STYLE_JOIN_BEVEL, + 'arrow' => [ + 'head' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_5, + 'w' => '', + 'len' => '', + ], + 'end' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_8, + 'w' => '', + 'len' => '', + ], + ], + ], + ]; + + /** + * Set Line Color Properties. + * + * @param string $value + * @param ?int $alpha + * @param string $colorType + */ + public function setLineColorProperties($value, $alpha = null, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void + { + $this->activateObject() + ->lineProperties['color'] = $this->setColorProperties( + $value, + $alpha, + $colorType + ); + } + + public function setColorPropertiesArray(array $color): void + { + $this->activateObject() + ->lineProperties['color'] = $color; + } + + /** + * Get Line Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getLineColorProperty($propertyName) + { + return $this->lineProperties['color'][$propertyName]; + } + + /** + * Set Line Style Properties. + * + * @param null|float|int|string $lineWidth + * @param string $compoundType + * @param string $dashType + * @param string $capType + * @param string $joinType + * @param string $headArrowType + * @param string $headArrowSize + * @param string $endArrowType + * @param string $endArrowSize + * @param string $headArrowWidth + * @param string $headArrowLength + * @param string $endArrowWidth + * @param string $endArrowLength + */ + public function setLineStyleProperties($lineWidth = null, $compoundType = '', $dashType = '', $capType = '', $joinType = '', $headArrowType = '', $headArrowSize = '', $endArrowType = '', $endArrowSize = '', $headArrowWidth = '', $headArrowLength = '', $endArrowWidth = '', $endArrowLength = ''): void + { + $this->activateObject(); + if (is_numeric($lineWidth)) { + $this->lineProperties['style']['width'] = $lineWidth; + } + if ($compoundType !== '') { + $this->lineProperties['style']['compound'] = $compoundType; + } + if ($dashType !== '') { + $this->lineProperties['style']['dash'] = $dashType; + } + if ($capType !== '') { + $this->lineProperties['style']['cap'] = $capType; + } + if ($joinType !== '') { + $this->lineProperties['style']['join'] = $joinType; + } + if ($headArrowType !== '') { + $this->lineProperties['style']['arrow']['head']['type'] = $headArrowType; + } + if (array_key_exists($headArrowSize, self::ARROW_SIZES)) { + $this->lineProperties['style']['arrow']['head']['size'] = $headArrowSize; + $this->lineProperties['style']['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; + $this->lineProperties['style']['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; + } + if ($endArrowType !== '') { + $this->lineProperties['style']['arrow']['end']['type'] = $endArrowType; + } + if (array_key_exists($endArrowSize, self::ARROW_SIZES)) { + $this->lineProperties['style']['arrow']['end']['size'] = $endArrowSize; + $this->lineProperties['style']['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; + $this->lineProperties['style']['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; + } + if ($headArrowWidth !== '') { + $this->lineProperties['style']['arrow']['head']['w'] = $headArrowWidth; + } + if ($headArrowLength !== '') { + $this->lineProperties['style']['arrow']['head']['len'] = $headArrowLength; + } + if ($endArrowWidth !== '') { + $this->lineProperties['style']['arrow']['end']['w'] = $endArrowWidth; + } + if ($endArrowLength !== '') { + $this->lineProperties['style']['arrow']['end']['len'] = $endArrowLength; + } + } + + /** + * Get Line Style Property. + * + * @param array|string $elements + * + * @return string + */ + public function getLineStyleProperty($elements) + { + return $this->getArrayElementsValue($this->lineProperties['style'], $elements); + } + + protected const ARROW_SIZES = [ + 1 => ['w' => 'sm', 'len' => 'sm'], + 2 => ['w' => 'sm', 'len' => 'med'], + 3 => ['w' => 'sm', 'len' => 'lg'], + 4 => ['w' => 'med', 'len' => 'sm'], + 5 => ['w' => 'med', 'len' => 'med'], + 6 => ['w' => 'med', 'len' => 'lg'], + 7 => ['w' => 'lg', 'len' => 'sm'], + 8 => ['w' => 'lg', 'len' => 'med'], + 9 => ['w' => 'lg', 'len' => 'lg'], + ]; + + protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) + { + return self::ARROW_SIZES[$arraySelector][$arrayKaySelector] ?? ''; + } + + /** + * Get Line Style Arrow Parameters. + * + * @param string $arrowSelector + * @param string $propertySelector + * + * @return string + */ + public function getLineStyleArrowParameters($arrowSelector, $propertySelector) + { + return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); + } + + /** + * Get Line Style Arrow Width. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowWidth($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'w']); + } + + /** + * Get Line Style Arrow Excel Length. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowLength($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'len']); + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 8e3d6386..b046bc24 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -100,6 +100,14 @@ class Chart $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } $this->readEffects($chartDetail, $xAxis); + if (isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + } + $this->setAxisProperties($chartDetail, $xAxis); break; case 'dateAx': @@ -144,18 +152,28 @@ class Chart } } $this->readEffects($chartDetail, $whichAxis); + if ($whichAxis !== null && isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + } if (isset($chartDetail->majorGridlines)) { $majorGridlines = new GridLines(); if (isset($chartDetail->majorGridlines->spPr)) { $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); } } if (isset($chartDetail->minorGridlines)) { $minorGridlines = new GridLines(); if (isset($chartDetail->minorGridlines->spPr)) { $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); } } + $this->setAxisProperties($chartDetail, $whichAxis); break; case 'barChart': @@ -1071,4 +1089,114 @@ class Chart return $result; } + + /** + * @param null|GridLines $chartObject may be extended to include other types + */ + private function readLineStyle(SimpleXMLElement $chartDetail, $chartObject): void + { + if (!isset($chartObject, $chartDetail->spPr)) { + return; + } + $sppr = $chartDetail->spPr->children($this->aNamespace); + + if (!isset($sppr->ln)) { + return; + } + $lineWidth = null; + /** @var string */ + $lineWidthTemp = self::getAttribute($sppr->ln, 'w', 'string'); + if (is_numeric($lineWidthTemp)) { + $lineWidth = Properties::xmlToPoints($lineWidthTemp); + } + /** @var string */ + $compoundType = self::getAttribute($sppr->ln, 'cmpd', 'string'); + /** @var string */ + $dashType = self::getAttribute($sppr->ln->prstDash, 'val', 'string'); + /** @var string */ + $capType = self::getAttribute($sppr->ln, 'cap', 'string'); + if (isset($sppr->ln->miter)) { + $joinType = Properties::LINE_STYLE_JOIN_MITER; + } elseif (isset($sppr->ln->bevel)) { + $joinType = Properties::LINE_STYLE_JOIN_BEVEL; + } else { + $joinType = ''; + } + $headArrowType = ''; + $headArrowSize = ''; + $endArrowType = ''; + $endArrowSize = ''; + /** @var string */ + $headArrowType = self::getAttribute($sppr->ln->headEnd, 'type', 'string'); + /** @var string */ + $headArrowWidth = self::getAttribute($sppr->ln->headEnd, 'w', 'string'); + /** @var string */ + $headArrowLength = self::getAttribute($sppr->ln->headEnd, 'len', 'string'); + /** @var string */ + $endArrowType = self::getAttribute($sppr->ln->tailEnd, 'type', 'string'); + /** @var string */ + $endArrowWidth = self::getAttribute($sppr->ln->tailEnd, 'w', 'string'); + /** @var string */ + $endArrowLength = self::getAttribute($sppr->ln->tailEnd, 'len', 'string'); + $chartObject->setLineStyleProperties( + $lineWidth, + $compoundType, + $dashType, + $capType, + $joinType, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize, + $headArrowWidth, + $headArrowLength, + $endArrowWidth, + $endArrowLength + ); + $colorArray = $this->readColor($sppr->ln->solidFill); + $chartObject->setColorPropertiesArray($colorArray); + } + + private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAxis): void + { + if (!isset($whichAxis)) { + return; + } + if (isset($chartDetail->crossBetween)) { + $whichAxis->setCrossBetween((string) self::getAttribute($chartDetail->crossBetween, 'val', 'string')); + } + if (isset($chartDetail->majorTickMark)) { + $whichAxis->setAxisOption('major_tick_mark', (string) self::getAttribute($chartDetail->majorTickMark, 'val', 'string')); + } + if (isset($chartDetail->minorTickMark)) { + $whichAxis->setAxisOption('minor_tick_mark', (string) self::getAttribute($chartDetail->minorTickMark, 'val', 'string')); + } + if (isset($chartDetail->tickLblPos)) { + $whichAxis->setAxisOption('axis_labels', (string) self::getAttribute($chartDetail->tickLblPos, 'val', 'string')); + } + if (isset($chartDetail->crosses)) { + $whichAxis->setAxisOption('horizontal_crosses', (string) self::getAttribute($chartDetail->crosses, 'val', 'string')); + } + if (isset($chartDetail->crossesAt)) { + $whichAxis->setAxisOption('horizontal_crosses_value', (string) self::getAttribute($chartDetail->crossesAt, 'val', 'string')); + } + if (isset($chartDetail->scaling->orientation)) { + $whichAxis->setAxisOption('orientation', (string) self::getAttribute($chartDetail->scaling->orientation, 'val', 'string')); + } + if (isset($chartDetail->scaling->max)) { + $whichAxis->setAxisOption('maximum', (string) self::getAttribute($chartDetail->scaling->max, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->majorUnit)) { + $whichAxis->setAxisOption('major_unit', (string) self::getAttribute($chartDetail->majorUnit, 'val', 'string')); + } + if (isset($chartDetail->minorUnit)) { + $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index dda395ac..1bdf4fe1 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -249,11 +249,11 @@ class Chart extends WriterPart $groupType = $plotGroup->getPlotType(); if ($groupType == $chartType) { $plotStyle = $plotGroup->getPlotStyle(); - if ($groupType === DataSeries::TYPE_RADARCHART) { + if (!empty($plotStyle) && $groupType === DataSeries::TYPE_RADARCHART) { $objWriter->startElement('c:radarStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); - } elseif ($groupType === DataSeries::TYPE_SCATTERCHART) { + } elseif (!empty($plotStyle) && $groupType === DataSeries::TYPE_SCATTERCHART) { $objWriter->startElement('c:scatterStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); @@ -431,10 +431,22 @@ class Chart extends WriterPart } $objWriter->startElement('c:scaling'); - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); - $objWriter->endElement(); - $objWriter->endElement(); + if ($yAxis->getAxisOptionsProperty('maximum') !== null) { + $objWriter->startElement('c:max'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('maximum')); + $objWriter->endElement(); + } + if ($yAxis->getAxisOptionsProperty('minimum') !== null) { + $objWriter->startElement('c:min'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minimum')); + $objWriter->endElement(); + } + if (!empty($yAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); $objWriter->writeAttribute('val', 0); @@ -486,19 +498,38 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } $objWriter->startElement('c:spPr'); + if (!empty($yAxis->getFillProperty('value'))) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:' . $yAxis->getFillProperty('type')); + $objWriter->writeAttribute('val', $yAxis->getFillProperty('value')); + $alpha = $yAxis->getFillProperty('alpha'); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } + $objWriter->endElement(); + $objWriter->endElement(); + } $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $yAxis); $this->writeShadow($objWriter, $yAxis); @@ -506,14 +537,28 @@ class Chart extends WriterPart $objWriter->endElement(); // effectLst $objWriter->endElement(); // spPr + if ($yAxis->getAxisOptionsProperty('major_unit') !== null) { + $objWriter->startElement('c:majorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_unit')); + $objWriter->endElement(); + } + + if ($yAxis->getAxisOptionsProperty('minor_unit') !== null) { + $objWriter->startElement('c:minorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_unit')); + $objWriter->endElement(); + } + if ($id2 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('horizontal_crosses'))) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); + $objWriter->endElement(); + } } $objWriter->startElement('c:auto'); @@ -568,11 +613,13 @@ class Chart extends WriterPart $objWriter->endElement(); } - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + if (!empty($xAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } - $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); $objWriter->writeAttribute('val', 0); @@ -585,48 +632,8 @@ class Chart extends WriterPart $objWriter->startElement('c:majorGridlines'); $objWriter->startElement('c:spPr'); - if ($majorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$majorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill + $this->writeGridlinesLn($objWriter, $majorGridlines); - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $majorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($majorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln - } $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $majorGridlines); $this->writeShadow($objWriter, $majorGridlines); @@ -640,48 +647,7 @@ class Chart extends WriterPart $objWriter->startElement('c:minorGridlines'); $objWriter->startElement('c:spPr'); - if ($minorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$minorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $minorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($minorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln - } + $this->writeGridlinesLn($objWriter, $minorGridlines); $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $minorGridlines); @@ -737,78 +703,41 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } $objWriter->startElement('c:spPr'); - if ($xAxis->getFillProperty('value') !== null) { + if (!empty($xAxis->getFillProperty('value'))) { $objWriter->startElement('a:solidFill'); $objWriter->startElement('a:' . $xAxis->getFillProperty('type')); $objWriter->writeAttribute('val', $xAxis->getFillProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getFillProperty('alpha')); - $objWriter->endElement(); + $alpha = $xAxis->getFillProperty('alpha'); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } $objWriter->endElement(); $objWriter->endElement(); } - $objWriter->startElement('a:ln'); - - $objWriter->writeAttribute('w', $xAxis->getLineStyleProperty('width')); - $objWriter->writeAttribute('cap', $xAxis->getLineStyleProperty('cap')); - $objWriter->writeAttribute('cmpd', $xAxis->getLineStyleProperty('compound')); - - if ($xAxis->getLineProperty('value') !== null) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $xAxis->getLineProperty('type')); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('alpha')); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $xAxis->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($xAxis->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('head')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('head')); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('end')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('end')); - $objWriter->endElement(); - } - - $objWriter->endElement(); + $this->writeGridlinesLn($objWriter, $xAxis); $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $xAxis); @@ -828,14 +757,20 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value')); $objWriter->endElement(); } else { - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + $crosses = $xAxis->getAxisOptionsProperty('horizontal_crosses'); + if ($crosses) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $crosses); + $objWriter->endElement(); + } } - $objWriter->startElement('c:crossBetween'); - $objWriter->writeAttribute('val', 'midCat'); - $objWriter->endElement(); + $crossBetween = $xAxis->getCrossBetween(); + if ($crossBetween !== '') { + $objWriter->startElement('c:crossBetween'); + $objWriter->writeAttribute('val', $crossBetween); + $objWriter->endElement(); + } if ($xAxis->getAxisOptionsProperty('major_unit') !== null) { $objWriter->startElement('c:majorUnit'); @@ -1521,7 +1456,7 @@ class Chart extends WriterPart */ private function writeShadow(XMLWriter $objWriter, $xAxis): void { - if ($xAxis->getShadowProperty('effect') === null) { + if (empty($xAxis->getShadowProperty('effect'))) { return; } /** @var string */ @@ -1609,4 +1544,73 @@ class Chart extends WriterPart $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $softEdgeSize)); $objWriter->endElement(); //end softEdge } + + /** + * Write Line Style for Gridlines. + * + * @param Axis|GridLines $gridlines + */ + private function writeGridlinesLn(XMLWriter $objWriter, $gridlines): void + { + $objWriter->startElement('a:ln'); + $widthTemp = $gridlines->getLineStyleProperty('width'); + if (is_numeric($widthTemp)) { + $objWriter->writeAttribute('w', Properties::pointsToXml((float) $widthTemp)); + } + $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap')); + $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound')); + if (!empty($gridlines->getLineColorProperty('value'))) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement("a:{$gridlines->getLineColorProperty('type')}"); + $objWriter->writeAttribute('val', (string) $gridlines->getLineColorProperty('value')); + $alpha = $gridlines->getLineColorProperty('alpha'); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); // alpha + } + $objWriter->endElement(); //end srgbClr + $objWriter->endElement(); //end solidFill + } + + $dash = $gridlines->getLineStyleProperty('dash'); + if (!empty($dash)) { + $objWriter->startElement('a:prstDash'); + $this->writeNotEmpty($objWriter, 'val', $dash); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty('join') === 'miter') { + $objWriter->startElement('a:miter'); + $objWriter->writeAttribute('lim', '800000'); + $objWriter->endElement(); + } elseif ($gridlines->getLineStyleProperty('join') === 'bevel') { + $objWriter->startElement('a:bevel'); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'head', 'type'])) { + $objWriter->startElement('a:headEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowParameters('head', 'w')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowParameters('head', 'len')); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'end', 'type'])) { + $objWriter->startElement('a:tailEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowParameters('end', 'w')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowParameters('end', 'len')); + $objWriter->endElement(); + } + $objWriter->endElement(); //end ln + } + + private function writeNotEmpty(XMLWriter $objWriter, string $name, ?string $value): void + { + if ($value !== null && $value !== '') { + $objWriter->writeAttribute($name, $value); + } + } } diff --git a/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php new file mode 100644 index 00000000..91df25cb --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php @@ -0,0 +1,226 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testAxisProperties(): 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)'); + $xAxis = new Axis(); + $xAxis->setFillParameters('FF0000', null, 'srgbClr'); + self::assertSame('FF0000', $xAxis->getFillProperty('value')); + self::assertSame('', $xAxis->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis->getFillProperty('type')); + + $xAxis->setAxisOptionsProperties( + Properties::AXIS_LABELS_HIGH, // axisLabels, + null, // $horizontalCrossesValue, + Properties::HORIZONTAL_CROSSES_MAXIMUM, //horizontalCrosses + Properties::ORIENTATION_REVERSED, //axisOrientation + Properties::TICK_MARK_INSIDE, //majorTmt + Properties::TICK_MARK_OUTSIDE, //minorTmt + '8', //minimum + '68', //maximum + '20', //majorUnit + '5' //minorUnit + ); + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis->getAxisOptionsProperty('axis_labels')); + self::assertNull($xAxis->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis->getAxisOptionsProperty('orientation')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis->getAxisOptionsProperty('minor_tick_mark')); + self::assertSame('8', $xAxis->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis->getAxisOptionsProperty('minor_unit')); + + $yAxis = new Axis(); + $yAxis->setFillParameters('accent1', 30, 'schemeClr'); + self::assertSame('accent1', $yAxis->getFillProperty('value')); + self::assertSame('30', $yAxis->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis->getFillProperty('type')); + + // 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, // xAxis + $yAxis, // yAxis + null, //majorGridlines, + null // minorGridlines + ); + $xAxis2 = $chart->getChartAxisX(); + self::assertSame('FF0000', $xAxis2->getFillProperty('value')); + self::assertSame('', $xAxis2->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis2->getFillProperty('type')); + + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis2->getAxisOptionsProperty('axis_labels')); + self::assertNull($xAxis2->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis2->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis2->getAxisOptionsProperty('orientation')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis2->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis2->getAxisOptionsProperty('minor_tick_mark')); + self::assertSame('8', $xAxis2->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis2->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis2->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis2->getAxisOptionsProperty('minor_unit')); + + $yAxis2 = $chart->getChartAxisY(); + self::assertSame('accent1', $yAxis2->getFillProperty('value')); + self::assertSame('30', $yAxis2->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis2->getFillProperty('type')); + + // 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); + $xAxis3 = $chart2->getChartAxisX(); + self::assertSame('FF0000', $xAxis3->getFillProperty('value')); + self::assertSame('', $xAxis3->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis3->getFillProperty('type')); + + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis3->getAxisOptionsProperty('axis_labels')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis3->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis3->getAxisOptionsProperty('minor_tick_mark')); + self::assertNull($xAxis3->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis3->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis3->getAxisOptionsProperty('orientation')); + self::assertSame('8', $xAxis3->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis3->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis3->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis3->getAxisOptionsProperty('minor_unit')); + + $yAxis3 = $chart2->getChartAxisY(); + self::assertSame('accent1', $yAxis3->getFillProperty('value')); + self::assertSame('30', $yAxis3->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis3->getFillProperty('type')); + + $xAxis3->setAxisOrientation(Properties::ORIENTATION_NORMAL); + self::assertSame(Properties::ORIENTATION_NORMAL, $xAxis3->getAxisOptionsProperty('orientation')); + $xAxis3->setAxisOptionsProperties( + Properties::AXIS_LABELS_HIGH, // axisLabels, + '5' // $horizontalCrossesValue, + ); + self::assertSame('5', $xAxis3->getAxisOptionsProperty('horizontal_crosses_value')); + + $yAxis3->setLineColorProperties('0000FF', null, 'srgbClr'); + self::assertSame('0000FF', $yAxis3->getLineProperty('value')); + self::assertNull($yAxis3->getLineProperty('alpha')); + self::assertSame('srgbClr', $yAxis3->getLineProperty('type')); + $yAxis3->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL); + self::assertFalse($yAxis3->getAxisIsNumericFormat()); + $yAxis3->setAxisNumberProperties(Properties::FORMAT_CODE_NUMBER); + self::assertTrue($yAxis3->getAxisIsNumericFormat()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php new file mode 100644 index 00000000..997413c5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php @@ -0,0 +1,215 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testLineStyles(): 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(); + $width = 2; + $compound = Properties::LINE_STYLE_COMPOUND_THICKTHIN; + $dash = Properties::LINE_STYLE_DASH_ROUND_DOT; + $cap = Properties::LINE_STYLE_CAP_ROUND; + $join = Properties::LINE_STYLE_JOIN_MITER; + $headArrowType = Properties::LINE_STYLE_ARROW_TYPE_DIAMOND; + $headArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_2; + $endArrowType = Properties::LINE_STYLE_ARROW_TYPE_OVAL; + $endArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_3; + $majorGridlines->setLineStyleProperties( + $width, + $compound, + $dash, + $cap, + $join, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize + ); + $minorGridlines = new GridLines(); + $minorGridlines->setLineColorProperties('00FF00', 30, 'srgbClr'); + + self::assertEquals($width, $majorGridlines->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleArrowWidth('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowLength('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowParameters('end', 'len')); + + self::assertSame('00FF00', $minorGridlines->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines->getLineColorProperty('type')); + + // 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 // minorGridlines + ); + $majorGridlines2 = $chart->getMajorGridlines(); + self::assertEquals($width, $majorGridlines2->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines2->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines2->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines2->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines2->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines2 = $chart->getMinorGridlines(); + self::assertSame('00FF00', $minorGridlines2->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines2->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines2->getLineColorProperty('type')); + + // 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($width, $majorGridlines3->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines3->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines3->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines3->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines3->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($endArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines3 = $chart2->getMinorGridlines(); + self::assertSame('00FF00', $minorGridlines3->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines3->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines3->getLineColorProperty('type')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +}