From a713e153aa4774042f237735b45f4a59512d542a Mon Sep 17 00:00:00 2001 From: redtailmatt <49280299+redtailmatt@users.noreply.github.com> Date: Sat, 30 Sep 2023 10:23:42 -0400 Subject: [PATCH] Added Conditional Formatting: ColorScale for Xlsx (#3738) * Added Conditional Formatting: ColorScale for Xlsx * Add Reader Support, Tests, Sample Also correct Phpstan and phpcs problems. * Update cond08_colorscale.php * Improve Coverage * More Coverage Improvements * Use StyleReader for Colors for ColorScale and DataBar The implementation of DataBar looks for an rgb attribute, but the color may be provided via theme attribute instead. The initial implementation for ColorScale did the same. Change both to use the existing code in Reader\Xlsx\Styles to parse the color. * Change Some Doc Blocks to Type Declarations --------- Co-authored-by: oleibman <10341515+oleibman@users.noreply.github.com> --- .../cond08_colorscale.php | 64 ++++++++++++ src/PhpSpreadsheet/Reader/Xlsx.php | 4 +- .../Reader/Xlsx/ConditionalStyles.php | 64 ++++++++++-- src/PhpSpreadsheet/Style/Conditional.php | 44 ++++----- .../ConditionalColorScale.php | 92 ++++++++++++++++++ .../ConditionalDataBar.php | 15 +-- .../ConditionalFormatValueObject.php | 35 ++----- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 66 ++++++++++++- .../Reader/Xlsx/ConditionalColorScaleTest.php | 33 +++++++ tests/data/Reader/XLSX/colorscale.xlsx | Bin 0 -> 9457 bytes 10 files changed, 339 insertions(+), 78 deletions(-) create mode 100644 samples/ConditionalFormatting/cond08_colorscale.php create mode 100644 src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalColorScaleTest.php create mode 100644 tests/data/Reader/XLSX/colorscale.xlsx diff --git a/samples/ConditionalFormatting/cond08_colorscale.php b/samples/ConditionalFormatting/cond08_colorscale.php new file mode 100644 index 000000000..d63145970 --- /dev/null +++ b/samples/ConditionalFormatting/cond08_colorscale.php @@ -0,0 +1,64 @@ +log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); +$sheet = $spreadsheet->getActiveSheet(); + +// Set document properties +$helper->log('Set document properties'); +$spreadsheet->getProperties()->setCreator('Owen Leibman') + ->setLastModifiedBy('Owen Leibman') + ->setTitle('PhpSpreadsheet Test Document') + ->setSubject('PhpSpreadsheet Test Document') + ->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.') + ->setKeywords('office PhpSpreadsheet php') + ->setCategory('Test result file'); + +// Create the worksheet +$helper->log('Add data'); +$sheet + ->setCellValue('A1', 1) + ->setCellValue('A2', 2) + ->setCellValue('A3', 8) + ->setCellValue('A4', 4) + ->setCellValue('A5', 5) + ->setCellValue('A6', 6) + ->setCellValue('A7', 7) + ->setCellValue('A8', 3) + ->setCellValue('A9', 9) + ->setCellValue('A10', 10); + +// Set conditional formatting rules and styles +$helper->log('Define conditional formatting using Color Scales'); + +$cellRange = 'A1:A10'; +$condition1 = new Conditional(); +$condition1->setConditionType(Conditional::CONDITION_COLORSCALE); +$colorScale = new ConditionalColorScale(); +$condition1->setColorScale($colorScale); +$colorScale + ->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject('min')) + ->setMidpointConditionalFormatValueObject(new ConditionalFormatValueObject('percentile', '40')) + ->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject('max')) + ->setMinimumColor(new Color('FFF8696B')) + ->setMidpointColor(new Color('FFFFEB84')) + ->setMaximumColor(new Color('FF63BE7B')); + +$conditionalStyles = [$condition1]; + +$sheet + ->getStyle($cellRange) + ->setConditionalStyles($conditionalStyles); +$sheet->setSelectedCells('B1'); + +// Save +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 57d1c1c40..fad37d7f1 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -790,10 +790,10 @@ class Xlsx extends BaseReader // Setting Conditional Styles adjusts selected cells, so we need to execute this // before reading the sheet view data to get the actual selected cells if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) { - (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->load(); + (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->load(); } if (!$this->readDataOnly && $xmlSheet->extLst) { - (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->loadFromExt($this->styleReader); + (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->loadFromExt(); } if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) { $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index 27025c1b1..280c75eb0 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -3,7 +3,9 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles as StyleReader; +use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueObject; @@ -25,11 +27,14 @@ class ConditionalStyles private array $dxfs; - public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs = []) + private StyleReader $styleReader; + + public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs, StyleReader $styleReader) { $this->worksheet = $workSheet; $this->worksheetXml = $worksheetXml; $this->dxfs = $dxfs; + $this->styleReader = $styleReader; } public function load(): void @@ -45,13 +50,13 @@ class ConditionalStyles $this->worksheet->setSelectedCells($selectedCells); } - public function loadFromExt(StyleReader $styleReader): void + public function loadFromExt(): void { $selectedCells = $this->worksheet->getSelectedCells(); $this->ns = $this->worksheetXml->getNamespaces(true); $this->setConditionalsFromExt( - $this->readConditionalsFromExt($this->worksheetXml->extLst, $styleReader) + $this->readConditionalsFromExt($this->worksheetXml->extLst) ); $this->worksheet->setSelectedCells($selectedCells); @@ -68,7 +73,7 @@ class ConditionalStyles } } - private function readConditionalsFromExt(SimpleXMLElement $extLst, StyleReader $styleReader): array + private function readConditionalsFromExt(SimpleXMLElement $extLst): array { $conditionals = []; if (!isset($extLst->ext)) { @@ -110,7 +115,7 @@ class ConditionalStyles $priority = (int) $attributes->priority; $conditional = $this->readConditionalRuleFromExt($extCfRuleXml, $attributes); - $cfStyle = $this->readStyleFromExt($extCfRuleXml, $styleReader); + $cfStyle = $this->readStyleFromExt($extCfRuleXml); $conditional->setStyle($cfStyle); $conditionals[$sqref][$priority] = $conditional; } @@ -146,17 +151,17 @@ class ConditionalStyles return $conditional; } - private function readStyleFromExt(SimpleXMLElement $extCfRuleXml, StyleReader $styleReader): Style + private function readStyleFromExt(SimpleXMLElement $extCfRuleXml): Style { $cfStyle = new Style(false, true); if ($extCfRuleXml->dxf) { $styleXML = $extCfRuleXml->dxf->children(); if ($styleXML->borders) { - $styleReader->readBorderStyle($cfStyle->getBorders(), $styleXML->borders); + $this->styleReader->readBorderStyle($cfStyle->getBorders(), $styleXML->borders); } if ($styleXML->fill) { - $styleReader->readFillStyle($cfStyle->getFill(), $styleXML->fill); + $this->styleReader->readFillStyle($cfStyle->getFill(), $styleXML->fill); } } @@ -198,6 +203,7 @@ class ConditionalStyles $conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst); $conditionalStyles = []; + /** @var SimpleXMLElement $cfRule */ foreach ($cfRules as $cfRule) { $objConditional = new Conditional(); $objConditional->setConditionType((string) $cfRule['type']); @@ -231,7 +237,11 @@ class ConditionalStyles if (isset($cfRule->dataBar)) { $objConditional->setDataBar( - $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) // @phpstan-ignore-line + $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) + ); + } elseif (isset($cfRule->colorScale)) { + $objConditional->setColorScale( + $this->readColorScale($cfRule) ); } elseif (isset($cfRule['dxfId'])) { $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); @@ -270,7 +280,7 @@ class ConditionalStyles //color if (isset($cfRule->dataBar->color)) { - $dataBar->setColor((string) $cfRule->dataBar->color['rgb']); + $dataBar->setColor($this->styleReader->readColor($cfRule->dataBar->color)); } //extLst $this->readDataBarExtLstOfConditionalRule($dataBar, $cfRule, $conditionalFormattingRuleExtensions); @@ -278,6 +288,40 @@ class ConditionalStyles return $dataBar; } + private function readColorScale(simpleXMLElement|stdClass $cfRule): ConditionalColorScale + { + $colorScale = new ConditionalColorScale(); + $types = []; + foreach ($cfRule->colorScale->cfvo as $cfvoXml) { + $attr = $cfvoXml->attributes() ?? []; + $type = (string) ($attr['type'] ?? ''); + $types[] = $type; + $val = $attr['val'] ?? null; + if ($type === 'min') { + $colorScale->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject($type, $val)); + } elseif ($type === 'percentile') { + $colorScale->setMidpointConditionalFormatValueObject(new ConditionalFormatValueObject($type, $val)); + } elseif ($type === 'max') { + $colorScale->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject($type, $val)); + } + } + $idx = 0; + foreach ($cfRule->colorScale->color as $color) { + $type = $types[$idx]; + $rgb = $this->styleReader->readColor($color); + if ($type === 'min') { + $colorScale->setMinimumColor(new Color($rgb)); + } elseif ($type === 'percentile') { + $colorScale->setMidpointColor(new Color($rgb)); + } elseif ($type === 'max') { + $colorScale->setMaximumColor(new Color($rgb)); + } + ++$idx; + } + + return $colorScale; + } + /** * @param SimpleXMLElement|stdClass $cfRule */ diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index 58d61ce87..9f6e8c7dc 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Style; use PhpOffice\PhpSpreadsheet\IComparable; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar; class Conditional implements IComparable @@ -11,6 +12,7 @@ class Conditional implements IComparable const CONDITION_NONE = 'none'; const CONDITION_BEGINSWITH = 'beginsWith'; const CONDITION_CELLIS = 'cellIs'; + const CONDITION_COLORSCALE = 'colorScale'; const CONDITION_CONTAINSBLANKS = 'containsBlanks'; const CONDITION_CONTAINSERRORS = 'containsErrors'; const CONDITION_CONTAINSTEXT = 'containsText'; @@ -27,6 +29,7 @@ class Conditional implements IComparable private const CONDITION_TYPES = [ self::CONDITION_BEGINSWITH, self::CONDITION_CELLIS, + self::CONDITION_COLORSCALE, self::CONDITION_CONTAINSBLANKS, self::CONDITION_CONTAINSERRORS, self::CONDITION_CONTAINSTEXT, @@ -91,10 +94,8 @@ class Conditional implements IComparable /** * Stop on this condition, if it matches. - * - * @var bool */ - private $stopIfTrue = false; + private bool $stopIfTrue = false; /** * Condition. @@ -103,18 +104,13 @@ class Conditional implements IComparable */ private $condition = []; - /** - * @var ConditionalDataBar - */ - private $dataBar; + private ?ConditionalDataBar $dataBar = null; + + private ?ConditionalColorScale $colorScale = null; - /** - * Style. - */ private Style $style; - /** @var bool */ - private $noFormatSet = false; + private bool $noFormatSet = false; /** * Create a new Conditional. @@ -296,21 +292,11 @@ class Conditional implements IComparable return $this; } - /** - * get DataBar. - * - * @return null|ConditionalDataBar - */ - public function getDataBar() + public function getDataBar(): ?ConditionalDataBar { return $this->dataBar; } - /** - * set DataBar. - * - * @return $this - */ public function setDataBar(ConditionalDataBar $dataBar): static { $this->dataBar = $dataBar; @@ -318,6 +304,18 @@ class Conditional implements IComparable return $this; } + public function getColorScale(): ?ConditionalColorScale + { + return $this->colorScale; + } + + public function setColorScale(ConditionalColorScale $colorScale): static + { + $this->colorScale = $colorScale; + + return $this; + } + /** * Get hash code. * diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php new file mode 100644 index 000000000..7fcc08038 --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php @@ -0,0 +1,92 @@ +minimumConditionalFormatValueObject; + } + + public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject): self + { + $this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject; + + return $this; + } + + public function getMidpointConditionalFormatValueObject(): ?ConditionalFormatValueObject + { + return $this->midpointConditionalFormatValueObject; + } + + public function setMidpointConditionalFormatValueObject(ConditionalFormatValueObject $midpointConditionalFormatValueObject): self + { + $this->midpointConditionalFormatValueObject = $midpointConditionalFormatValueObject; + + return $this; + } + + public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject + { + return $this->maximumConditionalFormatValueObject; + } + + public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject): self + { + $this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject; + + return $this; + } + + public function getMinimumColor(): ?Color + { + return $this->minimumColor; + } + + public function setMinimumColor(Color $minimumColor): self + { + $this->minimumColor = $minimumColor; + + return $this; + } + + public function getMidpointColor(): ?Color + { + return $this->midpointColor; + } + + public function setMidpointColor(Color $midpointColor): self + { + $this->midpointColor = $midpointColor; + + return $this; + } + + public function getMaximumColor(): ?Color + { + return $this->maximumColor; + } + + public function setMaximumColor(Color $maximumColor): self + { + $this->maximumColor = $maximumColor; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php index c07c753b8..370f10254 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php @@ -4,10 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting; class ConditionalDataBar { - /** attribute */ - - /** @var null|bool */ - private $showValue; + private ?bool $showValue = null; private ?ConditionalFormatValueObject $minimumConditionalFormatValueObject = null; @@ -17,18 +14,12 @@ class ConditionalDataBar private ?ConditionalFormattingRuleExtension $conditionalFormattingRuleExt = null; - /** - * @return null|bool - */ - public function getShowValue() + public function getShowValue(): ?bool { return $this->showValue; } - /** - * @param bool $showValue - */ - public function setShowValue($showValue): self + public function setShowValue(bool $showValue): self { $this->showValue = $showValue; diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php index d2149d96d..43f480804 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php @@ -4,61 +4,44 @@ namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting; class ConditionalFormatValueObject { - /** @var mixed */ - private $type; + private string $type; - /** @var mixed */ - private $value; + private null|float|int|string $value; - /** @var mixed */ - private $cellFormula; + private mixed $cellFormula; - /** - * ConditionalFormatValueObject constructor. - * - * @param null|mixed $cellFormula - */ - public function __construct(mixed $type, mixed $value = null, $cellFormula = null) + public function __construct(string $type, null|float|int|string $value = null, mixed $cellFormula = null) { $this->type = $type; $this->value = $value; $this->cellFormula = $cellFormula; } - /** - * @return mixed - */ - public function getType() + public function getType(): string { return $this->type; } - public function setType(mixed $type): self + public function setType(string $type): self { $this->type = $type; return $this; } - /** - * @return mixed - */ - public function getValue() + public function getValue(): null|float|int|string { return $this->value; } - public function setValue(mixed $value): self + public function setValue(null|float|int|string $value): self { $this->value = $value; return $this; } - /** - * @return mixed - */ - public function getCellFormula() + public function getCellFormula(): mixed { return $this->cellFormula; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 6c01911c4..7efaaf939 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -11,6 +11,7 @@ use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension; use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; @@ -685,15 +686,15 @@ class Worksheet extends WriterPart $minCfvo = $dataBar->getMinimumConditionalFormatValueObject(); if ($minCfvo) { $objWriter->startElement('cfvo'); - self::writeAttributeIf($objWriter, $minCfvo->getType(), 'type', (string) $minCfvo->getType()); - self::writeAttributeIf($objWriter, $minCfvo->getValue(), 'val', (string) $minCfvo->getValue()); + $objWriter->writeAttribute('type', $minCfvo->getType()); + self::writeAttributeIf($objWriter, $minCfvo->getValue() !== null, 'val', (string) $minCfvo->getValue()); $objWriter->endElement(); } $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject(); if ($maxCfvo) { $objWriter->startElement('cfvo'); - self::writeAttributeIf($objWriter, $maxCfvo->getType(), 'type', (string) $maxCfvo->getType()); - self::writeAttributeIf($objWriter, $maxCfvo->getValue(), 'val', (string) $maxCfvo->getValue()); + $objWriter->writeAttribute('type', $maxCfvo->getType()); + self::writeAttributeIf($objWriter, $maxCfvo->getValue() !== null, 'val', (string) $maxCfvo->getValue()); $objWriter->endElement(); } if ($dataBar->getColor()) { @@ -717,6 +718,57 @@ class Worksheet extends WriterPart } } + private static function writeColorScaleElements(XMLWriter $objWriter, ?ConditionalColorScale $colorScale): void + { + if ($colorScale) { + $objWriter->startElement('colorScale'); + + $minCfvo = $colorScale->getMinimumConditionalFormatValueObject(); + $minArgb = $colorScale->getMinimumColor()?->getARGB(); + $useMin = $minCfvo !== null || $minArgb !== null; + if ($useMin) { + $objWriter->startElement('cfvo'); + $objWriter->writeAttribute('type', $minCfvo?->getType() ?? 'min'); + self::writeAttributeIf($objWriter, $minCfvo?->getValue() !== null, 'val', (string) $minCfvo?->getValue()); + $objWriter->endElement(); + } + $midCfvo = $colorScale->getMidpointConditionalFormatValueObject(); + $midArgb = $colorScale->getMidpointColor()?->getARGB(); + $useMid = $midCfvo !== null || $midArgb !== null; + if ($useMid) { + $objWriter->startElement('cfvo'); + $objWriter->writeAttribute('type', $midCfvo?->getType() ?? 'percentile'); + $objWriter->writeAttribute('val', (string) (($midCfvo?->getValue()) ?? '50')); + $objWriter->endElement(); + } + $maxCfvo = $colorScale->getMaximumConditionalFormatValueObject(); + $maxArgb = $colorScale->getMaximumColor()?->getARGB(); + $useMax = $maxCfvo !== null || $maxArgb !== null; + if ($useMax) { + $objWriter->startElement('cfvo'); + $objWriter->writeAttribute('type', $maxCfvo?->getType() ?? 'max'); + self::writeAttributeIf($objWriter, $maxCfvo?->getValue() !== null, 'val', (string) $maxCfvo?->getValue()); + $objWriter->endElement(); + } + if ($useMin) { + $objWriter->startElement('color'); + self::writeAttributeIf($objWriter, $minArgb !== null, 'rgb', "$minArgb"); + $objWriter->endElement(); + } + if ($useMid) { + $objWriter->startElement('color'); + self::writeAttributeIf($objWriter, $midArgb !== null, 'rgb', "$midArgb"); + $objWriter->endElement(); + } + if ($useMax) { + $objWriter->startElement('color'); + self::writeAttributeIf($objWriter, $maxArgb !== null, 'rgb', "$maxArgb"); + $objWriter->endElement(); + } + $objWriter->endElement(); // end colorScale + } + } + /** * Write ConditionalFormatting. */ @@ -740,7 +792,9 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('type', $conditional->getConditionType()); self::writeAttributeIf( $objWriter, - ($conditional->getConditionType() !== Conditional::CONDITION_DATABAR && $conditional->getNoFormatSet() === false), + ($conditional->getConditionType() !== Conditional::CONDITION_COLORSCALE + && $conditional->getConditionType() !== Conditional::CONDITION_DATABAR + && $conditional->getNoFormatSet() === false), 'dxfId', (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) ); @@ -773,6 +827,8 @@ class Worksheet extends WriterPart self::writeTextCondElements($objWriter, $conditional, $topLeftCell); } elseif ($conditional->getConditionType() === Conditional::CONDITION_TIMEPERIOD) { self::writeTimePeriodCondElements($objWriter, $conditional, $topLeftCell); + } elseif ($conditional->getConditionType() === Conditional::CONDITION_COLORSCALE) { + self::writeColorScaleElements($objWriter, $conditional->getColorScale()); } else { self::writeOtherCondElements($objWriter, $conditional, $topLeftCell); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalColorScaleTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalColorScaleTest.php new file mode 100644 index 000000000..6296f8e97 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalColorScaleTest.php @@ -0,0 +1,33 @@ +load($filename); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $worksheet = $reloadedSpreadsheet->getActiveSheet(); + $styles = $worksheet->getConditionalStyles('A1:A10'); + self::assertCount(1, $styles); + $colorScale = $styles[0]->getColorScale(); + self::assertNotNull($colorScale); + self::assertNotNull($colorScale->getMinimumConditionalFormatValueObject()); + self::assertNotNull($colorScale->getMidpointConditionalFormatValueObject()); + self::assertSame('50', $colorScale->getMidpointConditionalFormatValueObject()->getValue()); + self::assertNotNull($colorScale->getMaximumConditionalFormatValueObject()); + self::assertSame('FFF8696B', $colorScale->getMinimumColor()?->getARGB()); + self::assertSame('FFFFEB84', $colorScale->getMidpointColor()?->getARGB()); + self::assertSame('FF63BE7B', $colorScale->getMaximumColor()?->getARGB()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/colorscale.xlsx b/tests/data/Reader/XLSX/colorscale.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..35652544c563637ea33a1c237befdcd53632ff28 GIT binary patch literal 9457 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 literal 0 HcmV?d00001