diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index 4aa48e17ce..5c1a9186c4 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -2,7 +2,10 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; +use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; @@ -25,7 +28,8 @@ public function load(): void { $this->setConditionalStyles( $this->worksheet, - $this->readConditionalStyles($this->worksheetXml) + $this->readConditionalStyles($this->worksheetXml), + $this->worksheetXml->extLst ); } @@ -36,14 +40,16 @@ private function readConditionalStyles($xmlSheet) foreach ($conditional->cfRule as $cfRule) { if ( ((string) $cfRule['type'] == Conditional::CONDITION_NONE - || (string) $cfRule['type'] == Conditional::CONDITION_CELLIS - || (string) $cfRule['type'] == Conditional::CONDITION_CONTAINSTEXT - || (string) $cfRule['type'] == Conditional::CONDITION_CONTAINSBLANKS - || (string) $cfRule['type'] == Conditional::CONDITION_NOTCONTAINSBLANKS - || (string) $cfRule['type'] == Conditional::CONDITION_EXPRESSION) + || (string) $cfRule['type'] == Conditional::CONDITION_CELLIS + || (string) $cfRule['type'] == Conditional::CONDITION_CONTAINSTEXT + || (string) $cfRule['type'] == Conditional::CONDITION_CONTAINSBLANKS + || (string) $cfRule['type'] == Conditional::CONDITION_NOTCONTAINSBLANKS + || (string) $cfRule['type'] == Conditional::CONDITION_EXPRESSION) && isset($this->dxfs[(int) ($cfRule['dxfId'])]) ) { $conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule; + } elseif ((string) $cfRule['type'] == Conditional::CONDITION_DATABAR) { + $conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule; } } } @@ -51,11 +57,11 @@ private function readConditionalStyles($xmlSheet) return $conditionals; } - private function setConditionalStyles(Worksheet $worksheet, array $conditionals): void + private function setConditionalStyles(Worksheet $worksheet, array $conditionals, $xmlExtLst): void { foreach ($conditionals as $ref => $cfRules) { ksort($cfRules); - $conditionalStyles = $this->readStyleRules($cfRules); + $conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst); // Extract all cell references in $ref $cellBlocks = explode(' ', str_replace('$', '', strtoupper($ref))); @@ -65,8 +71,9 @@ private function setConditionalStyles(Worksheet $worksheet, array $conditionals) } } - private function readStyleRules($cfRules) + private function readStyleRules($cfRules, $extLst) { + $conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst); $conditionalStyles = []; foreach ($cfRules as $cfRule) { $objConditional = new Conditional(); @@ -88,10 +95,49 @@ private function readStyleRules($cfRules) } else { $objConditional->addCondition((string) $cfRule->formula); } - $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); + + if (isset($cfRule->dataBar)) { + $objConditional->setDataBar($this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions)); + } else { + $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); + } + $conditionalStyles[] = $objConditional; } return $conditionalStyles; } + + private function readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions): ConditionalDataBar + { + $dataBar = new ConditionalDataBar(); + //dataBar attribute + if (isset($cfRule->dataBar['showValue'])) { + $dataBar->setShowValue((int) $cfRule->dataBar['showValue']); + } + + //dataBar children + //conditionalFormatValueObjects + $cfvoXml = $cfRule->dataBar->cfvo; + foreach ((count($cfvoXml) > 1 ? $cfvoXml : [$cfvoXml]) as $cfvo) { + $dataBar->addConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val']); + } + + //color + if (isset($cfRule->dataBar->color)) { + $dataBar->setColor(new Color((string) $cfRule->dataBar->color['rgb'])); + } + //extLst + if (isset($cfRule->extLst)) { + $ns = $cfRule->extLst->getNamespaces(true); + foreach ((count($cfRule->extLst) > 0 ? $cfRule->extLst->ext : [$cfRule->extLst->ext]) as $ext) { + $extId = (string) $ext->children($ns['x14'])->id; + if (isset($conditionalFormattingRuleExtensions[$extId]) && (string) $ext['uri'] === '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}') { + $dataBar->addConditionalFormattingRuleExtList($conditionalFormattingRuleExtensions[$extId]); + } + } + } + + return $dataBar; + } } diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index 35ec479ba5..cc6532a32b 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\ConditionalDataBar; class Conditional implements IComparable { @@ -13,6 +14,7 @@ class Conditional implements IComparable const CONDITION_EXPRESSION = 'expression'; const CONDITION_CONTAINSBLANKS = 'containsBlanks'; const CONDITION_NOTCONTAINSBLANKS = 'notContainsBlanks'; + const CONDITION_DATABAR = 'dataBar'; // Operator types const OPERATOR_NONE = ''; @@ -64,6 +66,11 @@ class Conditional implements IComparable */ private $condition = []; + /** + * @var ConditionalDataBar + */ + private $dataBar; + /** * Style. * @@ -241,6 +248,28 @@ public function setStyle(?Style $pValue = null) return $this; } + /** + * get DataBar. + * + * @return ConditionalDataBar | null + */ + public function getDataBar() + { + return $this->dataBar; + } + + /** + * set DataBar. + * + * @return $this + */ + public function setDataBar(ConditionalDataBar $dataBar) + { + $this->dataBar = $dataBar; + + return $this; + } + /** * Get hash code. * diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php new file mode 100644 index 0000000000..66a0700b51 --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php @@ -0,0 +1,112 @@ + attribute */ + private $showValue; + + /** children */ + + /** @var ConditionalFormatValueObject[] */ + private $conditionalFormatValueObjects = []; + + /** @var Color */ + private $color; + + /** @var ConditionalFormattingRuleExtension[] */ + private $conditionalFormattingRuleExtList = []; + + /** + * @return null|string + */ + public function getShowValue() + { + return $this->showValue; + } + + /** + * @param int $showValue + */ + public function setShowValue($showValue) + { + $this->showValue = $showValue; + + return $this; + } + + /** + * @return ConditionalFormatValueObject[] + */ + public function getConditionalFormatValueObjects(): array + { + return $this->conditionalFormatValueObjects; + } + + /** + * @param ConditionalFormatValueObject[] $conditionalFormatValueObjects + */ + public function setConditionalFormatValueObjects(array $conditionalFormatValueObjects) + { + $this->conditionalFormatValueObjects = $conditionalFormatValueObjects; + + return $this; + } + + /** + * @param mixed $type + * @param null|mixed $value + * @param null|mixed $cellFomula + * + * @return $this + */ + public function addConditionalFormatValueObject($type, $value = null, $cellFomula = null): self + { + $this->conditionalFormatValueObjects[] = new ConditionalFormatValueObject($type, $value, $cellFomula); + + return $this; + } + + public function getColor(): Color + { + return $this->color; + } + + public function setColor(Color $color): self + { + $this->color = $color; + + return $this; + } + + /** + * @return ConditionalFormattingRuleExtension[] + */ + public function getConditionalFormattingRuleExtList(): array + { + return $this->conditionalFormattingRuleExtList; + } + + /** + * @param ConditionalFormattingRuleExtension[] $conditionalFormattingRuleExtList + */ + public function setConditionalFormattingRuleExtList(array $conditionalFormattingRuleExtList): self + { + $this->conditionalFormattingRuleExtList = $conditionalFormattingRuleExtList; + + return $this; + } + + /** + * @return $this + */ + public function addConditionalFormattingRuleExtList(ConditionalFormattingRuleExtension $conditionalFormattingRuleExt): self + { + $this->conditionalFormattingRuleExtList[] = $conditionalFormattingRuleExt; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php new file mode 100644 index 0000000000..d23b3a0782 --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php @@ -0,0 +1,278 @@ + attributes */ + + /** @var int */ + private $minLength; + + /** @var int */ + private $maxLength; + + /** @var int */ + private $border; + + /** @var int */ + private $gradient; + + /** @var string */ + private $direction; + + /** @var int */ + private $negativeBarBorderColorSameAsPositive; + + /** @var string */ + private $axisPosition; + + // children + + /** @var ConditionalFormatValueObject[] */ + private $conditionalFormatValueObjects = []; + + /** @var string */ + private $borderColor; + + /** @var string */ + private $negativeFillColor; + + /** @var string */ + private $negativeBorderColor; + + /** @var array */ + private $axisColor = [ + 'rgb' => null, + 'theme' => null, + 'tint' => null, + ]; + + public function getXmlAttributes() + { + $ret = []; + $attributes = [ + 'minLength', 'maxLength', 'border', 'gradient', 'direction', 'negativeBarBorderColorSameAsPositive', 'axisPosition', + ]; + foreach ($attributes as $attrKey) { + if (null !== $this->{$attrKey}) { + $ret[$attrKey] = $this->{$attrKey}; + } + } + + return $ret; + } + + public function getXmlElements() + { + $ret = []; + $elms = ['borderColor', 'negativeFillColor', 'negativeBorderColor']; + foreach ($elms as $elmKey) { + if (null !== $this->{$elmKey}) { + $ret[$elmKey] = ['rgb' => $this->{$elmKey}]; + } + } + foreach (array_filter($this->axisColor) as $attrKey => $axisColorAttr) { + if (!isset($ret['axisColor'])) { + $ret['axisColor'] = []; + } + $ret['axisColor'][$attrKey] = $axisColorAttr; + } + + return $ret; + } + + /** + * @return int + */ + public function getMinLength() + { + return $this->minLength; + } + + public function setMinLength(int $minLength): self + { + $this->minLength = $minLength; + + return $this; + } + + /** + * @return int + */ + public function getMaxLength() + { + return $this->maxLength; + } + + public function setMaxLength(int $maxLength): self + { + $this->maxLength = $maxLength; + + return $this; + } + + /** + * @return int + */ + public function getBorder() + { + return $this->border; + } + + public function setBorder(int $border): self + { + $this->border = $border; + + return $this; + } + + /** + * @return int + */ + public function getGradient() + { + return $this->gradient; + } + + public function setGradient(int $gradient): self + { + $this->gradient = $gradient; + + return $this; + } + + /** + * @return string + */ + public function getDirection() + { + return $this->direction; + } + + public function setDirection(string $direction): self + { + $this->direction = $direction; + + return $this; + } + + /** + * @return int + */ + public function getNegativeBarBorderColorSameAsPositive() + { + return $this->negativeBarBorderColorSameAsPositive; + } + + public function setNegativeBarBorderColorSameAsPositive(int $negativeBarBorderColorSameAsPositive): self + { + $this->negativeBarBorderColorSameAsPositive = $negativeBarBorderColorSameAsPositive; + + return $this; + } + + /** + * @return string + */ + public function getAxisPosition() + { + return $this->axisPosition; + } + + public function setAxisPosition(string $axisPosition): self + { + $this->axisPosition = $axisPosition; + + return $this; + } + + /** + * @return ConditionalFormatValueObject[] + */ + public function getConditionalFormatValueObjects(): array + { + return $this->conditionalFormatValueObjects; + } + + /** + * @param ConditionalFormatValueObject[] $conditionalFormatValueObjects + */ + public function setConditionalFormatValueObjects(array $conditionalFormatValueObjects): self + { + $this->conditionalFormatValueObjects = $conditionalFormatValueObjects; + + return $this; + } + + public function addConditionalFormatValueObject($type, $value = null, $cellFormula = null): void + { + $this->conditionalFormatValueObjects[] = new ConditionalFormatValueObject($type, $value, $cellFormula); + } + + /** + * @return string + */ + public function getBorderColor() + { + return $this->borderColor; + } + + public function setBorderColor(string $borderColor): self + { + $this->borderColor = $borderColor; + + return $this; + } + + /** + * @return string + */ + public function getNegativeFillColor() + { + return $this->negativeFillColor; + } + + public function setNegativeFillColor(string $negativeFillColor): self + { + $this->negativeFillColor = $negativeFillColor; + + return $this; + } + + /** + * @return string + */ + public function getNegativeBorderColor() + { + return $this->negativeBorderColor; + } + + public function setNegativeBorderColor(string $negativeBorderColor): self + { + $this->negativeBorderColor = $negativeBorderColor; + + return $this; + } + + public function getAxisColor(): array + { + return $this->axisColor; + } + + /** + * @param mixed $rgb + * @param null|mixed $theme + * @param null|mixed $tint + */ + public function setAxisColor($rgb, $theme = null, $tint = null): self + { + $this->axisColor = [ + 'rgb' => $rgb, + 'theme' => $theme, + 'tint' => $tint, + ]; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php new file mode 100644 index 0000000000..c6370b86b8 --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php @@ -0,0 +1,80 @@ +type = $type; + $this->value = $value; + $this->cellFormula = $cellFormula; + } + + /** + * @return mixed + */ + public function getType() + { + return $this->type; + } + + /** + * @param mixed $type + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param mixed $value + */ + public function setValue($value) + { + $this->value = $value; + + return $this; + } + + /** + * @return mixed + */ + public function getCellFormula() + { + return $this->cellFormula; + } + + /** + * @param mixed $cellFormula + */ + public function setCellFormula($cellFormula) + { + $this->cellFormula = $cellFormula; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php new file mode 100644 index 0000000000..ce50a120f7 --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php @@ -0,0 +1,171 @@ + attributes */ + private $id; + + /** @var string Conditional Formatting Rule */ + private $cfRule; + + /** children */ + + /** @var ConditionalDataBarExtension */ + private $dataBar; + + /** @var string Sequence of References */ + private $sqref; + + /** + * ConditionalFormattingRuleExtension constructor. + * + * @param $id + */ + public function __construct($id, string $cfRule = self::CONDITION_EXTENSION_DATABAR) + { + $this->id = $id; + $this->cfRule = $cfRule; + } + + public static function parseExtLstXml($extLstXml) + { + $conditionalFormattingRuleExtensions = []; + $conditionalFormattingRuleExtensionXml = null; + if ($extLstXml instanceof SimpleXMLElement) { + foreach ((count($extLstXml) > 0 ? $extLstXml : [$extLstXml]) as $extLst) { + //this uri is conditionalFormattings + //https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627 + if (isset($extLst->ext['uri']) && (string) $extLst->ext['uri'] === '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') { + $conditionalFormattingRuleExtensionXml = $extLst->ext; + } + } + if ($conditionalFormattingRuleExtensionXml) { + $ns = $conditionalFormattingRuleExtensionXml->getNamespaces(true); + $extFormattingsXml = $conditionalFormattingRuleExtensionXml->children($ns['x14']); + + foreach ($extFormattingsXml->children($ns['x14']) as $extFormattingXml) { + $extCfRuleXml = $extFormattingXml->cfRule; + $extFormattingRuleObj = new self((string) $extCfRuleXml->attributes()->id); + $extFormattingRuleObj->setSqref((string) $extFormattingXml->children($ns['xm'])->sqref); + $conditionalFormattingRuleExtensions[$extFormattingRuleObj->getId()] = $extFormattingRuleObj; + + $extDataBarObj = new ConditionalDataBarExtension(); + $extFormattingRuleObj->setDataBar($extDataBarObj); + + $dataBarXml = $extCfRuleXml->dataBar; + self::parseExtDataBarAttributesFromXml($extDataBarObj, $dataBarXml); + self::parseExtDataBarElementChildrenFromXml($extDataBarObj, $dataBarXml, $ns); + } + } + } + + return $conditionalFormattingRuleExtensions; + } + + private static function parseExtDataBarAttributesFromXml(ConditionalDataBarExtension $extDataBarObj, SimpleXMLElement $dataBarXml): void + { + $dataBarAttribute = $dataBarXml->attributes(); + if ($dataBarAttribute->minLength) { + $extDataBarObj->setMinLength((int) $dataBarAttribute->minLength); + } + if ($dataBarAttribute->maxLength) { + $extDataBarObj->setMaxLength((int) $dataBarAttribute->maxLength); + } + if ($dataBarAttribute->border) { + $extDataBarObj->setBorder((int) $dataBarAttribute->border); + } + if ($dataBarAttribute->gradient) { + $extDataBarObj->setGradient((int) $dataBarAttribute->gradient); + } + if ($dataBarAttribute->direction) { + $extDataBarObj->setDirection((string) $dataBarAttribute->direction); + } + if ($dataBarAttribute->negativeBarBorderColorSameAsPositive) { + $extDataBarObj->setNegativeBarBorderColorSameAsPositive((int) $dataBarAttribute->negativeBarBorderColorSameAsPositive); + } + if ($dataBarAttribute->axisPosition) { + $extDataBarObj->setAxisPosition((string) $dataBarAttribute->axisPosition); + } + } + + private static function parseExtDataBarElementChildrenFromXml(ConditionalDataBarExtension $extDataBarObj, SimpleXMLElement $dataBarXml, $ns): void + { + if ($dataBarXml->borderColor) { + $extDataBarObj->setBorderColor((string) $dataBarXml->borderColor->attributes()['rgb']); + } + if ($dataBarXml->negativeFillColor) { + $extDataBarObj->setNegativeFillColor((string) $dataBarXml->negativeFillColor->attributes()['rgb']); + } + if ($dataBarXml->negativeBorderColor) { + $extDataBarObj->setNegativeBorderColor((string) $dataBarXml->negativeBorderColor->attributes()['rgb']); + } + if ($dataBarXml->axisColor) { + $axisColorAttr = $dataBarXml->axisColor->attributes(); + $extDataBarObj->setAxisColor((string) $axisColorAttr['rgb'], (string) $axisColorAttr['theme'], (string) $axisColorAttr['tint']); + } + foreach ($dataBarXml->cfvo as $cfvo) { + $f = (string) $cfvo->children($ns['xm'])->f; + $extDataBarObj->addConditionalFormatValueObject((string) $cfvo->attributes()['type'], null, (empty($f) ? null : $f)); + } + } + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * @param mixed $id + */ + public function setId($id): self + { + $this->id = $id; + + return $this; + } + + public function getCfRule(): string + { + return $this->cfRule; + } + + public function setCfRule(string $cfRule): self + { + $this->cfRule = $cfRule; + + return $this; + } + + public function getDataBar(): ConditionalDataBarExtension + { + return $this->dataBar; + } + + public function setDataBar(ConditionalDataBarExtension $dataBar): self + { + $this->dataBar = $dataBar; + + return $this; + } + + public function getSqref(): string + { + return $this->sqref; + } + + public function setSqref(string $sqref): self + { + $this->sqref = $sqref; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index b6a6fc390b..cd588ef38a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; @@ -44,6 +45,7 @@ public function writeWorksheet(PhpspreadsheetWorksheet $pSheet, $pStringTable = $objWriter->writeAttribute('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing'); $objWriter->writeAttribute('xmlns:x14', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'); + $objWriter->writeAttribute('xmlns:xm', 'http://schemas.microsoft.com/office/excel/2006/main'); $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); $objWriter->writeAttribute('mc:Ignorable', 'x14ac'); $objWriter->writeAttribute('xmlns:x14ac', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac'); @@ -114,6 +116,10 @@ public function writeWorksheet(PhpspreadsheetWorksheet $pSheet, $pStringTable = // AlternateContent $this->writeAlternateContent($objWriter, $pSheet); + // ConditionalFormattingRuleExtensionList + // (Must be inserted last. Not insert last, an Excel parse error will occur) + $this->writeExtLst($objWriter, $pSheet); + $objWriter->endElement(); // Return @@ -503,6 +509,73 @@ private static function writeTextCondElements(XMLWriter $objWriter, Conditional } } + private static function writeExtConditionalFormattingElements(XMLWriter $objWriter, ConditionalFormattingRuleExtension $ruleExtension): void + { + $prefix = 'x14'; + $objWriter->startElementNs($prefix, 'conditionalFormatting', null); + + $objWriter->startElementNs($prefix, 'cfRule', null); + $objWriter->writeAttribute('type', $ruleExtension->getCfRule()); + $objWriter->writeAttribute('id', $ruleExtension->getId()); + $objWriter->startElementNs($prefix, 'dataBar', null); + $dataBar = $ruleExtension->getDataBar(); + foreach ($dataBar->getXmlAttributes() as $attrKey => $val) { + $objWriter->writeAttribute($attrKey, $val); + } + foreach ($dataBar->getConditionalFormatValueObjects() as $cfvo) { + $objWriter->startElementNs($prefix, 'cfvo', null); + $objWriter->writeAttribute('type', $cfvo->getType()); + if ($cfvo->getCellFormula()) { + $objWriter->writeElement('xm:f', $cfvo->getCellFormula()); + } + $objWriter->endElement(); //end cfvo + } + foreach ($dataBar->getXmlElements() as $elmKey => $elmAttr) { + $objWriter->startElementNs($prefix, $elmKey, null); + foreach ($elmAttr as $attrKey => $attrVal) { + $objWriter->writeAttribute($attrKey, $attrVal); + } + $objWriter->endElement(); //end elmKey + } + $objWriter->endElement(); //end dataBar + $objWriter->endElement(); //end cfRule + $objWriter->writeElement('xm:sqref', $ruleExtension->getSqref()); + $objWriter->endElement(); //end conditionalFormatting + } + + private static function writeDataBarElements(XMLWriter $objWriter, $dataBar): void + { + if ($dataBar) { + $objWriter->startElement('dataBar'); + self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', (string) $dataBar->getShowValue()); + foreach ($dataBar->getConditionalFormatValueObjects() as $cfvo) { + $objWriter->startElement('cfvo'); + self::writeAttributeIf($objWriter, $cfvo->getType(), 'type', (string) $cfvo->getType()); + self::writeAttributeIf($objWriter, $cfvo->getValue(), 'val', (string) $cfvo->getValue()); + $objWriter->endElement(); + } + if ($dataBar->getColor()) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $dataBar->getColor()->getARGB()); + $objWriter->endElement(); + } + $objWriter->endElement(); // end dataBar + + if (count($dataBar->getConditionalFormattingRuleExtList()) > 0) { + $objWriter->startElement('extLst'); + foreach ($dataBar->getConditionalFormattingRuleExtList() as $extension) { + $objWriter->startElement('ext'); + $objWriter->writeAttribute('uri', '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}'); + $objWriter->startElementNs('x14', 'id', null); + $objWriter->text($extension->getId()); + $objWriter->endElement(); + $objWriter->endElement(); + } + $objWriter->endElement(); //end extLst + } + } + } + /** * Write ConditionalFormatting. * @@ -529,7 +602,12 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet // cfRule $objWriter->startElement('cfRule'); $objWriter->writeAttribute('type', $conditional->getConditionType()); - $objWriter->writeAttribute('dxfId', $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())); + self::writeAttributeIf( + $objWriter, + ($conditional->getConditionType() != Conditional::CONDITION_DATABAR), + 'dxfId', + $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) + ); $objWriter->writeAttribute('priority', $id++); self::writeAttributeif( @@ -548,7 +626,10 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet self::writeOtherCondElements($objWriter, $conditional, $cellCoordinate); } - $objWriter->endElement(); + // + self::writeDataBarElements($objWriter, $conditional->getDataBar()); + + $objWriter->endElement(); //end cfRule $objWriter->endElement(); } @@ -1279,4 +1360,40 @@ private function writeAlternateContent(XMLWriter $objWriter, PhpspreadsheetWorks $objWriter->writeRaw($alternateContent); } } + + /** + * write + * only implementation conditionalFormattings. + * + * @url https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627 + */ + private function writeExtLst(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void + { + $conditionalFormattingRuleExtList = []; + foreach ($pSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { + /** @var Conditional $conditional */ + foreach ($conditionalStyles as $conditional) { + $dataBar = $conditional->getDataBar(); + if ($dataBar && count($dataBar->getConditionalFormattingRuleExtList()) > 0) { + foreach ($dataBar->getConditionalFormattingRuleExtList() as $ext) { + $conditionalFormattingRuleExtList[] = $ext; + } + } + } + } + + if (count($conditionalFormattingRuleExtList) > 0) { + $conditionalFormattingRuleExtNsPrefix = 'x14'; + $objWriter->startElement('extLst'); + $objWriter->startElement('ext'); + $objWriter->writeAttribute('uri', '{78C0D931-6437-407d-A8EE-F0AAD7539E65}'); + $objWriter->startElementNs($conditionalFormattingRuleExtNsPrefix, 'conditionalFormattings', null); + foreach ($conditionalFormattingRuleExtList as $extension) { + self::writeExtConditionalFormattingElements($objWriter, $extension); + } + $objWriter->endElement(); //end conditionalFormattings + $objWriter->endElement(); //end ext + $objWriter->endElement(); //end extLst + } + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php new file mode 100644 index 0000000000..f3107baaf4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php @@ -0,0 +1,344 @@ +load($filename); + $worksheet = $spreadsheet->getActiveSheet(); + + $this->pattern1Assertion($worksheet); + $this->pattern2Assertion($worksheet); + $this->pattern3Assertion($worksheet); + $this->pattern4Assertion($worksheet); + } + + public function testReloadXlsxConditionalFormattingDataBar(): void + { + // Make sure conditionals from existing file are maintained across save + $filename = 'tests/data/Reader/XLSX/conditionalFormattingDataBarTest.xlsx'; + $outfile = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $reader = IOFactory::createReader('Xlsx'); + $spreadshee1 = $reader->load($filename); + $writer = IOFactory::createWriter($spreadshee1, 'Xlsx'); + $writer->save($outfile); + $spreadsheet = $reader->load($outfile); + unlink($outfile); + $worksheet = $spreadsheet->getActiveSheet(); + + $this->pattern1Assertion($worksheet); + $this->pattern2Assertion($worksheet); + $this->pattern3Assertion($worksheet); + $this->pattern4Assertion($worksheet); + } + + public function testNewXlsxConditionalFormattingDataBar(): void + { + // Make sure blanks/non-blanks added by PhpSpreadsheet are handled correctly + $outfile = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $spreadshee1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $sheet = $spreadshee1->getActiveSheet(); + $sheet->setCellValue('A1', 1); + $sheet->setCellValue('A2', 2); + $sheet->setCellValue('A3', 3); + $sheet->setCellValue('A4', 4); + $sheet->setCellValue('A5', 5); + $cond1 = new Conditional(); + $cond1->setConditionType(Conditional::CONDITION_DATABAR); + $cond1->setDataBar(new ConditionalDataBar()); + $cond1->getDataBar() + ->addConditionalFormatValueObject('min') + ->addConditionalFormatValueObject('max') + ->setColor(new Color(Color::COLOR_GREEN)); + $cond = [$cond1]; + $sheet->getStyle('A1:A5')->setConditionalStyles($cond); + $writer = IOFactory::createWriter($spreadshee1, 'Xlsx'); + $writer->save($outfile); + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($outfile); + unlink($outfile); + $worksheet = $spreadsheet->getActiveSheet(); + + $conditionalStyle = $worksheet->getConditionalStyles('A1:A5'); + self::assertNotEmpty($conditionalStyle); + /** @var Conditional $conditionalRule */ + $conditionalRule = $conditionalStyle[0]; + $conditions = $conditionalRule->getConditions(); + self::assertNotEmpty($conditions); + self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); + self::assertNotEmpty($conditionalRule->getDataBar()); + + $dataBar = $conditionalRule->getDataBar(); + self::assertIsArray($dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $dataBar->getConditionalFormatValueObjects()); + self::assertEquals('min', $dataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('max', $dataBar->getConditionalFormatValueObjects()[1]->getType()); + self::assertEquals(Color::COLOR_GREEN, $dataBar->getColor()->getARGB()); + } + + private function pattern1Assertion(Worksheet $worksheet): void + { + self::assertEquals( + "Type: Automatic, Automatic\nDirection: Automatic\nFills: Gradient\nAxis Position: Automatic", + $worksheet->getCell('A2')->getValue() + ); + + $conditionalStyle = $worksheet->getConditionalStyles('A3:A23'); + self::assertNotEmpty($conditionalStyle); + /** @var Conditional $conditionalRule */ + $conditionalRule = $conditionalStyle[0]; + $dataBar = $conditionalRule->getDataBar(); + + self::assertNotEmpty($dataBar); + self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); + self::assertIsArray($dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $dataBar->getConditionalFormatValueObjects()); + self::assertEquals('min', $dataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('max', $dataBar->getConditionalFormatValueObjects()[1]->getType()); + self::assertEquals('FF638EC6', $dataBar->getColor()->getARGB()); + self::assertNotEmpty($dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayNotHasKey(1, $dataBar->getConditionalFormattingRuleExtList()); + //ext + $rule1ext = $dataBar->getConditionalFormattingRuleExtList()[0]; + self::assertEquals('{72C64AE0-5CD9-164F-83D1-AB720F263E79}', $rule1ext->getId()); + self::assertEquals('dataBar', $rule1ext->getCfRule()); + self::assertEquals('A3:A23', $rule1ext->getSqref()); + $extDataBar = $rule1ext->getDataBar(); + self::assertNotEmpty($extDataBar); + $pattern1 = [ + 'minLength' => '0', + 'maxLength' => '100', + 'border' => '1', + 'gradient' => null, + 'direction' => null, + 'axisPosition' => null, + 'negativeBarBorderColorSameAsPositive' => '0', + 'borderColor' => 'FF638EC6', + 'negativeFillColor' => 'FFFF0000', + 'negativeBorderColor' => 'FFFF0000', + ]; + foreach ($pattern1 as $key => $value) { + $funcName = 'get' . ucwords($key); + self::assertEquals($value, $extDataBar->$funcName(), $funcName . ' function patten'); + } + + self::assertIsArray($extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $extDataBar->getConditionalFormatValueObjects()); + + self::assertEquals('autoMin', $extDataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('autoMax', $extDataBar->getConditionalFormatValueObjects()[1]->getType()); + + self::assertArrayHasKey('rgb', $extDataBar->getAxisColor()); + self::assertEquals('FF000000', $extDataBar->getAxisColor()['rgb']); + } + + private function pattern2Assertion(Worksheet $worksheet): void + { + self::assertEquals( + "Type: Number, Number\nValue: -5, 5\nDirection: Automatic\nFills: Solid\nAxis Position: Automatic", + $worksheet->getCell('B2')->getValue() + ); + + $conditionalStyle = $worksheet->getConditionalStyles('B3:B23'); + self::assertNotEmpty($conditionalStyle); + /** @var Conditional $conditionalRule */ + $conditionalRule = $conditionalStyle[0]; + $dataBar = $conditionalRule->getDataBar(); + + self::assertNotEmpty($dataBar); + self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); + self::assertIsArray($dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $dataBar->getConditionalFormatValueObjects()); + self::assertEquals('num', $dataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('num', $dataBar->getConditionalFormatValueObjects()[1]->getType()); + self::assertEquals('-5', $dataBar->getConditionalFormatValueObjects()[0]->getValue()); + self::assertEquals('5', $dataBar->getConditionalFormatValueObjects()[1]->getValue()); + self::assertEquals('FF63C384', $dataBar->getColor()->getARGB()); + self::assertNotEmpty($dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayNotHasKey(1, $dataBar->getConditionalFormattingRuleExtList()); + //ext + $rule1ext = $dataBar->getConditionalFormattingRuleExtList()[0]; + self::assertEquals('{98904F60-57F0-DF47-B480-691B20D325E3}', $rule1ext->getId()); + self::assertEquals('dataBar', $rule1ext->getCfRule()); + self::assertEquals('B3:B23', $rule1ext->getSqref()); + $extDataBar = $rule1ext->getDataBar(); + self::assertNotEmpty($extDataBar); + $pattern1 = [ + 'minLength' => '0', + 'maxLength' => '100', + 'border' => null, + 'gradient' => '0', + 'direction' => null, + 'axisPosition' => null, + 'negativeBarBorderColorSameAsPositive' => null, + 'borderColor' => null, + 'negativeFillColor' => 'FFFF0000', + 'negativeBorderColor' => null, + ]; + foreach ($pattern1 as $key => $value) { + $funcName = 'get' . ucwords($key); + self::assertEquals($value, $extDataBar->$funcName(), $funcName . ' function patten'); + } + + self::assertIsArray($extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $extDataBar->getConditionalFormatValueObjects()); + + self::assertEquals('num', $extDataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('num', $extDataBar->getConditionalFormatValueObjects()[1]->getType()); + self::assertEquals('-5', $extDataBar->getConditionalFormatValueObjects()[0]->getCellFormula()); + self::assertEquals('5', $extDataBar->getConditionalFormatValueObjects()[1]->getCellFormula()); + + self::assertArrayHasKey('rgb', $extDataBar->getAxisColor()); + self::assertEquals('FF000000', $extDataBar->getAxisColor()['rgb']); + } + + private function pattern3Assertion(Worksheet $worksheet): void + { + self::assertEquals( + "Type: Automatic, Automatic\nDirection: rightToLeft\nFills: Solid\nAxis Position: None", + $worksheet->getCell('C2')->getValue() + ); + + $conditionalStyle = $worksheet->getConditionalStyles('C3:C23'); + self::assertNotEmpty($conditionalStyle); + /** @var Conditional $conditionalRule */ + $conditionalRule = $conditionalStyle[0]; + $dataBar = $conditionalRule->getDataBar(); + + self::assertNotEmpty($dataBar); + self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); + self::assertIsArray($dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $dataBar->getConditionalFormatValueObjects()); + self::assertEquals('min', $dataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('max', $dataBar->getConditionalFormatValueObjects()[1]->getType()); + self::assertEquals('FFFF555A', $dataBar->getColor()->getARGB()); + self::assertNotEmpty($dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayNotHasKey(1, $dataBar->getConditionalFormattingRuleExtList()); + //ext + $rule1ext = $dataBar->getConditionalFormattingRuleExtList()[0]; + self::assertEquals('{453C04BA-7ABD-8548-8A17-D9CFD2BDABE9}', $rule1ext->getId()); + self::assertEquals('dataBar', $rule1ext->getCfRule()); + self::assertEquals('C3:C23', $rule1ext->getSqref()); + $extDataBar = $rule1ext->getDataBar(); + self::assertNotEmpty($extDataBar); + $pattern1 = [ + 'minLength' => '0', + 'maxLength' => '100', + 'border' => null, + 'gradient' => '0', + 'direction' => 'rightToLeft', + 'axisPosition' => 'none', + 'negativeBarBorderColorSameAsPositive' => null, + 'borderColor' => null, + 'negativeFillColor' => 'FFFF0000', + 'negativeBorderColor' => null, + ]; + foreach ($pattern1 as $key => $value) { + $funcName = 'get' . ucwords($key); + self::assertEquals($value, $extDataBar->$funcName(), $funcName . ' function patten'); + } + + self::assertIsArray($extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $extDataBar->getConditionalFormatValueObjects()); + + self::assertEquals('autoMin', $extDataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('autoMax', $extDataBar->getConditionalFormatValueObjects()[1]->getType()); + + self::assertArrayHasKey('rgb', $extDataBar->getAxisColor()); + self::assertEmpty($extDataBar->getAxisColor()['rgb']); + } + + private function pattern4Assertion(Worksheet $worksheet): void + { + self::assertEquals( + "type: formula, formula\nValue: =2+3, =10+10\nDirection: leftToRight\nShowDataBarOnly\nFills: Solid\nBorder: Solid\nAxis Position: Midpoint", + $worksheet->getCell('D2')->getValue() + ); + + $conditionalStyle = $worksheet->getConditionalStyles('D3:D23'); + self::assertNotEmpty($conditionalStyle); + /** @var Conditional $conditionalRule */ + $conditionalRule = $conditionalStyle[0]; + $dataBar = $conditionalRule->getDataBar(); + + self::assertNotEmpty($dataBar); + self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); + self::assertIsArray($dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $dataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $dataBar->getConditionalFormatValueObjects()); + self::assertEquals('formula', $dataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('formula', $dataBar->getConditionalFormatValueObjects()[1]->getType()); + self::assertEquals('3+2', $dataBar->getConditionalFormatValueObjects()[0]->getValue()); + self::assertEquals('10+10', $dataBar->getConditionalFormatValueObjects()[1]->getValue()); + self::assertEquals('FFFF555A', $dataBar->getColor()->getARGB()); + self::assertNotEmpty($dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayHasKey(0, $dataBar->getConditionalFormattingRuleExtList()); + self::assertArrayNotHasKey(1, $dataBar->getConditionalFormattingRuleExtList()); + //ext + $rule1ext = $dataBar->getConditionalFormattingRuleExtList()[0]; + self::assertEquals('{6C1E066A-E240-3D4A-98F8-8CC218B0DFD2}', $rule1ext->getId()); + self::assertEquals('dataBar', $rule1ext->getCfRule()); + self::assertEquals('D3:D23', $rule1ext->getSqref()); + $extDataBar = $rule1ext->getDataBar(); + self::assertNotEmpty($extDataBar); + $pattern1 = [ + 'minLength' => '0', + 'maxLength' => '100', + 'border' => '1', + 'gradient' => '0', + 'direction' => 'leftToRight', + 'axisPosition' => 'middle', + 'negativeBarBorderColorSameAsPositive' => null, + 'borderColor' => 'FF000000', + 'negativeFillColor' => 'FFFF0000', + 'negativeBorderColor' => null, + ]; + foreach ($pattern1 as $key => $value) { + $funcName = 'get' . ucwords($key); + self::assertEquals($value, $extDataBar->$funcName(), $funcName . ' function patten'); + } + self::assertIsArray($extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(0, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayHasKey(1, $extDataBar->getConditionalFormatValueObjects()); + self::assertArrayNotHasKey(2, $extDataBar->getConditionalFormatValueObjects()); + + self::assertEquals('formula', $extDataBar->getConditionalFormatValueObjects()[0]->getType()); + self::assertEquals('formula', $extDataBar->getConditionalFormatValueObjects()[1]->getType()); + self::assertEquals('3+2', $extDataBar->getConditionalFormatValueObjects()[0]->getCellFormula()); + self::assertEquals('10+10', $extDataBar->getConditionalFormatValueObjects()[1]->getCellFormula()); + + self::assertArrayHasKey('rgb', $extDataBar->getAxisColor()); + self::assertEquals('FF000000', $extDataBar->getAxisColor()['rgb']); + } +} diff --git a/tests/data/Reader/XLSX/conditionalFormattingDataBarTest.xlsx b/tests/data/Reader/XLSX/conditionalFormattingDataBarTest.xlsx new file mode 100644 index 0000000000..f1434329f0 Binary files /dev/null and b/tests/data/Reader/XLSX/conditionalFormattingDataBarTest.xlsx differ