Skip to content

Conditional and table formatting support for html #4412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 27, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

37 changes: 22 additions & 15 deletions docs/topics/conditional-formatting.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions docs/topics/tables.md
Original file line number Diff line number Diff line change
@@ -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);
```
25 changes: 25 additions & 0 deletions samples/Html/html_01_Basic_Conditional_Formatting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;

require __DIR__ . '/../Header.php';

$inputFileName = 'BasicConditionalFormatting.xlsx';
$inputFilePath = __DIR__ . '/../templates/' . $inputFileName;

$codePath = $helper->isCli() ? ('samples/templates/' . $inputFileName) : ('<code>' . 'samples/templates/' . $inputFileName . '</code>');
$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(...));
25 changes: 25 additions & 0 deletions samples/Html/html_02_More_Conditional_Formatting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;

require __DIR__ . '/../Header.php';

$inputFileName = 'ConditionalFormattingConditions.xlsx';
$inputFilePath = __DIR__ . '/../templates/' . $inputFileName;

$codePath = $helper->isCli() ? ('samples/templates/' . $inputFileName) : ('<code>' . 'samples/templates/' . $inputFileName . '</code>');
$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(...));
25 changes: 25 additions & 0 deletions samples/Html/html_03_Color_Scale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;

require __DIR__ . '/../Header.php';

$inputFileName = 'ColourScale.xlsx';
$inputFilePath = __DIR__ . '/../templates/' . $inputFileName;

$codePath = $helper->isCli() ? ('samples/templates/' . $inputFileName) : ('<code>' . 'samples/templates/' . $inputFileName . '</code>');
$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(...));
25 changes: 25 additions & 0 deletions samples/Html/html_04_Table_Format_without_Conditional.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;

require __DIR__ . '/../Header.php';

$inputFileName = 'TableFormat.xlsx';
$inputFilePath = __DIR__ . '/../templates/' . $inputFileName;

$codePath = $helper->isCli() ? ('samples/templates/' . $inputFileName) : ('<code>' . 'samples/templates/' . $inputFileName . '</code>');
$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(...));
27 changes: 27 additions & 0 deletions samples/Html/html_05_Table_Format_with_Conditional.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;

require __DIR__ . '/../Header.php';

$inputFileName = 'TableFormat.xlsx';
$inputFilePath = __DIR__ . '/../templates/' . $inputFileName;

$codePath = $helper->isCli() ? ('samples/templates/' . $inputFileName) : ('<code>' . 'samples/templates/' . $inputFileName . '</code>');
$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(...));
Binary file added samples/templates/BasicConditionalFormatting.xlsx
Binary file not shown.
Binary file added samples/templates/ColourScale.xlsx
Binary file not shown.
Binary file not shown.
Binary file added samples/templates/TableFormat.xlsx
Binary file not shown.
15 changes: 10 additions & 5 deletions src/PhpSpreadsheet/Reader/Xlsx.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
7 changes: 7 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
41 changes: 41 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx/Styles.php
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 11 additions & 5 deletions src/PhpSpreadsheet/Reader/Xlsx/TableReader.php
Original file line number Diff line number Diff line change
@@ -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);
}
Loading