Skip to content

Commit a6e9926

Browse files
committed
Xls Conditional Border
Xls Writer Conditional Border had been creating corrupt spreadsheets. This was mainly because a pack statement that should have specified `V` instead specified `v`. Even changing that, the logic was still slightly wrong on write, and missing altogether on read. This PR corrects the write problems and adds the missing read code. It also adds italic and strikethrough support for Xls Writer Conditional Font italic and strikethrough (read code was already in place). With this, Xls Conditional Writer is completely supported except for NumberFormat. Xls does support that, but I cannot figure out how from the available documentation.
1 parent 2ed696f commit a6e9926

File tree

5 files changed

+344
-35
lines changed

5 files changed

+344
-35
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1717

1818
### Deprecated
1919

20-
- Nothing
20+
- Writer\Xls\Style\ColorMap is no longer needed.
2121

2222
### Moved
2323

@@ -28,7 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2828
- Incorrect Reader CSV with BOM. [Issue #4028](https://github.com/PHPOffice/PhpSpreadsheet/issues/4028) [PR #4029](https://github.com/PHPOffice/PhpSpreadsheet/pull/4029)
2929
- POWER Null/Bool Args. [PR #4031](https://github.com/PHPOffice/PhpSpreadsheet/pull/4031)
3030
- Do Not Output Alignment and Protection for Conditional Format. [Issue #4025](https://github.com/PHPOffice/PhpSpreadsheet/issues/4025) [PR #4027](https://github.com/PHPOffice/PhpSpreadsheet/pull/4027)
31-
- Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030)
31+
- Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033)
3232

3333
## 2024-05-11 - 2.1.0
3434

src/PhpSpreadsheet/Reader/Xls.php

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls;
2424
use PhpOffice\PhpSpreadsheet\Spreadsheet;
2525
use PhpOffice\PhpSpreadsheet\Style\Alignment;
26+
use PhpOffice\PhpSpreadsheet\Style\Border;
2627
use PhpOffice\PhpSpreadsheet\Style\Borders;
2728
use PhpOffice\PhpSpreadsheet\Style\Conditional;
2829
use PhpOffice\PhpSpreadsheet\Style\Fill;
@@ -163,6 +164,26 @@ class Xls extends BaseReader
163164
// Size of stream blocks when using RC4 encryption
164165
const REKEY_BLOCK = 0x400;
165166

167+
// should be consistent with Writer\Xls\Style\CellBorder
168+
const BORDER_STYLE_MAP = [
169+
Border::BORDER_NONE, // => 0x00,
170+
Border::BORDER_THIN, // => 0x01,
171+
Border::BORDER_MEDIUM, // => 0x02,
172+
Border::BORDER_DASHED, // => 0x03,
173+
Border::BORDER_DOTTED, // => 0x04,
174+
Border::BORDER_THICK, // => 0x05,
175+
Border::BORDER_DOUBLE, // => 0x06,
176+
Border::BORDER_HAIR, // => 0x07,
177+
Border::BORDER_MEDIUMDASHED, // => 0x08,
178+
Border::BORDER_DASHDOT, // => 0x09,
179+
Border::BORDER_MEDIUMDASHDOT, // => 0x0A,
180+
Border::BORDER_DASHDOTDOT, // => 0x0B,
181+
Border::BORDER_MEDIUMDASHDOTDOT, // => 0x0C,
182+
Border::BORDER_SLANTDASHDOT, // => 0x0D,
183+
Border::BORDER_OMIT, // => 0x0E,
184+
Border::BORDER_OMIT, // => 0x0F,
185+
];
186+
166187
/**
167188
* Summary Information stream data.
168189
*/
@@ -1945,12 +1966,9 @@ private function readFont(): void
19451966
$objFont->colorIndex = $colorIndex;
19461967

19471968
// offset: 6; size: 2; font weight
1948-
$weight = self::getUInt2d($recordData, 6);
1949-
switch ($weight) {
1950-
case 0x02BC:
1951-
$objFont->setBold(true);
1952-
1953-
break;
1969+
$weight = self::getUInt2d($recordData, 6); // regular=400 bold=700
1970+
if ($weight >= 550) {
1971+
$objFont->setBold(true);
19541972
}
19551973

19561974
// offset: 8; size: 2; escapement type
@@ -7326,6 +7344,11 @@ public function getMapCellStyleXfIndex(): array
73267344
return $this->mapCellStyleXfIndex;
73277345
}
73287346

7347+
/**
7348+
* Parse conditional formatting blocks.
7349+
*
7350+
* @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
7351+
*/
73297352
private function readCFHeader(): array
73307353
{
73317354
$length = self::getUInt2d($this->data, $this->pos + 2);
@@ -7394,6 +7417,11 @@ private function readCFRule(array $cellRangeAddresses): void
73947417
$hasBorderRecord = (bool) ((0x10000000 & $options) >> 28);
73957418
$hasFillRecord = (bool) ((0x20000000 & $options) >> 29);
73967419
$hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30);
7420+
// note unexpected values for following 4
7421+
$hasBorderLeft = !(bool) (0x00000400 & $options);
7422+
$hasBorderRight = !(bool) (0x00000800 & $options);
7423+
$hasBorderTop = !(bool) (0x00001000 & $options);
7424+
$hasBorderBottom = !(bool) (0x00002000 & $options);
73977425

73987426
$offset = 12;
73997427

@@ -7410,8 +7438,8 @@ private function readCFRule(array $cellRangeAddresses): void
74107438
}
74117439

74127440
if ($hasBorderRecord === true) {
7413-
//$borderStyle = substr($recordData, $offset, 8);
7414-
//$this->getCFBorderStyle($borderStyle, $style);
7441+
$borderStyle = substr($recordData, $offset, 8);
7442+
$this->getCFBorderStyle($borderStyle, $style, $hasBorderLeft, $hasBorderRight, $hasBorderTop, $hasBorderBottom);
74157443
$offset += 8;
74167444
}
74177445

@@ -7459,9 +7487,23 @@ private function getCFFontStyle(string $options, Style $style): void
74597487
if ($fontSize !== -1) {
74607488
$style->getFont()->setSize($fontSize / 20); // Convert twips to points
74617489
}
7490+
$options68 = self::getInt4d($options, 68);
7491+
$options88 = self::getInt4d($options, 88);
74627492

7463-
$bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold
7464-
$style->getFont()->setBold($bold);
7493+
if (($options88 & 2) === 0) {
7494+
$bold = self::getUInt2d($options, 72); // 400 = normal, 700 = bold
7495+
if ($bold !== 0) {
7496+
$style->getFont()->setBold($bold >= 550);
7497+
}
7498+
if (($options68 & 2) !== 0) {
7499+
$style->getFont()->setItalic(true);
7500+
}
7501+
}
7502+
if (($options88 & 0x80) === 0) {
7503+
if (($options68 & 0x80) !== 0) {
7504+
$style->getFont()->setStrikethrough(true);
7505+
}
7506+
}
74657507

74667508
$color = self::getInt4d($options, 80);
74677509

@@ -7474,9 +7516,45 @@ private function getCFFontStyle(string $options, Style $style): void
74747516
{
74757517
}*/
74767518

7477-
/*private function getCFBorderStyle(string $options, Style $style): void
7519+
private function getCFBorderStyle(string $options, Style $style, bool $hasBorderLeft, bool $hasBorderRight, bool $hasBorderTop, bool $hasBorderBottom): void
74787520
{
7479-
}*/
7521+
$valueArray = unpack('V', $options);
7522+
$value = is_array($valueArray) ? $valueArray[1] : 0;
7523+
$left = $value & 15;
7524+
$right = ($value >> 4) & 15;
7525+
$top = ($value >> 8) & 15;
7526+
$bottom = ($value >> 12) & 15;
7527+
$leftc = ($value >> 16) & 0x7F;
7528+
$rightc = ($value >> 23) & 0x7F;
7529+
$valueArray = unpack('V', substr($options, 4));
7530+
$value = is_array($valueArray) ? $valueArray[1] : 0;
7531+
$topc = $value & 0x7F;
7532+
$bottomc = ($value & 0x3F80) >> 7;
7533+
if ($hasBorderLeft) {
7534+
$style->getBorders()->getLeft()
7535+
->setBorderStyle(self::BORDER_STYLE_MAP[$left]);
7536+
$style->getBorders()->getLeft()->getColor()
7537+
->setRGB(Xls\Color::map($leftc, $this->palette, $this->version)['rgb']);
7538+
}
7539+
if ($hasBorderRight) {
7540+
$style->getBorders()->getRight()
7541+
->setBorderStyle(self::BORDER_STYLE_MAP[$right]);
7542+
$style->getBorders()->getRight()->getColor()
7543+
->setRGB(Xls\Color::map($rightc, $this->palette, $this->version)['rgb']);
7544+
}
7545+
if ($hasBorderTop) {
7546+
$style->getBorders()->getTop()
7547+
->setBorderStyle(self::BORDER_STYLE_MAP[$top]);
7548+
$style->getBorders()->getTop()->getColor()
7549+
->setRGB(Xls\Color::map($topc, $this->palette, $this->version)['rgb']);
7550+
}
7551+
if ($hasBorderBottom) {
7552+
$style->getBorders()->getBottom()
7553+
->setBorderStyle(self::BORDER_STYLE_MAP[$bottom]);
7554+
$style->getBorders()->getBottom()->getColor()
7555+
->setRGB(Xls\Color::map($bottomc, $this->palette, $this->version)['rgb']);
7556+
}
7557+
}
74807558

74817559
private function getCFFillStyle(string $options, Style $style): void
74827560
{

src/PhpSpreadsheet/Writer/Xls/Worksheet.php

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1313
use PhpOffice\PhpSpreadsheet\Shared\Xls;
1414
use PhpOffice\PhpSpreadsheet\Style\Border;
15+
use PhpOffice\PhpSpreadsheet\Style\Borders;
1516
use PhpOffice\PhpSpreadsheet\Style\Conditional;
1617
use PhpOffice\PhpSpreadsheet\Style\Protection;
1718
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
@@ -2722,6 +2723,8 @@ private function writePageLayoutView(): void
27222723

27232724
/**
27242725
* Write CFRule Record.
2726+
*
2727+
* @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
27252728
*/
27262729
private function writeCFRule(
27272730
ConditionalHelper $conditionalFormulaHelper,
@@ -2824,7 +2827,13 @@ private function writeCFRule(
28242827
$bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
28252828
$bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
28262829
$bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
2827-
if ($bBorderLeft === 1 || $bBorderRight === 1 || $bBorderTop === 1 || $bBorderBottom === 1) {
2830+
$bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
2831+
$diagonalDirection = $conditional->getStyle()->getBorders()->getDiagonalDirection();
2832+
// Excel does not support conditional diagonal border even for xlsx
2833+
$bBorderDiagTop = self::$always0; //$diagonalDirection === Borders::DIAGONAL_DOWN || $diagonalDirection === Borders::DIAGONAL_BOTH;
2834+
$bBorderDiagBottom = self::$always0; //$diagonalDirection === Borders::DIAGONAL_UP || $diagonalDirection === Borders::DIAGONAL_BOTH;
2835+
2836+
if ($bBorderLeft === 1 || $bBorderRight === 1 || $bBorderTop === 1 || $bBorderBottom === 1 || $bBorderDiagTop === 1 || $bBorderDiagBottom === 1) {
28282837
$bFormatBorder = 1;
28292838
} else {
28302839
$bFormatBorder = 0;
@@ -2869,13 +2878,13 @@ private function writeCFRule(
28692878
// Protection
28702879
//$flags |= (1 == $bProtLocked ? 0x00000100 : 0);
28712880
//$flags |= (1 == $bProtHidden ? 0x00000200 : 0);
2872-
// Border
2873-
$flags |= (1 == $bBorderLeft ? 0x00000400 : 0);
2874-
$flags |= (1 == $bBorderRight ? 0x00000800 : 0);
2875-
$flags |= (1 == $bBorderTop ? 0x00001000 : 0);
2876-
$flags |= (1 == $bBorderBottom ? 0x00002000 : 0);
2877-
$flags |= (1 == self::$always1 ? 0x00004000 : 0); // Top left to Bottom right border
2878-
$flags |= (1 == self::$always1 ? 0x00008000 : 0); // Bottom left to Top right border
2881+
// Border, note that flags are opposite of what you might expect
2882+
$flags |= (0 == $bBorderLeft ? 0x00000400 : 0);
2883+
$flags |= (0 == $bBorderRight ? 0x00000800 : 0);
2884+
$flags |= (0 == $bBorderTop ? 0x00001000 : 0);
2885+
$flags |= (0 == $bBorderBottom ? 0x00002000 : 0);
2886+
$flags |= (0 === $bBorderDiagTop ? 0x00004000 : 0); // Top left to Bottom right border
2887+
$flags |= (0 === $bBorderDiagBottom ? 0x00008000 : 0); // Bottom left to Top right border
28792888
// Pattern
28802889
$flags |= (1 == $bFillStyle ? 0x00010000 : 0);
28812890
$flags |= (1 == $bFillColor ? 0x00020000 : 0);
@@ -2915,10 +2924,19 @@ private function writeCFRule(
29152924
$dataBlockFont .= pack('V', 20 * $conditional->getStyle()->getFont()->getSize());
29162925
}
29172926
// Font Options
2918-
$dataBlockFont .= pack('V', 0);
2927+
$italicStrike = 0;
2928+
if ($conditional->getStyle()->getFont()->getItalic() === true) {
2929+
$italicStrike |= 2;
2930+
}
2931+
if ($conditional->getStyle()->getFont()->getStrikethrough() === true) {
2932+
$italicStrike |= 0x80;
2933+
}
2934+
$dataBlockFont .= pack('V', $italicStrike);
29192935
// Font weight
29202936
if ($conditional->getStyle()->getFont()->getBold() === true) {
29212937
$dataBlockFont .= pack('v', 0x02BC);
2938+
} elseif ($conditional->getStyle()->getFont()->getBold() === null) {
2939+
$dataBlockFont .= pack('v', 0x0000);
29222940
} else {
29232941
$dataBlockFont .= pack('v', 0x0190);
29242942
}
@@ -2975,12 +2993,11 @@ private function writeCFRule(
29752993
$dataBlockFont .= pack('V', 0x00000000);
29762994
// Options flags for modified font attributes
29772995
$optionsFlags = 0;
2978-
$optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() === null ? 1 : 0);
2979-
$optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0);
2996+
$optionsFlags |= ($conditional->getStyle()->getFont()->getBold() === null && $conditional->getStyle()->getFont()->getItalic() === null) ? 2 : 0;
29802997
$optionsFlags |= (1 == self::$always1 ? 0x00000008 : 0);
29812998
$optionsFlags |= (1 == self::$always1 ? 0x00000010 : 0);
29822999
$optionsFlags |= (1 == self::$always0 ? 0x00000020 : 0);
2983-
$optionsFlags |= (1 == self::$always1 ? 0x00000080 : 0);
3000+
$optionsFlags |= ($conditional->getStyle()->getFont()->getStrikethrough() === null) ? 0x80 : 0;
29843001
$dataBlockFont .= pack('V', $optionsFlags);
29853002
// Escapement type
29863003
$dataBlockFont .= pack('V', $fontEscapement);
@@ -3025,16 +3042,37 @@ private function writeCFRule(
30253042
$blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getTop()) << 8;
30263043
$blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getBottom()) << 12;
30273044

3028-
// TODO writeCFRule() => $blockLineStyle => Index Color for left line
3029-
// TODO writeCFRule() => $blockLineStyle => Index Color for right line
3030-
// TODO writeCFRule() => $blockLineStyle => Top-left to bottom-right on/off
3031-
// TODO writeCFRule() => $blockLineStyle => Bottom-left to top-right on/off
3045+
if ($bBorderLeft !== 0) {
3046+
$colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getLeft()->getColor()->getRgb(), 0);
3047+
$blockLineStyle |= $colorIdx << 16;
3048+
}
3049+
if ($bBorderRight !== 0) {
3050+
$colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getRight()->getColor()->getRgb(), 0);
3051+
$blockLineStyle |= $colorIdx << 23;
3052+
}
30323053
$blockColor = 0;
3033-
// TODO writeCFRule() => $blockColor => Index Color for top line
3034-
// TODO writeCFRule() => $blockColor => Index Color for bottom line
3035-
// TODO writeCFRule() => $blockColor => Index Color for diagonal line
3036-
$blockColor |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getDiagonal()) << 21;
3037-
$dataBlockBorder = pack('vv', $blockLineStyle, $blockColor);
3054+
if ($bBorderTop !== 0) {
3055+
$colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getTop()->getColor()->getRgb(), 0);
3056+
$blockColor |= $colorIdx;
3057+
}
3058+
if ($bBorderBottom !== 0) {
3059+
$colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getBottom()->getColor()->getRgb(), 0);
3060+
$blockColor |= $colorIdx << 7;
3061+
}
3062+
/* Excel does not support condtional diagonal borders even for xlsx
3063+
if ($bBorderDiagTop !== 0 || $bBorderDiagBottom !== 0) {
3064+
$colorIdx = $this->workbookColorIndex($conditional->getStyle()->getBorders()->getDiagonal()->getColor()->getRgb(), 0);
3065+
$blockColor |= $colorIdx << 14;
3066+
$blockColor |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getDiagonal()) << 21;
3067+
if ($bBorderDiagTop !== 0) {
3068+
$blockLineStyle |= 1 << 30;
3069+
}
3070+
if ($bBorderDiagBottom !== 0) {
3071+
$blockLineStyle |= 1 << 31;
3072+
}
3073+
}
3074+
*/
3075+
$dataBlockBorder = pack('VV', $blockLineStyle, $blockColor);
30383076
}
30393077
if ($bFormatFill === 1) {
30403078
// Fill Pattern Style

0 commit comments

Comments
 (0)