diff --git a/samples/ConditionalFormatting/cond09_iconset.php b/samples/ConditionalFormatting/cond09_iconset.php
new file mode 100644
index 0000000000..61239b01c7
--- /dev/null
+++ b/samples/ConditionalFormatting/cond09_iconset.php
@@ -0,0 +1,110 @@
+log('Create new Spreadsheet object');
+$spreadsheet = new Spreadsheet();
+$sheet = $spreadsheet->getActiveSheet();
+
+// Set document properties
+$helper->log('Set document properties');
+$spreadsheet->getProperties()->setCreator('issakujitsuk')
+ ->setLastModifiedBy('issakujitsuk')
+ ->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');
+foreach (['A', 'B', 'C'] as $columnIndex) {
+ $sheet
+ ->setCellValue("{$columnIndex}1", 1)
+ ->setCellValue("{$columnIndex}2", 2)
+ ->setCellValue("{$columnIndex}3", 8)
+ ->setCellValue("{$columnIndex}4", 4)
+ ->setCellValue("{$columnIndex}5", 5)
+ ->setCellValue("{$columnIndex}6", 6)
+ ->setCellValue("{$columnIndex}7", 7)
+ ->setCellValue("{$columnIndex}8", 3)
+ ->setCellValue("{$columnIndex}9", 9)
+ ->setCellValue("{$columnIndex}10", 10);
+}
+
+// Set conditional formatting rules and styles
+$helper->log('Define conditional formatting using Icon Set');
+
+// 3 icons
+$sheet->getStyle('A1:A10')
+ ->setConditionalStyles([
+ makeConditionalIconSet(
+ IconSetValues::ThreeSymbols,
+ [
+ new ConditionalFormatValueObject('percent', 0),
+ new ConditionalFormatValueObject('percent', 33),
+ new ConditionalFormatValueObject('percent', 67),
+ ]
+ ),
+ ]);
+
+// 4 icons
+$sheet->getStyle('B1:B10')
+ ->setConditionalStyles([
+ makeConditionalIconSet(
+ IconSetValues::FourArrows,
+ [
+ new ConditionalFormatValueObject('percent', 0),
+ new ConditionalFormatValueObject('percent', 25),
+ new ConditionalFormatValueObject('percent', 50),
+ new ConditionalFormatValueObject('percent', 75),
+ ]
+ ),
+ ]);
+
+// 5 icons
+$sheet->getStyle('C1:C10')
+ ->setConditionalStyles([
+ makeConditionalIconSet(
+ IconSetValues::FiveQuarters,
+ [
+ new ConditionalFormatValueObject('percent', 0),
+ new ConditionalFormatValueObject('percent', 20),
+ new ConditionalFormatValueObject('percent', 40),
+ new ConditionalFormatValueObject('percent', 60),
+ new ConditionalFormatValueObject('percent', 80),
+ ]
+ ),
+ ]);
+
+// Save
+$sheet->setSelectedCells('A1');
+$helper->write($spreadsheet, __FILE__, ['Xlsx']);
+
+/**
+ * Helper function to create a Conditional object with an IconSet.
+ *
+ * @param IconSetValues $type The type of icon set
+ * @param ConditionalFormatValueObject[] $cfvos The conditional format value objects
+ */
+function makeConditionalIconSet(
+ IconSetValues $type,
+ array $cfvos,
+): Conditional {
+ $condition = new Conditional();
+ $condition->setConditionType(Conditional::CONDITION_ICONSET);
+ $iconSet = new ConditionalIconSet();
+ $condition->setIconSet($iconSet);
+ $iconSet->setIconSetType($type)
+ ->setCfvos($cfvos);
+
+ return $condition;
+}
diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php
index b09de2ad69..2892dd1225 100644
--- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php
+++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php
@@ -9,6 +9,8 @@
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 @@ private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
$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 @@ private function readColorScale(SimpleXMLElement|stdClass $cfRule): ConditionalC
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 2ef7cacd79..fedd0aa486 100644
--- a/src/PhpSpreadsheet/Style/Conditional.php
+++ b/src/PhpSpreadsheet/Style/Conditional.php
@@ -5,6 +5,7 @@
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 @@ public function setColorScale(ConditionalColorScale $colorScale): static
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 @@ public function getHashCode(): string
{
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 e6d1035f4e..d1dfb51593 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 @@ public function setCellFormula(?string $cellFormula): self
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 0000000000..b7c21d869e
--- /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 0000000000..c27d36e6ed
--- /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 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet
$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 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet
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 @@ private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell
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 0000000000..e3e77d0b40
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php
@@ -0,0 +1,87 @@
+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;
+ }
+
+ // unsupported icon sets
+ for ($columnIndex = 'R'; $columnIndex <= 'U'; ++$columnIndex) {
+ $styles = $worksheet->getConditionalStyles("{$columnIndex}2:{$columnIndex}11");
+ $iconSet = $styles[0]->getIconSet();
+ self::assertNull($iconSet);
+ }
+ }
+}
diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/ConditionalFormatIconSetTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/ConditionalFormatIconSetTest.php
new file mode 100644
index 0000000000..9d64b9b8a5
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/ConditionalFormatIconSetTest.php
@@ -0,0 +1,152 @@
+setConditionType(Conditional::CONDITION_ICONSET);
+ $iconSet = $condition->setIconSet(new ConditionalIconSet())
+ ->getIconSet();
+ self::assertNotNull($iconSet);
+ if ($type !== null) {
+ $iconSet->setIconSetType($type);
+ }
+ $iconSet->setCfvos([
+ new ConditionalFormatValueObject('percent', 0),
+ new ConditionalFormatValueObject('percent', 33),
+ new ConditionalFormatValueObject('percent', 67),
+ ]);
+ if ($reverse !== null) {
+ $iconSet->setReverse($reverse);
+ }
+ if ($showValue !== null) {
+ $iconSet->setShowValue($showValue);
+ }
+ if ($custom !== null) {
+ $iconSet->setCustom($custom);
+ }
+
+ $spreadsheet = new Spreadsheet();
+ $worksheet = $spreadsheet->getActiveSheet();
+ $worksheet->setConditionalStyles(self::COORDINATE, [$condition]);
+
+ $writer = new Xlsx($spreadsheet);
+ $writerWorksheet = new Xlsx\Worksheet($writer);
+ $data = $writerWorksheet->writeWorksheet($worksheet, []);
+
+ $expected = preg_replace(['/^\s+/m', "/\n/"], '', $expected);
+ self::assertIsString($expected);
+ self::assertStringContainsString($expected, $data);
+ }
+
+ public static function iconSetsProvider(): Generator
+ {
+ $coordinate = self::COORDINATE;
+ $cfvos = <<
+
+
+ XML;
+ foreach (IconSetValues::cases() as $type) {
+ yield $type->name => [
+ <<
+
+
+ {$cfvos}
+
+
+
+ XML,
+ $type,
+ ];
+ }
+
+ yield 'null' => [
+ <<
+
+
+ {$cfvos}
+
+
+
+ XML,
+ null,
+ ];
+
+ foreach ([1, 0] as $reverse) {
+ yield "null/reverse=$reverse" => [
+ <<
+
+
+ {$cfvos}
+
+
+
+ XML,
+ null,
+ $reverse === 1,
+ ];
+ }
+
+ foreach ([1, 0] as $showValue) {
+ yield "null/showValue=$showValue" => [
+ <<
+
+
+ {$cfvos}
+
+
+
+ XML,
+ null,
+ null,
+ $showValue === 1,
+ ];
+ }
+
+ foreach ([1, 0] as $custom) {
+ yield "null/custom=$custom" => [
+ <<
+
+
+ {$cfvos}
+
+
+
+ XML,
+ null,
+ null,
+ null,
+ $custom === 1,
+ ];
+ }
+ }
+}
diff --git a/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx b/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx
new file mode 100644
index 0000000000..e731b973f8
Binary files /dev/null and b/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx differ