Skip to content

Commit be734cf

Browse files
authored
Merge pull request #2696 from PHPOffice/Xls-Reader-Conditional-Formatting
Xls Reader: initial work on reading conditional formatting Currently the ranges and CF Rules are read, but only the most basic style information for the font (size, weight and colour) This will be expanded with future PRs
2 parents 1bd5369 + 23e2d70 commit be734cf

File tree

7 files changed

+500
-0
lines changed

7 files changed

+500
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1818
This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators.
1919

2020
- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532)
21+
- Limited support for Xls Reader to handle Conditional Formatting:
22+
23+
Ranges and Rules are read, but style is currently limited to font size, weight and color.
2124

2225
### Changed
2326

src/PhpSpreadsheet/Reader/Xls.php

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
88
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
99
use PhpOffice\PhpSpreadsheet\NamedRange;
10+
use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting;
1011
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont;
1112
use PhpOffice\PhpSpreadsheet\RichText\RichText;
1213
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
@@ -21,6 +22,7 @@
2122
use PhpOffice\PhpSpreadsheet\Spreadsheet;
2223
use PhpOffice\PhpSpreadsheet\Style\Alignment;
2324
use PhpOffice\PhpSpreadsheet\Style\Borders;
25+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
2426
use PhpOffice\PhpSpreadsheet\Style\Font;
2527
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
2628
use PhpOffice\PhpSpreadsheet\Style\Protection;
@@ -142,6 +144,8 @@ class Xls extends BaseReader
142144
const XLS_TYPE_SHEETLAYOUT = 0x0862;
143145
const XLS_TYPE_XFEXT = 0x087d;
144146
const XLS_TYPE_PAGELAYOUTVIEW = 0x088b;
147+
const XLS_TYPE_CFHEADER = 0x01b0;
148+
const XLS_TYPE_CFRULE = 0x01b1;
145149
const XLS_TYPE_UNKNOWN = 0xffff;
146150

147151
// Encryption type
@@ -1031,6 +1035,14 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
10311035
case self::XLS_TYPE_DATAVALIDATION:
10321036
$this->readDataValidation();
10331037

1038+
break;
1039+
case self::XLS_TYPE_CFHEADER:
1040+
$cellRangeAddresses = $this->readCFHeader();
1041+
1042+
break;
1043+
case self::XLS_TYPE_CFRULE:
1044+
$this->readCFRule($cellRangeAddresses ?? []);
1045+
10341046
break;
10351047
case self::XLS_TYPE_SHEETLAYOUT:
10361048
$this->readSheetLayout();
@@ -7846,4 +7858,211 @@ public function getMapCellStyleXfIndex(): array
78467858
{
78477859
return $this->mapCellStyleXfIndex;
78487860
}
7861+
7862+
private function readCFHeader(): array
7863+
{
7864+
$length = self::getUInt2d($this->data, $this->pos + 2);
7865+
$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7866+
7867+
// move stream pointer forward to next record
7868+
$this->pos += 4 + $length;
7869+
7870+
if ($this->readDataOnly) {
7871+
return [];
7872+
}
7873+
7874+
// offset: 0; size: 2; Rule Count
7875+
// $ruleCount = self::getUInt2d($recordData, 0);
7876+
7877+
// offset: var; size: var; cell range address list with
7878+
$cellRangeAddressList = ($this->version == self::XLS_BIFF8)
7879+
? $this->readBIFF8CellRangeAddressList(substr($recordData, 12))
7880+
: $this->readBIFF5CellRangeAddressList(substr($recordData, 12));
7881+
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
7882+
7883+
return $cellRangeAddresses;
7884+
}
7885+
7886+
private function readCFRule(array $cellRangeAddresses): void
7887+
{
7888+
$length = self::getUInt2d($this->data, $this->pos + 2);
7889+
$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7890+
7891+
// move stream pointer forward to next record
7892+
$this->pos += 4 + $length;
7893+
7894+
if ($this->readDataOnly) {
7895+
return;
7896+
}
7897+
7898+
// offset: 0; size: 2; Options
7899+
$cfRule = self::getUInt2d($recordData, 0);
7900+
7901+
// bit: 8-15; mask: 0x00FF; type
7902+
$type = (0x00FF & $cfRule) >> 0;
7903+
$type = ConditionalFormatting::type($type);
7904+
7905+
// bit: 0-7; mask: 0xFF00; type
7906+
$operator = (0xFF00 & $cfRule) >> 8;
7907+
$operator = ConditionalFormatting::operator($operator);
7908+
7909+
if ($type === null || $operator === null) {
7910+
return;
7911+
}
7912+
7913+
// offset: 2; size: 2; Size1
7914+
$size1 = self::getUInt2d($recordData, 2);
7915+
7916+
// offset: 4; size: 2; Size2
7917+
$size2 = self::getUInt2d($recordData, 4);
7918+
7919+
// offset: 6; size: 4; Options
7920+
$options = self::getInt4d($recordData, 6);
7921+
7922+
$style = new Style();
7923+
$this->getCFStyleOptions($options, $style);
7924+
7925+
$hasFontRecord = (bool) ((0x04000000 & $options) >> 26);
7926+
$hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27);
7927+
$hasBorderRecord = (bool) ((0x10000000 & $options) >> 28);
7928+
$hasFillRecord = (bool) ((0x20000000 & $options) >> 29);
7929+
$hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30);
7930+
7931+
$offset = 12;
7932+
7933+
if ($hasFontRecord === true) {
7934+
$fontStyle = substr($recordData, $offset, 118);
7935+
$this->getCFFontStyle($fontStyle, $style);
7936+
$offset += 118;
7937+
}
7938+
7939+
if ($hasAlignmentRecord === true) {
7940+
$alignmentStyle = substr($recordData, $offset, 8);
7941+
$this->getCFAlignmentStyle($alignmentStyle, $style);
7942+
$offset += 8;
7943+
}
7944+
7945+
if ($hasBorderRecord === true) {
7946+
$borderStyle = substr($recordData, $offset, 8);
7947+
$this->getCFBorderStyle($borderStyle, $style);
7948+
$offset += 8;
7949+
}
7950+
7951+
if ($hasFillRecord === true) {
7952+
$fillStyle = substr($recordData, $offset, 4);
7953+
$this->getCFFillStyle($fillStyle, $style);
7954+
$offset += 4;
7955+
}
7956+
7957+
if ($hasProtectionRecord === true) {
7958+
$protectionStyle = substr($recordData, $offset, 4);
7959+
$this->getCFProtectionStyle($protectionStyle, $style);
7960+
$offset += 2;
7961+
}
7962+
7963+
$formula1 = $formula2 = null;
7964+
if ($size1 > 0) {
7965+
$formula1 = $this->readCFFormula($recordData, $offset, $size1);
7966+
if ($formula1 === null) {
7967+
return;
7968+
}
7969+
7970+
$offset += $size1;
7971+
}
7972+
7973+
if ($size2 > 0) {
7974+
$formula2 = $this->readCFFormula($recordData, $offset, $size2);
7975+
if ($formula2 === null) {
7976+
return;
7977+
}
7978+
7979+
$offset += $size2;
7980+
}
7981+
7982+
$this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style);
7983+
}
7984+
7985+
private function getCFStyleOptions(int $options, Style $style): void
7986+
{
7987+
}
7988+
7989+
private function getCFFontStyle(string $options, Style $style): void
7990+
{
7991+
$fontSize = self::getInt4d($options, 64);
7992+
if ($fontSize !== -1) {
7993+
$style->getFont()->setSize($fontSize / 20); // Convert twips to points
7994+
}
7995+
7996+
$bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold
7997+
$style->getFont()->setBold($bold);
7998+
7999+
$color = self::getInt4d($options, 80);
8000+
8001+
if ($color !== -1) {
8002+
$style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']);
8003+
}
8004+
}
8005+
8006+
private function getCFAlignmentStyle(string $options, Style $style): void
8007+
{
8008+
}
8009+
8010+
private function getCFBorderStyle(string $options, Style $style): void
8011+
{
8012+
}
8013+
8014+
private function getCFFillStyle(string $options, Style $style): void
8015+
{
8016+
}
8017+
8018+
private function getCFProtectionStyle(string $options, Style $style): void
8019+
{
8020+
}
8021+
8022+
/**
8023+
* @return null|float|int|string
8024+
*/
8025+
private function readCFFormula(string $recordData, int $offset, int $size)
8026+
{
8027+
try {
8028+
$formula = substr($recordData, $offset, $size);
8029+
$formula = pack('v', $size) . $formula; // prepend the length
8030+
8031+
$formula = $this->getFormulaFromStructure($formula);
8032+
if (is_numeric($formula)) {
8033+
return (strpos($formula, '.') !== false) ? (float) $formula : (int) $formula;
8034+
}
8035+
8036+
return $formula;
8037+
} catch (PhpSpreadsheetException $e) {
8038+
}
8039+
8040+
return null;
8041+
}
8042+
8043+
/**
8044+
* @param null|float|int|string $formula1
8045+
* @param null|float|int|string $formula2
8046+
*/
8047+
private function setCFRules(array $cellRanges, string $type, string $operator, $formula1, $formula2, Style $style): void
8048+
{
8049+
foreach ($cellRanges as $cellRange) {
8050+
$conditional = new Conditional();
8051+
$conditional->setConditionType($type);
8052+
$conditional->setOperatorType($operator);
8053+
if ($formula1 !== null) {
8054+
$conditional->addCondition($formula1);
8055+
}
8056+
if ($formula2 !== null) {
8057+
$conditional->addCondition($formula2);
8058+
}
8059+
$conditional->setStyle($style);
8060+
8061+
$conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles();
8062+
$conditionalStyles[] = $conditional;
8063+
8064+
$this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles);
8065+
$this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles);
8066+
}
8067+
}
78498068
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
4+
5+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
6+
7+
class ConditionalFormatting
8+
{
9+
/**
10+
* @var array<int, string>
11+
*/
12+
private static $types = [
13+
0x01 => Conditional::CONDITION_CELLIS,
14+
0x02 => Conditional::CONDITION_EXPRESSION,
15+
];
16+
17+
/**
18+
* @var array<int, string>
19+
*/
20+
private static $operators = [
21+
0x00 => Conditional::OPERATOR_NONE,
22+
0x01 => Conditional::OPERATOR_BETWEEN,
23+
0x02 => Conditional::OPERATOR_NOTBETWEEN,
24+
0x03 => Conditional::OPERATOR_EQUAL,
25+
0x04 => Conditional::OPERATOR_NOTEQUAL,
26+
0x05 => Conditional::OPERATOR_GREATERTHAN,
27+
0x06 => Conditional::OPERATOR_LESSTHAN,
28+
0x07 => Conditional::OPERATOR_GREATERTHANOREQUAL,
29+
0x08 => Conditional::OPERATOR_LESSTHANOREQUAL,
30+
];
31+
32+
public static function type(int $type): ?string
33+
{
34+
if (isset(self::$types[$type])) {
35+
return self::$types[$type];
36+
}
37+
38+
return null;
39+
}
40+
41+
public static function operator(int $operator): ?string
42+
{
43+
if (isset(self::$operators[$operator])) {
44+
return self::$operators[$operator];
45+
}
46+
47+
return null;
48+
}
49+
}

0 commit comments

Comments
 (0)