From 00dae1bbdaec33869d7196552ca4bf194e8b3225 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 10 Jun 2022 01:15:57 +0200 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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)) {