diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c0e63c0d..0ca1b6c663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add FormulaRange to IgnoredErrors. [PR #4393](https://github.com/PHPOffice/PhpSpreadsheet/pull/4393) - TextGrid improvements. [PR #4418](https://github.com/PHPOffice/PhpSpreadsheet/pull/4418) - Permit read to class which extends Spreadsheet. [Discussion #4402](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4402) [PR #4404](https://github.com/PHPOffice/PhpSpreadsheet/pull/4404) +- Conditional and table formatting support for html writer [PR #4412](https://github.com/PHPOffice/PhpSpreadsheet/pull/4412) ### Removed diff --git a/docs/topics/conditional-formatting.md b/docs/topics/conditional-formatting.md index 352acbef10..735215c628 100644 --- a/docs/topics/conditional-formatting.md +++ b/docs/topics/conditional-formatting.md @@ -143,20 +143,28 @@ Currently, the following Conditional Types are supported for the following Reade MS Excel | Conditional Type | Readers | Writers ---|---|---|--- -| Cell Value | Conditional::CONDITION_CELLIS | Xlsx | Xlsx, Xls -Specific Text | Conditional::CONDITION_CONTAINSTEXT | Xlsx | Xlsx - | Conditional::CONDITION_NOTCONTAINSTEXT | Xlsx | Xlsx - | Conditional::CONDITION_BEGINSWITH | Xlsx | Xlsx - | Conditional::CONDITION_ENDSWITH | Xlsx | Xlsx -Dates Occurring | Conditional::CONDITION_TIMEPERIOD | Xlsx | Xlsx -Blanks | Conditional::CONDITION_CONTAINSBLANKS | Xlsx | Xlsx -No Blanks | Conditional::CONDITION_NOTCONTAINSBLANKS | Xlsx | Xlsx -Errors | Conditional::CONDITION_CONTAINSERRORS | Xlsx | Xlsx -No Errors | Conditional::CONDITION_NOTCONTAINSERRORS | Xlsx | Xlsx -Duplicates/Unique | Conditional::CONDITION_DUPLICATES | Xlsx | Xlsx - | Conditional::CONDITION_UNIQUE | Xlsx | Xlsx -Use a formula | Conditional::CONDITION_EXPRESSION | Xlsx | Xlsx, Xls -Data Bars | Conditional::CONDITION_DATABAR | Xlsx | Xlsx +| Cell Value | Conditional::CONDITION_CELLIS | Xlsx | Xlsx, Xls, Html +Specific Text | Conditional::CONDITION_CONTAINSTEXT | Xlsx | Xlsx, Html + | Conditional::CONDITION_NOTCONTAINSTEXT | Xlsx | Xlsx, Html + | Conditional::CONDITION_BEGINSWITH | Xlsx | Xlsx, Html + | Conditional::CONDITION_ENDSWITH | Xlsx | Xlsx, Html +Dates Occurring | Conditional::CONDITION_TIMEPERIOD | Xlsx | Xlsx, Html +Blanks | Conditional::CONDITION_CONTAINSBLANKS | Xlsx | Xlsx, Html +No Blanks | Conditional::CONDITION_NOTCONTAINSBLANKS | Xlsx | Xlsx, Html +Errors | Conditional::CONDITION_CONTAINSERRORS | Xlsx | Xlsx, Html +No Errors | Conditional::CONDITION_NOTCONTAINSERRORS | Xlsx | Xlsx, Html +Duplicates/Unique | Conditional::CONDITION_DUPLICATES | Xlsx | Xlsx, Html + | Conditional::CONDITION_UNIQUE | Xlsx | Xlsx, Html +Use a formula | Conditional::CONDITION_EXPRESSION | Xlsx | Xlsx, Xls, Html +Data Bars | Conditional::CONDITION_DATABAR | Xlsx | Xlsx, Html +Colour Scales | Conditional::COLORSCALE | Xlsx | Html + +To enable conditional formatting for Html writer, use: + +```php + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); +``` The following Conditional Types are currently not supported by any Readers or Writers: @@ -165,7 +173,6 @@ MS Excel | Conditional Type Above/Below Average | ? Top/Bottom Items | ? Top/Bottom %age | ? -Colour Scales |? Icon Sets | ? Unsupported types will by ignored by the Readers, and cannot be created through PHPSpreadsheet. diff --git a/docs/topics/tables.md b/docs/topics/tables.md new file mode 100644 index 0000000000..a16d036bf4 --- /dev/null +++ b/docs/topics/tables.md @@ -0,0 +1,16 @@ +# Tables + +## Introduction + +To make managing and analyzing a group of related data easier, you can turn a range of cells into an Excel table (previously known as an Excel list). + +## Support + +Currently tables are supported in Xlsx reader and Html Writer + +To enable table formatting for Html writer, use: + +```php + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); +``` \ No newline at end of file diff --git a/samples/Html/html_01_Basic_Conditional_Formatting.php b/samples/Html/html_01_Basic_Conditional_Formatting.php new file mode 100644 index 0000000000..c2a1efd1a1 --- /dev/null +++ b/samples/Html/html_01_Basic_Conditional_Formatting.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath . ' with conditional formatting'); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/html_02_More_Conditional_Formatting.php b/samples/Html/html_02_More_Conditional_Formatting.php new file mode 100644 index 0000000000..b8971f0e37 --- /dev/null +++ b/samples/Html/html_02_More_Conditional_Formatting.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath . ' with conditional formatting'); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/html_03_Color_Scale.php b/samples/Html/html_03_Color_Scale.php new file mode 100644 index 0000000000..ed2296bfb4 --- /dev/null +++ b/samples/Html/html_03_Color_Scale.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath . ' with color scale'); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/html_04_Table_Format_without_Conditional.php b/samples/Html/html_04_Table_Format_without_Conditional.php new file mode 100644 index 0000000000..396af5375a --- /dev/null +++ b/samples/Html/html_04_Table_Format_without_Conditional.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable table formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setTableFormats(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/html_05_Table_Format_with_Conditional.php b/samples/Html/html_05_Table_Format_with_Conditional.php new file mode 100644 index 0000000000..bdfa6ab88c --- /dev/null +++ b/samples/Html/html_05_Table_Format_with_Conditional.php @@ -0,0 +1,27 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable table formatting output'); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setTableFormats(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/templates/BasicConditionalFormatting.xlsx b/samples/templates/BasicConditionalFormatting.xlsx new file mode 100644 index 0000000000..962c0583f3 Binary files /dev/null and b/samples/templates/BasicConditionalFormatting.xlsx differ diff --git a/samples/templates/ColourScale.xlsx b/samples/templates/ColourScale.xlsx new file mode 100644 index 0000000000..606f964dbd Binary files /dev/null and b/samples/templates/ColourScale.xlsx differ diff --git a/samples/templates/ConditionalFormattingConditions.xlsx b/samples/templates/ConditionalFormattingConditions.xlsx new file mode 100644 index 0000000000..6f1f315763 Binary files /dev/null and b/samples/templates/ConditionalFormattingConditions.xlsx differ diff --git a/samples/templates/TableFormat.xlsx b/samples/templates/TableFormat.xlsx new file mode 100644 index 0000000000..3112e20171 Binary files /dev/null and b/samples/templates/TableFormat.xlsx differ diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index f1f15926ff..2d17a49590 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -692,6 +692,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $this->styleReader->setNamespace($mainNS); $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles); $dxfs = $this->styleReader->dxfs($this->readDataOnly); + $tableStyles = $this->styleReader->tableStyles($this->readDataOnly); $styles = $this->styleReader->styles(); // Read content after setting the styles @@ -1000,7 +1001,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'); } - $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS); + $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs); if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) { foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) { @@ -2311,12 +2312,14 @@ private function readTables( string $dir, string $fileWorksheet, ZipArchive $zip, - string $namespaceTable + string $namespaceTable, + array $tableStyles, + array $dxfs ): void { if ($xmlSheet && $xmlSheet->tableParts) { $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0]; if (((int) $attributes['count']) > 0) { - $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable); + $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs); } } } @@ -2327,7 +2330,9 @@ private function readTablesInTablesFile( string $fileWorksheet, ZipArchive $zip, Worksheet $docSheet, - string $namespaceTable + string $namespaceTable, + array $tableStyles, + array $dxfs ): void { foreach ($xmlSheet->tableParts->tablePart as $tablePart) { $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT); @@ -2346,7 +2351,7 @@ private function readTablesInTablesFile( if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable); - (new TableReader($docSheet, $tableXml))->load(); + (new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index a03fa71b24..436d9ffb83 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -193,6 +193,13 @@ private function setConditionalStyles(Worksheet $worksheet, array $conditionals, // N.B. In Excel UI, intersection is space and union is comma. // But in Xml, intersection is comma and union is space. $cellRangeReference = str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], strtoupper($cellRangeReference)); + + foreach ($conditionalStyles as $cs) { + $scale = $cs->getColorScale(); + if ($scale !== null) { + $scale->setSqRef($cellRangeReference, $worksheet); + } + } $worksheet->getStyle($cellRangeReference)->setConditionalStyles($conditionalStyles); } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 676ea81760..c1e27e9e32 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -12,6 +12,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; use PhpOffice\PhpSpreadsheet\Style\Style; +use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableDxfsStyle; use SimpleXMLElement; use stdClass; @@ -447,6 +448,46 @@ public function dxfs(bool $readDataOnly = false): array return $dxfs; } + // get TableStyles + public function tableStyles(bool $readDataOnly = false): array + { + $tableStyles = []; + if (!$readDataOnly && $this->styleXml) { + // Conditional Styles + if ($this->styleXml->tableStyles) { + foreach ($this->styleXml->tableStyles->tableStyle as $s) { + $attrs = Xlsx::getAttributes($s); + if (isset($attrs['name'][0])) { + $style = new TableDxfsStyle((string) ($attrs['name'][0])); + foreach ($s->tableStyleElement as $e) { + $a = Xlsx::getAttributes($e); + if (isset($a['dxfId'][0], $a['type'][0])) { + switch ($a['type'][0]) { + case 'headerRow': + $style->setHeaderRow((int) ($a['dxfId'][0])); + + break; + case 'firstRowStripe': + $style->setFirstRowStripe((int) ($a['dxfId'][0])); + + break; + case 'secondRowStripe': + $style->setSecondRowStripe((int) ($a['dxfId'][0])); + + break; + default: + } + } + } + $tableStyles[] = $style; + } + } + } + } + + return $tableStyles; + } + public function styles(): array { return $this->styles; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php index a63c817d4f..c84b8198f6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php @@ -25,20 +25,20 @@ public function __construct(Worksheet $workSheet, SimpleXMLElement $tableXml) /** * Loads Table into the Worksheet. */ - public function load(): void + public function load(array $tableStyles, array $dxfs): void { $this->tableAttributes = $this->tableXml->attributes() ?? []; // Remove all "$" in the table range $tableRange = (string) preg_replace('/\$/', '', $this->tableAttributes['ref'] ?? ''); if (str_contains($tableRange, ':')) { - $this->readTable($tableRange); + $this->readTable($tableRange, $tableStyles, $dxfs); } } /** * Read Table from xml. */ - private function readTable(string $tableRange): void + private function readTable(string $tableRange, array $tableStyles, array $dxfs): void { $table = new Table($tableRange); $table->setName((string) ($this->tableAttributes['displayName'] ?? '')); @@ -47,7 +47,7 @@ private function readTable(string $tableRange): void $this->readTableAutoFilter($table, $this->tableXml->autoFilter); $this->readTableColumns($table, $this->tableXml->tableColumns); - $this->readTableStyle($table, $this->tableXml->tableStyleInfo); + $this->readTableStyle($table, $this->tableXml->tableStyleInfo, $tableStyles, $dxfs); (new AutoFilter($table, $this->tableXml))->load(); $this->worksheet->addTable($table); @@ -100,7 +100,7 @@ private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXm /** * Reads TableStyle from xml. */ - private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void + private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml, array $tableStyles, array $dxfs): void { $tableStyle = new TableStyle(); $attributes = $tableStyleInfoXml->attributes(); @@ -110,6 +110,12 @@ private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXm $tableStyle->setShowColumnStripes((string) $attributes['showColumnStripes'] === '1'); $tableStyle->setShowFirstColumn((string) $attributes['showFirstColumn'] === '1'); $tableStyle->setShowLastColumn((string) $attributes['showLastColumn'] === '1'); + + foreach ($tableStyles as $style) { + if ($style->getName() === (string) $attributes['name']) { + $tableStyle->setTableDxfsStyle($style, $dxfs); + } + } } $table->setStyle($tableStyle); } diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index d476bdffd2..736b72be5e 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -269,8 +269,16 @@ public function addCondition($condition): static /** * Get Style. */ - public function getStyle(): Style + public function getStyle(mixed $cellData = null): Style { + if ($this->conditionType === self::CONDITION_COLORSCALE && $cellData !== null && $this->colorScale !== null && is_numeric($cellData)) { + $style = new Style(); + $style->getFill()->setFillType(Fill::FILL_SOLID); + $style->getFill()->getStartColor()->setARGB($this->colorScale->getColorForValue((float) $cellData)); + + return $style; + } + return $this->style; } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php index 61027975aa..6b6e59965f 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php @@ -111,6 +111,7 @@ public function evaluateConditional(Conditional $conditional): bool // Last 7 Days AND(TODAY()-FLOOR(,1)<=6,FLOOR(,1)<=TODAY()) Conditional::CONDITION_TIMEPERIOD, Conditional::CONDITION_EXPRESSION => $this->processExpression($conditional), + Conditional::CONDITION_COLORSCALE => $this->processColorScale($conditional), default => false, }; } @@ -141,8 +142,8 @@ protected function conditionCellAdjustment(array $matches): float|int|string { $column = $matches[6]; $row = $matches[7]; - if (!str_contains($column, '$')) { + // $column = Coordinate::stringFromColumnIndex($this->cellColumn); $column = Coordinate::columnIndexFromString($column); $column += $this->cellColumn - $this->referenceColumn; $column = Coordinate::stringFromColumnIndex($column); @@ -214,6 +215,15 @@ protected function processOperatorComparison(Conditional $conditional): bool return $this->evaluateExpression($expression); } + protected function processColorScale(Conditional $conditional): bool + { + if (is_numeric($this->wrapCellValue()) && $conditional->getColorScale()?->colorScaleReadyForUse()) { + return true; + } + + return false; + } + protected function processRangeOperator(Conditional $conditional): bool { $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php index bcf59dee86..f8826f05fa 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php @@ -12,8 +12,11 @@ class CellStyleAssessor protected StyleMerger $styleMerger; + protected Cell $cell; + public function __construct(Cell $cell, string $conditionalRange) { + $this->cell = $cell; $this->cellMatcher = new CellMatcher($cell, $conditionalRange); $this->styleMerger = new StyleMerger($cell->getStyle()); } @@ -26,7 +29,7 @@ public function matchConditions(array $conditionalStyles = []): Style foreach ($conditionalStyles as $conditional) { if ($this->cellMatcher->evaluateConditional($conditional) === true) { // Merging the conditional style into the base style goes in here - $this->styleMerger->mergeStyle($conditional->getStyle()); + $this->styleMerger->mergeStyle($conditional->getStyle($this->cell->getValue())); if ($conditional->getStopIfTrue() === true) { break; } @@ -35,4 +38,28 @@ public function matchConditions(array $conditionalStyles = []): Style return $this->styleMerger->getStyle(); } + + /** + * @param Conditional[] $conditionalStyles + */ + public function matchConditionsReturnNullIfNoneMatched(array $conditionalStyles, string $cellData, bool $stopAtFirstMatch = false): ?Style + { + $matched = false; + $value = (float) $cellData; + foreach ($conditionalStyles as $conditional) { + if ($this->cellMatcher->evaluateConditional($conditional) === true) { + $matched = true; + // Merging the conditional style into the base style goes in here + $this->styleMerger->mergeStyle($conditional->getStyle($value)); + if ($conditional->getStopIfTrue() === true || $stopAtFirstMatch) { + break; + } + } + } + if ($matched) { + return $this->styleMerger->getStyle(); + } + + return null; + } } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php index 7fcc08038d..e11abd122a 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Percentiles; use PhpOffice\PhpSpreadsheet\Style\Color; class ConditionalColorScale @@ -18,6 +19,18 @@ class ConditionalColorScale private ?Color $maximumColor = null; + private ?string $sqref = null; + + private array $valueArray = []; + + private float $minValue = 0; + + private float $maxValue = 0; + + private float $midValue = 0; + + private ?\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet = null; + public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->minimumConditionalFormatValueObject; @@ -89,4 +102,155 @@ public function setMaximumColor(Color $maximumColor): self return $this; } + + public function getSqRef(): ?string + { + return $this->sqref; + } + + public function setSqRef(string $sqref, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): self + { + $this->sqref = $sqref; + $this->worksheet = $worksheet; + + return $this; + } + + public function setScaleArray(): self + { + if ($this->sqref !== null && $this->worksheet !== null) { + $values = $this->worksheet->rangesToArray($this->sqref, null, true, true, true); + $this->valueArray = []; + foreach ($values as $key => $value) { + foreach ($value as $k => $v) { + $this->valueArray[] = (float) $v; + } + } + $this->prepareColorScale(); + } + + return $this; + } + + public function getColorForValue(float $value): string + { + if ($this->minimumColor === null || $this->midpointColor === null || $this->maximumColor === null) { + return 'FF000000'; + } + $minColor = $this->minimumColor->getARGB(); + $midColor = $this->midpointColor->getARGB(); + $maxColor = $this->maximumColor->getARGB(); + + if ($minColor === null || $midColor === null || $maxColor === null) { + return 'FF000000'; + } + + if ($value <= $this->minValue) { + return $minColor; + } + if ($value >= $this->maxValue) { + return $maxColor; + } + if ($value == $this->midValue) { + return $midColor; + } + if ($value < $this->midValue) { + $blend = ($value - $this->minValue) / ($this->midValue - $this->minValue); + $alpha1 = hexdec(substr($minColor, 0, 2)); + $alpha2 = hexdec(substr($midColor, 0, 2)); + $red1 = hexdec(substr($minColor, 2, 2)); + $red2 = hexdec(substr($midColor, 2, 2)); + $green1 = hexdec(substr($minColor, 4, 2)); + $green2 = hexdec(substr($midColor, 4, 2)); + $blue1 = hexdec(substr($minColor, 6, 2)); + $blue2 = hexdec(substr($midColor, 6, 2)); + + return strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend)))); + } + $blend = ($value - $this->midValue) / ($this->maxValue - $this->midValue); + $alpha1 = hexdec(substr($midColor, 0, 2)); + $alpha2 = hexdec(substr($maxColor, 0, 2)); + $red1 = hexdec(substr($midColor, 2, 2)); + $red2 = hexdec(substr($maxColor, 2, 2)); + $green1 = hexdec(substr($midColor, 4, 2)); + $green2 = hexdec(substr($maxColor, 4, 2)); + $blue1 = hexdec(substr($midColor, 6, 2)); + $blue2 = hexdec(substr($maxColor, 6, 2)); + + return strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend)))); + } + + private function getLimitValue(string $type, float $value = 0, float $formula = 0): float + { + if (count($this->valueArray) === 0) { + return 0; + } + switch ($type) { + case 'min': + return (float) min($this->valueArray); + case 'max': + return (float) max($this->valueArray); + case 'percentile': + return (float) Percentiles::PERCENTILE($this->valueArray, (float) ($value / 100)); + case 'formula': + return $formula; + case 'percent': + $min = (float) min($this->valueArray); + $max = (float) max($this->valueArray); + + return $min + (float) ($value / 100) * ($max - $min); + default: + return 0; + } + } + + /** + * Prepares color scale for execution, see the first if for variables that must be set beforehand. + */ + public function prepareColorScale(): self + { + if ($this->minimumConditionalFormatValueObject !== null && $this->maximumConditionalFormatValueObject !== null && $this->minimumColor !== null && $this->maximumColor !== null) { + if ($this->midpointConditionalFormatValueObject !== null && $this->midpointConditionalFormatValueObject->getType() !== 'None') { + $this->minValue = $this->getLimitValue($this->minimumConditionalFormatValueObject->getType(), (float) $this->minimumConditionalFormatValueObject->getValue(), (float) $this->minimumConditionalFormatValueObject->getCellFormula()); + $this->midValue = $this->getLimitValue($this->midpointConditionalFormatValueObject->getType(), (float) $this->midpointConditionalFormatValueObject->getValue(), (float) $this->midpointConditionalFormatValueObject->getCellFormula()); + $this->maxValue = $this->getLimitValue($this->maximumConditionalFormatValueObject->getType(), (float) $this->maximumConditionalFormatValueObject->getValue(), (float) $this->maximumConditionalFormatValueObject->getCellFormula()); + } else { + $this->minValue = $this->getLimitValue($this->minimumConditionalFormatValueObject->getType(), (float) $this->minimumConditionalFormatValueObject->getValue(), (float) $this->minimumConditionalFormatValueObject->getCellFormula()); + $this->maxValue = $this->getLimitValue($this->maximumConditionalFormatValueObject->getType(), (float) $this->maximumConditionalFormatValueObject->getValue(), (float) $this->maximumConditionalFormatValueObject->getCellFormula()); + $this->midValue = (float) ($this->minValue + $this->maxValue) / 2; + $blend = 0.5; + + $minColor = $this->minimumColor->getARGB(); + $maxColor = $this->maximumColor->getARGB(); + + if ($minColor !== null && $maxColor !== null) { + $alpha1 = hexdec(substr($minColor, 0, 2)); + $alpha2 = hexdec(substr($maxColor, 0, 2)); + $red1 = hexdec(substr($minColor, 2, 2)); + $red2 = hexdec(substr($maxColor, 2, 2)); + $green1 = hexdec(substr($minColor, 4, 2)); + $green2 = hexdec(substr($maxColor, 4, 2)); + $blue1 = hexdec(substr($minColor, 6, 2)); + $blue2 = hexdec(substr($maxColor, 6, 2)); + $this->midpointColor = new Color(strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend))))); + } else { + $this->midpointColor = null; + } + } + } + + return $this; + } + + /** + * Checks that all needed color scale data is in place. + */ + public function colorScaleReadyForUse(): bool + { + if ($this->minimumColor === null || $this->midpointColor === null || $this->maximumColor === null) { + return false; + } + + return true; + } } diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 7f5b876eee..072beab2d6 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -540,6 +540,19 @@ public function setAutoFilter(AutoFilter $autoFilter): self return $this; } + /** + * Get the row number on this table for given coordinates. + */ + public function getRowNumber(string $coordinate): int + { + $range = $this->getRange(); + $coords = Coordinate::splitRange($range); + $firstCell = Coordinate::coordinateFromString($coords[0][0]); + $thisCell = Coordinate::coordinateFromString($coordinate); + + return (int) $thisCell[1] - (int) $firstCell[1]; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Worksheet/Table/TableDxfsStyle.php b/src/PhpSpreadsheet/Worksheet/Table/TableDxfsStyle.php new file mode 100644 index 0000000000..e674a71463 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Table/TableDxfsStyle.php @@ -0,0 +1,170 @@ +name = $name; + } + + /** + * Get name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Set header row dxfs index. + */ + public function setHeaderRow(int $row): self + { + $this->headerRow = $row; + + return $this; + } + + /** + * Get header row dxfs index. + */ + public function getHeaderRow(): ?int + { + return $this->headerRow; + } + + /** + * Set first row stripe dxfs index. + */ + public function setFirstRowStripe(int $row): self + { + $this->firstRowStripe = $row; + + return $this; + } + + /** + * Get first row stripe dxfs index. + */ + public function getFirstRowStripe(): ?int + { + return $this->firstRowStripe; + } + + /** + * Set second row stripe dxfs index. + */ + public function setSecondRowStripe(int $row): self + { + $this->secondRowStripe = $row; + + return $this; + } + + /** + * Get second row stripe dxfs index. + */ + public function getSecondRowStripe(): ?int + { + return $this->secondRowStripe; + } + + /** + * Set Header row Style. + */ + public function setHeaderRowStyle(Style $style): self + { + $this->headerRowStyle = $style; + + return $this; + } + + /** + * Get Header row Style. + */ + public function getHeaderRowStyle(): ?Style + { + return $this->headerRowStyle; + } + + /** + * Set first row stripe Style. + */ + public function setFirstRowStripeStyle(Style $style): self + { + $this->firstRowStripeStyle = $style; + + return $this; + } + + /** + * Get first row stripe Style. + */ + public function getFirstRowStripeStyle(): ?Style + { + return $this->firstRowStripeStyle; + } + + /** + * Set second row stripe Style. + */ + public function setSecondRowStripeStyle(Style $style): self + { + $this->secondRowStripeStyle = $style; + + return $this; + } + + /** + * Get second row stripe Style. + */ + public function getSecondRowStripeStyle(): ?Style + { + return $this->secondRowStripeStyle; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php index 81153027da..2c0173c19c 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php +++ b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php @@ -93,6 +93,11 @@ class TableStyle */ private bool $showColumnStripes = false; + /** + * TableDxfsStyle. + */ + private ?TableDxfsStyle $tableStyle = null; + /** * Table. */ @@ -198,6 +203,34 @@ public function setShowColumnStripes(bool $showColumnStripes): self return $this; } + /** + * Get this Style's Dxfs TableStyle. + */ + public function getTableDxfsStyle(): ?TableDxfsStyle + { + return $this->tableStyle; + } + + /** + * Set this Style's Dxfs TableStyle. + */ + public function setTableDxfsStyle(TableDxfsStyle $tableStyle, array $dxfs): self + { + $this->tableStyle = $tableStyle; + + if ($this->tableStyle->getHeaderRow() !== null && isset($dxfs[$this->tableStyle->getHeaderRow()])) { + $this->tableStyle->setHeaderRowStyle($dxfs[$this->tableStyle->getHeaderRow()]); + } + if ($this->tableStyle->getFirstRowStripe() !== null && isset($dxfs[$this->tableStyle->getFirstRowStripe()])) { + $this->tableStyle->setFirstRowStripeStyle($dxfs[$this->tableStyle->getFirstRowStripe()]); + } + if ($this->tableStyle->getSecondRowStripe() !== null && isset($dxfs[$this->tableStyle->getSecondRowStripe()])) { + $this->tableStyle->setSecondRowStripeStyle($dxfs[$this->tableStyle->getSecondRowStripe()]); + } + + return $this; + } + /** * Get this Style's Table. */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index daa1853bba..60bf3b12ab 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1414,6 +1414,32 @@ public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordina return $this->getParentOrThrow()->getCellXfSupervisor(); } + /** + * Get table styles set for the for given cell. + * + * @param Cell $cell + * The Cell for which the tables are retrieved + */ + public function getTablesWithStylesForCell(Cell $cell): array + { + $retVal = []; + + foreach ($this->tableCollection as $table) { + /** @var Table $table */ + $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle(); + if ($dxfsTableStyle !== null) { + if ($dxfsTableStyle->getHeaderRowStyle() !== null || $dxfsTableStyle->getFirstRowStripeStyle() !== null || $dxfsTableStyle->getSecondRowStripeStyle() !== null) { + $range = $table->getRange(); + if ($cell->isInRange($range)) { + $retVal[] = $table; + } + } + } + } + + return $retVal; + } + /** * Get conditional styles for a cell. * @@ -2888,6 +2914,40 @@ public function rangeToArray( return $returnValue; } + /** + * Create array from a multiple ranges of cells. (such as A1:A3,A15,B17:C17). + * + * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist + * @param bool $calculateFormulas Should formulas be calculated? + * @param bool $formatData Should formatting be applied to cell values? + * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero + * True - Return rows and columns indexed by their actual row and column IDs + * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden. + * True - Don't return values for rows/columns that are defined as hidden. + */ + public function rangesToArray( + string $ranges, + mixed $nullValue = null, + bool $calculateFormulas = true, + bool $formatData = true, + bool $returnCellRef = false, + bool $ignoreHidden = false, + bool $reduceArrays = false + ): array { + $returnValue = []; + + $parts = explode(',', $ranges); + foreach ($parts as $part) { + // Loop through rows + foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays) as $rowRef => $rowArray) { + $returnValue[$rowRef] = $rowArray; + } + } + + // Return + return $returnValue; + } + /** * Create array from a range of cells, yielding each row in turn. * diff --git a/src/PhpSpreadsheet/Writer/BaseWriter.php b/src/PhpSpreadsheet/Writer/BaseWriter.php index 5e6d3cd49e..50df5b167a 100644 --- a/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Writer; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; + abstract class BaseWriter implements IWriter { /** @@ -17,6 +19,18 @@ abstract class BaseWriter implements IWriter */ protected bool $preCalculateFormulas = true; + /** + * Table formats + * Enables table formats in writer, disabled here, must be enabled in writer via a setter. + */ + protected bool $tableFormats = false; + + /** + * Conditional Formatting + * Enables conditional formatting in writer, disabled here, must be enabled in writer via a setter. + */ + protected bool $conditionalFormatting = false; + /** * Use disk caching where possible? */ @@ -58,6 +72,34 @@ public function setPreCalculateFormulas(bool $precalculateFormulas): self return $this; } + public function getTableFormats(): bool + { + return $this->tableFormats; + } + + public function setTableFormats(bool $tableFormats): self + { + if ($tableFormats) { + throw new PhpSpreadsheetException('Table formatting not implemented for this writer'); + } + + return $this; + } + + public function getConditionalFormatting(): bool + { + return $this->conditionalFormatting; + } + + public function setConditionalFormatting(bool $conditionalFormatting): self + { + if ($conditionalFormatting) { + throw new PhpSpreadsheetException('Conditional Formatting not implemented for this writer'); + } + + return $this; + } + public function getUseDiskCaching(): bool { return $this->useDiskCaching; diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index bc26c0ff28..4d7bfcafbe 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -4,6 +4,7 @@ use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -22,6 +23,9 @@ use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; +use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\StyleMerger; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; @@ -162,13 +166,10 @@ public function __construct(Spreadsheet $spreadsheet) public function save($filename, int $flags = 0): void { $this->processFlags($flags); - // Open file $this->openFileHandle($filename); - // Write html fwrite($this->fileHandle, $this->generateHTMLAll()); - // Close file $this->maybeCloseFileHandle(); } @@ -473,12 +474,24 @@ public function generateSheetData(): string // Loop all sheets $sheetId = 0; + + $activeSheet = $this->spreadsheet->getActiveSheetIndex(); + foreach ($sheets as $sheet) { + // save active cells + $selectedCells = $sheet->getSelectedCells(); // Write table header $html .= $this->generateTableHeader($sheet); $this->sheetCharts = []; $this->sheetDrawings = []; - + $condStylesCollection = $sheet->getConditionalStylesCollection(); + foreach ($condStylesCollection as $condStyles) { + foreach ($condStyles as $key => $cs) { + if ($cs->getConditionType() === Conditional::CONDITION_COLORSCALE) { + $cs->getColorScale()->setScaleArray(); + } + } + } // Get worksheet dimension [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); [$minCol, $minRow, $minColString] = Coordinate::indexesFromString($min); @@ -486,7 +499,6 @@ public function generateSheetData(): string $this->extendRowsAndColumns($sheet, $maxCol, $maxRow); [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); - // Loop through cells $row = $minRow - 1; while ($row++ < $maxRow) { @@ -514,7 +526,6 @@ public function generateSheetData(): string $html .= $endTag; } - // Write table footer $html .= $this->generateTableFooter(); // Writing PDF? @@ -526,7 +537,9 @@ public function generateSheetData(): string // Next sheet ++$sheetId; + $sheet->setSelectedCells($selectedCells); } + $this->spreadsheet->setActiveSheetIndex($activeSheet); return $html; } @@ -1356,7 +1369,11 @@ private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, stri $cellData .= $this->generateRowCellDataValueRich($cell->getValue()); } else { if ($this->preCalculateFormulas) { - $origData = $cell->getCalculatedValue(); + try { + $origData = $cell->getCalculatedValue(); + } catch (CalculationException $exception) { + $origData = '#ERROR'; // mark as error, rather than crash everything + } if ($this->betterBoolean && is_bool($origData)) { $origData2 = $origData ? $this->getTrue : $this->getFalse; } else { @@ -1473,7 +1490,8 @@ private function generateRowWriteCell( array|string $cssClass, int $colNum, int $sheetIndex, - int $row + int $row, + array $condStyles = [] ): void { // Image? $htmlx = $this->writeImageInCell($coordinate); @@ -1540,8 +1558,57 @@ private function generateRowWriteCell( $html .= ' class="gridlines gridlinesp"'; } } + $html = $this->generateRowSpans($html, $rowSpan, $colSpan); + $tables = $worksheet->getTablesWithStylesForCell($worksheet->getCell($coordinate)); + if (count($tables) > 0 || count($condStyles) > 0) { + $matched = false; // TODO the style gotten from the merger overrides everything + $styleMerger = new StyleMerger($worksheet->getCell($coordinate)->getStyle()); + if ($this->tableFormats) { + if (count($tables) > 0) { + foreach ($tables as $ts) { + $dxfsTableStyle = $ts->getStyle()->getTableDxfsStyle(); + if ($dxfsTableStyle !== null) { + $tableRow = $ts->getRowNumber($coordinate); + if ($tableRow === 0 && $dxfsTableStyle->getHeaderRowStyle() !== null) { + $styleMerger->mergeStyle($dxfsTableStyle->getHeaderRowStyle()); + $matched = true; + } elseif ($tableRow % 2 === 1 && $dxfsTableStyle->getFirstRowStripeStyle() !== null) { + $styleMerger->mergeStyle($dxfsTableStyle->getFirstRowStripeStyle()); + $matched = true; + } elseif ($tableRow % 2 === 0 && $dxfsTableStyle->getSecondRowStripeStyle() !== null) { + $styleMerger->mergeStyle($dxfsTableStyle->getSecondRowStripeStyle()); + $matched = true; + } + } + } + } + } + if (count($condStyles) > 0 && $this->conditionalFormatting) { + if ($worksheet->getConditionalRange($coordinate) !== null) { + $assessor = new CellStyleAssessor($worksheet->getCell($coordinate), $worksheet->getConditionalRange($coordinate)); + } else { + $assessor = new CellStyleAssessor($worksheet->getCell($coordinate), $coordinate); + } + $matchedStyle = $assessor->matchConditionsReturnNullIfNoneMatched($condStyles, $cellData, true); + + if ($matchedStyle !== null) { + $matched = true; + // this is really slow + $styleMerger->mergeStyle($matchedStyle); + } + } + if ($matched) { + $styles = $this->createCSSStyle($styleMerger->getStyle()); + $html .= ' style="'; + foreach ($styles as $key => $value) { + $html .= $key . ':' . $value . ';'; + } + $html .= '"'; + } + } + $html .= '>'; $html .= $htmlx; @@ -1587,6 +1654,9 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri // Cell Data $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass); + // Get an array of all styles + $condStyles = $worksheet->getStyle($coordinate)->getConditionalStyles(); + // Hyperlink? if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) { $url = $worksheet->getHyperlink($coordinate)->getUrl(); @@ -1636,7 +1706,7 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri // Write if ($writeCell) { - $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row); + $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row, $condStyles); } // Next column @@ -1738,6 +1808,20 @@ public function setUseInlineCss(bool $useInlineCss): static return $this; } + public function setTableFormats(bool $tableFormats): self + { + $this->tableFormats = $tableFormats; + + return $this; + } + + public function setConditionalFormatting(bool $conditionalFormatting): self + { + $this->conditionalFormatting = $conditionalFormatting; + + return $this; + } + /** * Add color to formatted string as inline style. * diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php new file mode 100644 index 0000000000..87a9fb6983 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php @@ -0,0 +1,69 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~5<', 'cell E1'], + ['F1', 'background-color:#CBCD71;">6<', 'cell F1'], + ['G1', 'background-color:#E3D16C;">7<', 'cell G1'], + ['D2', 'background-color:#57BB8A;">4<', 'cell D2'], + ['E2', 'background-color:#A1C77A;">5<', 'cell E2'], + ['F2', 'background-color:#F1A36D;">6<', 'cell F2'], + ['D3', 'background-color:#FFD666;">4<', 'cell D3'], + ['G3', 'background-color:#EC926F;">7<', 'cell G3'], + ['H3', 'background-color:#E67C73;">8<', 'cell H3'], + ['A4', 'background-color:#57BB8A;">1<', 'cell A4'], + ['I4', 'null"><', 'empty cell I4'], + ['J4', 'background-color:#E67C73;">10<', 'cell J4'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php new file mode 100644 index 0000000000..c5a31e415c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php @@ -0,0 +1,65 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~Jan<', 'no conditional styling for B1'], + ['F2', 'background-color:#C6EFCE;">120<', 'conditional style for F2'], + ['H2', 'background-color:#FFEB9C;">90<', 'conditional style for H2'], + ['F3', 'background-color:#C6EFCE;">70<', 'conditional style for cell F3'], + ['H3', 'background-color:#FFEB9C;">60<', 'conditional style for cell H3'], + ['F4', 'background-color:#C6EFCE;">1<', 'conditional style for cell F4'], + ['L4', 'background-color:#FFC7CE;">5<', 'conditional style for cell L4'], + ['F5', 'class="column5 style1 n">0<', 'no conditional styling for F5'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php new file mode 100644 index 0000000000..4bdb886210 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php @@ -0,0 +1,94 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~1<', 'A1 equals hit'], + ['B1', 'class="column1 style1 n">2<', 'B1 equals miss'], + ['E1', 'background-color:#B7E1CD;">1<', 'E1 equals horizontal reference hit'], + ['F1', 'class="column5 style1 n">2<', 'F1 equals horizontal reference miss'], + ['G1', 'class="column6 style1 n">3<', 'G1 equals horizontal reference miss'], + ['A2', 'background-color:#B7E1CD;">terve<', 'A2 text contains hit'], + ['B2', 'class="column1 style1 s">moi<', 'B2 text contains miss'], + ['A3', 'background-color:#B7E1CD;">terve<', 'A3 text does not contain hit'], + ['B3', 'class="column1 style1 s">moi<', 'B2 text does not contain miss'], + ['A4', 'background-color:#B7E1CD;">terve<', 'A4 text starts with hit'], + ['B4', 'class="column1 style1 s">moi<', 'B2 text starts with miss'], + ['A5', 'background-color:#B7E1CD;">terve<', 'A5 text ends with hit'], + ['B5', 'class="column1 style1 s">moi<', 'B5 text ends with miss'], + ['A6', 'background-color:#B7E1CD;">2025/01/01<', 'A6 date after hit'], + ['B6', 'class="column1 style2 n">2020/01/01<', 'B6 date after miss'], + ['A7', 'background-color:#B7E1CD;">terve vaan<', 'A7 text contains hit'], + ['B7', 'class="column1 style1 s">moi<', 'B7 text contains miss'], + ['A8', 'background-color:#B7E1CD;">terve<', 'A8 text does not contain hit'], + ['B8', 'class="column1 style1 s">terve vaan<', 'B2 does not contain miss'], + ['A9', 'background-color:#B7E1CD;">#DIV/0!<', 'A10 own formula is error hit'], + ['B9', 'class="column1 style1 s">moi<', 'B9 own formula is error miss'], + ['A10', 'background-color:#B7E1CD;">moi<', 'A10 own formula is not error hit'], + ['B10', 'class="column1 style3 s">#DIV/0!<', 'B10 own formula is not error miss'], + ['A11', 'background-color:#B7E1CD;">terve<', 'A11 own formula count instances of cell on line and hit when more than one hit'], + ['B11', 'background-color:#B7E1CD;">terve<', 'B11 own formula count instances of cell on line and hit when more than one hit'], + ['C11', 'class="column2 style1 s">moi<', 'C11 own formula count instances of cell on line and hit when more than one miss'], + ['A12', 'background-color:#B7E1CD;">moi<', 'A12 own formula count instances of cell on line and hit when at most 1 hit'], + ['B12', 'class="column1 style1 s">terve<', 'B12 own formula count instances of cell on line and hit when at most 1 miss'], + ['C12', 'class="column2 style1 s">terve<', 'C11 own formula count instances of cell on line and hit when at most 1 miss'], + ['A13', 'background-color:#B7E1CD;">12<', 'A13 own formula self reference hit'], + ['B13', 'class="column1 style1 n">10<', 'B13 own formula self reference miss'], + ['A14', 'background-color:#B7E1CD;">10<', 'A14 multiple conditional hits'], + ['B14', 'class="column1 style1 n">1<', 'B14 multiple conditionals miss'], + ['F7', 'background-color:#B7E1CD;">1<', 'F7 equals vertical reference hit'], + ['F8', 'class="column5 style1 n">2<', 'F8 equals vertical reference miss'], + ['F9', 'class="column5 style1 n">3<', 'F9 equals vertical reference miss'], + ['F10', 'class="column5 style1 n">4<', 'F10 equals vertical reference miss'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatTest.php new file mode 100644 index 0000000000..c78dda2591 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatTest.php @@ -0,0 +1,64 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setTableFormats(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~Sep<', 'table style for header row cell J1'], + ['J2', 'background-color:#C0E4F5;">110<', 'table style for cell J2'], + ['I3', 'background-color:#82CAEB;">70<', 'table style for cell I3'], + ['J3', 'background-color:#82CAEB;">70<', 'table style for cell J3'], // as conditional calculations are off + ['K3', 'background-color:#82CAEB;">70<', 'table style for cell K3'], + ['J4', 'background-color:#C0E4F5;">1<', 'table style for cell J4'], + ['J5', 'background-color:#82CAEB;">1<', 'table style for cell J5'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatWithConditionalTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatWithConditionalTest.php new file mode 100644 index 0000000000..1c8a16d27e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatWithConditionalTest.php @@ -0,0 +1,65 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setTableFormats(true); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~Sep<', 'table style for header row cell J1'], + ['J2', 'background-color:#C0E4F5;">110<', 'table style for cell J2'], + ['I3', 'background-color:#82CAEB;">70<', 'table style for cell I3'], + ['J3', 'background-color:#B7E1CD;">70<', 'conditional style for cell J3'], // as conditional calculations are on + ['K3', 'background-color:#82CAEB;">70<', 'table style for cell K3'], + ['J4', 'background-color:#C0E4F5;">1<', 'table style for cell J4'], + ['J5', 'background-color:#82CAEB;">1<', 'table style for cell J5'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +}