,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);
+ }
+ }
+}
| |