From 0327b490609ab034c9bb6cbf61ce975ca9d57e7b Mon Sep 17 00:00:00 2001 From: issa <12658523+issakujitsuk@users.noreply.github.com> Date: Thu, 7 Aug 2025 07:24:22 +0900 Subject: [PATCH] Added Conditional Formatting: IconSet for Xlsx (#4560) --- .../Reader/Xlsx/ConditionalStyles.php | 38 +++++++ src/PhpSpreadsheet/Style/Conditional.php | 25 ++++- .../ConditionalFormatValueObject.php | 19 ++++ .../ConditionalIconSet.php | 96 ++++++++++++++++++ .../ConditionalFormatting/IconSetValues.php | 28 +++++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 49 ++++++++- .../Reader/Xlsx/ConditionalIconSetTest.php | 80 +++++++++++++++ .../XLSX/conditionalFormattingIconSet.xlsx | Bin 0 -> 11578 bytes 8 files changed, 329 insertions(+), 6 deletions(-) create mode 100644 src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php create mode 100644 src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php create mode 100644 tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index b09de2ad6..2892dd122 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -9,6 +9,8 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueObject; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalIconSet; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\IconSetValues; use PhpOffice\PhpSpreadsheet\Style\Style as Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; @@ -265,6 +267,8 @@ class ConditionalStyles $objConditional->setColorScale( $this->readColorScale($cfRule) ); + } elseif (isset($cfRule->iconSet)) { + $objConditional->setIconSet($this->readIconSet($cfRule)); } elseif (isset($cfRule['dxfId'])) { $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); } @@ -349,6 +353,40 @@ class ConditionalStyles return $colorScale; } + private function readIconSet(SimpleXMLElement $cfRule): ConditionalIconSet + { + $iconSet = new ConditionalIconSet(); + + if (isset($cfRule->iconSet['iconSet'])) { + $iconSet->setIconSetType(IconSetValues::from($cfRule->iconSet['iconSet'])); + } + if (isset($cfRule->iconSet['reverse'])) { + $iconSet->setReverse('1' === (string) $cfRule->iconSet['reverse']); + } + if (isset($cfRule->iconSet['showValue'])) { + $iconSet->setShowValue('1' === (string) $cfRule->iconSet['showValue']); + } + if (isset($cfRule->iconSet['custom'])) { + $iconSet->setCustom('1' === (string) $cfRule->iconSet['custom']); + } + + $cfvos = []; + foreach ($cfRule->iconSet->cfvo as $cfvoXml) { + $type = (string) $cfvoXml['type']; + $value = (string) ($cfvoXml['val'] ?? ''); + $cfvo = new ConditionalFormatValueObject($type, $value); + if (isset($cfvoXml['gte'])) { + $cfvo->setGreaterThanOrEqual('1' === (string) $cfvoXml['gte']); + } + $cfvos[] = $cfvo; + } + $iconSet->setCfvos($cfvos); + + // TODO: The cfIcon element is not implemented yet. + + return $iconSet; + } + /** @param ConditionalFormattingRuleExtension[] $conditionalFormattingRuleExtensions */ private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): void { diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index 2ef7cacd7..fedd0aa48 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Style; use PhpOffice\PhpSpreadsheet\IComparable; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalIconSet; class Conditional implements IComparable { @@ -25,6 +26,7 @@ class Conditional implements IComparable const CONDITION_TIMEPERIOD = 'timePeriod'; const CONDITION_DUPLICATES = 'duplicateValues'; const CONDITION_UNIQUE = 'uniqueValues'; + const CONDITION_ICONSET = 'iconSet'; private const CONDITION_TYPES = [ self::CONDITION_BEGINSWITH, @@ -43,6 +45,7 @@ class Conditional implements IComparable self::CONDITION_NOTCONTAINSTEXT, self::CONDITION_TIMEPERIOD, self::CONDITION_UNIQUE, + self::CONDITION_ICONSET, ]; // Operator types @@ -102,6 +105,8 @@ class Conditional implements IComparable private ?ConditionalColorScale $colorScale = null; + private ?ConditionalIconSet $iconSet = null; + private Style $style; private bool $noFormatSet = false; @@ -318,6 +323,18 @@ class Conditional implements IComparable return $this; } + public function getIconSet(): ?ConditionalIconSet + { + return $this->iconSet; + } + + public function setIconSet(ConditionalIconSet $iconSet): static + { + $this->iconSet = $iconSet; + + return $this; + } + /** * Get hash code. * @@ -327,10 +344,10 @@ class Conditional implements IComparable { return md5( $this->conditionType - . $this->operatorType - . implode(';', $this->condition) - . $this->style->getHashCode() - . __CLASS__ + . $this->operatorType + . implode(';', $this->condition) + . $this->style->getHashCode() + . __CLASS__ ); } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php index e6d1035f4..d1dfb5159 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php @@ -10,6 +10,13 @@ class ConditionalFormatValueObject private ?string $cellFormula; + /** + * For icon sets, determines whether this threshold value uses the greater + * than or equal to operator. False indicates 'greater than' is used instead + * of 'greater than or equal to'. + */ + private ?bool $greaterThanOrEqual = null; + public function __construct(string $type, null|float|int|string $value = null, ?string $cellFormula = null) { $this->type = $type; @@ -52,4 +59,16 @@ class ConditionalFormatValueObject return $this; } + + public function getGreaterThanOrEqual(): ?bool + { + return $this->greaterThanOrEqual; + } + + public function setGreaterThanOrEqual(?bool $greaterThanOrEqual): self + { + $this->greaterThanOrEqual = $greaterThanOrEqual; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php new file mode 100644 index 000000000..b7c21d869 --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php @@ -0,0 +1,96 @@ +iconSetType; + } + + public function setIconSetType(IconSetValues $type): self + { + $this->iconSetType = $type; + + return $this; + } + + public function getReverse(): ?bool + { + return $this->reverse; + } + + public function setReverse(bool $reverse): self + { + $this->reverse = $reverse; + + return $this; + } + + public function getShowValue(): ?bool + { + return $this->showValue; + } + + public function setShowValue(bool $showValue): self + { + $this->showValue = $showValue; + + return $this; + } + + public function getCustom(): ?bool + { + return $this->custom; + } + + public function setCustom(bool $custom): self + { + $this->custom = $custom; + + return $this; + } + + /** + * Get the conditional format value objects. + * + * @return ConditionalFormatValueObject[] + */ + public function getCfvos(): array + { + return $this->cfvos; + } + + /** + * Set the conditional format value objects. + * + * @param ConditionalFormatValueObject[] $cfvos + */ + public function setCfvos(array $cfvos): self + { + $this->cfvos = $cfvos; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php new file mode 100644 index 000000000..c27d36e6e --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php @@ -0,0 +1,28 @@ +startElement('iconSet'); + if ($iconSet->getIconSetType() !== null) { + $objWriter->writeAttribute('iconSet', $iconSet->getIconSetType()->value); + } + foreach ( + [ + 'reverse' => $iconSet->getReverse(), + 'showValue' => $iconSet->getShowValue(), + 'custom' => $iconSet->getCustom(), + ] as $attr => $value + ) { + self::writeAttributeIf($objWriter, $value !== null, $attr, $value ? '1' : '0'); + } + + foreach ($iconSet->getCfvos() as $cfvo) { + $objWriter->startElement('cfvo'); + $objWriter->writeAttribute('type', $cfvo->getType()); + self::writeAttributeIf( + $objWriter, + $cfvo->getValue() !== null, + 'val', + (string) $cfvo->getValue(), + ); + self::writeAttributeIf( + $objWriter, + $cfvo->getGreaterThanOrEqual() !== null, + 'gte', + $cfvo->getGreaterThanOrEqual() ? '1' : '0', + ); + $objWriter->endElement(); // end cfvo + } + + $objWriter->endElement(); // end iconSet + } + /** * Write ConditionalFormatting. */ @@ -897,6 +939,7 @@ class Worksheet extends WriterPart $objWriter, ($conditional->getConditionType() !== Conditional::CONDITION_COLORSCALE && $conditional->getConditionType() !== Conditional::CONDITION_DATABAR + && $conditional->getConditionType() !== Conditional::CONDITION_ICONSET && $conditional->getNoFormatSet() === false), 'dxfId', (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) @@ -933,6 +976,8 @@ class Worksheet extends WriterPart self::writeTimePeriodCondElements($objWriter, $conditional, $topLeftCell); } elseif ($conditional->getConditionType() === Conditional::CONDITION_COLORSCALE) { self::writeColorScaleElements($objWriter, $conditional->getColorScale()); + } elseif ($conditional->getConditionType() === Conditional::CONDITION_ICONSET) { + self::writeIconSetElements($objWriter, $conditional->getIconSet()); } else { self::writeOtherCondElements($objWriter, $conditional, $topLeftCell); } @@ -1566,8 +1611,8 @@ class Worksheet extends WriterPart self::writeElementIf( $objWriter, $this->getParentWriter()->getOffice2003Compatibility() === false - && $this->getParentWriter()->getPreCalculateFormulas() - && $calculatedValue !== null, + && $this->getParentWriter()->getPreCalculateFormulas() + && $calculatedValue !== null, 'v', (!is_array($calculatedValue) && !str_starts_with($calculatedValueString, '#')) ? StringHelper::formatNumber($calculatedValueString) : '0' diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php new file mode 100644 index 000000000..7189f6ea0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php @@ -0,0 +1,80 @@ +load($filename); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $worksheet = $reloadedSpreadsheet->getActiveSheet(); + + $columnIndex = 'A'; + foreach (IconSetValues::cases() as $iconSetValue) { + // styles + $styles = $worksheet->getConditionalStyles("{$columnIndex}2:{$columnIndex}11"); + self::assertCount(1, $styles); + + // icon set + $iconSet = $styles[0]->getIconSet(); + self::assertNotNull($iconSet); + self::assertSame($iconSetValue, $iconSet->getIconSetType() ?? IconSetValues::ThreeTrafficLights1); + + ++$columnIndex; + } + + // icon set attributes + $columnIndex = 'A'; + foreach ( + [ + ['reverse' => false, 'showValue' => false], + ['reverse' => true, 'showValue' => false], + ['reverse' => false, 'showValue' => true], + ] as $expected + ) { + $styles = $worksheet->getConditionalStyles("{$columnIndex}2:{$columnIndex}11"); + $iconSet = $styles[0]->getIconSet(); + self::assertNotNull($iconSet); + self::assertSame($expected['reverse'], $iconSet->getReverse() ?? false); + self::assertSame($expected['showValue'], $iconSet->getShowValue() ?? true); + self::assertFalse($iconSet->getCustom() ?? false); + + ++$columnIndex; + } + + // cfvos + $columnIndex = 'A'; + foreach ( + [ + [['percent', '0', true], ['percent', '33', false], ['percent', '67', true]], + [['percent', '0', true], ['num', '3', false], ['num', '7', true]], + [['percent', '0', true], ['formula', '10/3', false], ['formula', '10/2', true]], + [['percent', '0', true], ['percentile', '33', false], ['percentile', '67', true]], + ] as $expected + ) { + $styles = $worksheet->getConditionalStyles("{$columnIndex}2:{$columnIndex}11"); + $iconSet = $styles[0]->getIconSet(); + self::assertNotNull($iconSet); + $cfvos = $iconSet->getCfvos(); + self::assertCount(count($expected), $cfvos); + foreach ($expected as $i => [$type, $value, $gte]) { + $cfvo = $cfvos[$i]; + self::assertSame($type, $cfvo->getType()); + self::assertSame($value, $cfvo->getValue()); + self::assertSame($gte, $cfvo->getGreaterThanOrEqual() ?? true); + self::assertNull($cfvo->getCellFormula()); + } + + ++$columnIndex; + } + } +} diff --git a/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx b/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6c3f0539d3fad13f7c8097ab56d089e3bc4b824e GIT binary patch literal 11578 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 V!a+d%BkPU^FoW1ZEB%k&{{iD1u_OQh literal 0 HcmV?d00001