Skip to content

Commit 9cf526a

Browse files
authored
Reading Xlsx With Supplied Palette (#2595)
Fix #2499, which see for details of an obscure problem affecting both PhpSpreadsheet and Excel. Add support for palette contained in workbook styles. This seems to be a very rare occurrence, so allow it only when the palette contains exactly 64 entries. If there are other possibilities, we'll presumably have a new workbook to guide us how to handle them. Also add some tests for specification of indexed color without palette, another rarity (no in-range examples amongst our current files). Also change one private static array, initialized once at run-time and never changed, to a constant.
1 parent 0cb60a5 commit 9cf526a

File tree

6 files changed

+174
-72
lines changed

6 files changed

+174
-72
lines changed

src/PhpSpreadsheet/Reader/Xlsx.php

+19
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
541541
$xmlStyles = $this->loadZip("$dir/$xpath[Target]", $mainNS);
542542
}
543543

544+
$palette = self::extractPalette($xmlStyles);
545+
$this->styleReader->setWorkbookPalette($palette);
544546
$fills = self::extractStyles($xmlStyles, 'fills', 'fill');
545547
$fonts = self::extractStyles($xmlStyles, 'fonts', 'font');
546548
$borders = self::extractStyles($xmlStyles, 'borders', 'border');
@@ -2103,4 +2105,21 @@ private static function extractStyles(?SimpleXMLElement $sxml, string $node1, st
21032105

21042106
return $array;
21052107
}
2108+
2109+
private static function extractPalette(?SimpleXMLElement $sxml): array
2110+
{
2111+
$array = [];
2112+
if ($sxml && $sxml->colors->indexedColors) {
2113+
foreach ($sxml->colors->indexedColors->rgbColor as $node) {
2114+
if ($node !== null) {
2115+
$attr = $node->attributes();
2116+
if (isset($attr['rgb'])) {
2117+
$array[] = (string) $attr['rgb'];
2118+
}
2119+
}
2120+
}
2121+
}
2122+
2123+
return (count($array) === 64) ? $array : [];
2124+
}
21062125
}

src/PhpSpreadsheet/Reader/Xlsx/Styles.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class Styles extends BaseParserClass
2424
*/
2525
private $theme;
2626

27+
/** @var array */
28+
private $workbookPalette = [];
29+
2730
/** @var array */
2831
private $styles = [];
2932

@@ -41,6 +44,11 @@ public function setNamespace(string $namespace): void
4144
$this->namespace = $namespace;
4245
}
4346

47+
public function setWorkbookPalette(array $palette): void
48+
{
49+
$this->workbookPalette = $palette;
50+
}
51+
4452
/**
4553
* Cast SimpleXMLElement to bool to overcome Scrutinizer problem.
4654
*
@@ -355,7 +363,11 @@ public function readColor(SimpleXMLElement $color, bool $background = false): st
355363
return (string) $attr['rgb'];
356364
}
357365
if (isset($attr['indexed'])) {
358-
return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? '';
366+
if (empty($this->workbookPalette)) {
367+
return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? '';
368+
}
369+
370+
return Color::indexedColor((int) ($attr['indexed']), $background, $this->workbookPalette)->getARGB() ?? '';
359371
}
360372
if (isset($attr['theme'])) {
361373
if ($this->theme !== null) {

src/PhpSpreadsheet/Style/Color.php

+67-71
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,64 @@ class Color extends Supervisor
4545
const VALIDATE_COLOR_6 = '/^[A-F0-9]{6}$/i';
4646
const VALIDATE_COLOR_8 = '/^[A-F0-9]{8}$/i';
4747

48-
/**
49-
* Indexed colors array.
50-
*
51-
* @var array
52-
*/
53-
protected static $indexedColors;
48+
private const INDEXED_COLORS = [
49+
1 => 'FF000000', // System Colour #1 - Black
50+
2 => 'FFFFFFFF', // System Colour #2 - White
51+
3 => 'FFFF0000', // System Colour #3 - Red
52+
4 => 'FF00FF00', // System Colour #4 - Green
53+
5 => 'FF0000FF', // System Colour #5 - Blue
54+
6 => 'FFFFFF00', // System Colour #6 - Yellow
55+
7 => 'FFFF00FF', // System Colour #7- Magenta
56+
8 => 'FF00FFFF', // System Colour #8- Cyan
57+
9 => 'FF800000', // Standard Colour #9
58+
10 => 'FF008000', // Standard Colour #10
59+
11 => 'FF000080', // Standard Colour #11
60+
12 => 'FF808000', // Standard Colour #12
61+
13 => 'FF800080', // Standard Colour #13
62+
14 => 'FF008080', // Standard Colour #14
63+
15 => 'FFC0C0C0', // Standard Colour #15
64+
16 => 'FF808080', // Standard Colour #16
65+
17 => 'FF9999FF', // Chart Fill Colour #17
66+
18 => 'FF993366', // Chart Fill Colour #18
67+
19 => 'FFFFFFCC', // Chart Fill Colour #19
68+
20 => 'FFCCFFFF', // Chart Fill Colour #20
69+
21 => 'FF660066', // Chart Fill Colour #21
70+
22 => 'FFFF8080', // Chart Fill Colour #22
71+
23 => 'FF0066CC', // Chart Fill Colour #23
72+
24 => 'FFCCCCFF', // Chart Fill Colour #24
73+
25 => 'FF000080', // Chart Line Colour #25
74+
26 => 'FFFF00FF', // Chart Line Colour #26
75+
27 => 'FFFFFF00', // Chart Line Colour #27
76+
28 => 'FF00FFFF', // Chart Line Colour #28
77+
29 => 'FF800080', // Chart Line Colour #29
78+
30 => 'FF800000', // Chart Line Colour #30
79+
31 => 'FF008080', // Chart Line Colour #31
80+
32 => 'FF0000FF', // Chart Line Colour #32
81+
33 => 'FF00CCFF', // Standard Colour #33
82+
34 => 'FFCCFFFF', // Standard Colour #34
83+
35 => 'FFCCFFCC', // Standard Colour #35
84+
36 => 'FFFFFF99', // Standard Colour #36
85+
37 => 'FF99CCFF', // Standard Colour #37
86+
38 => 'FFFF99CC', // Standard Colour #38
87+
39 => 'FFCC99FF', // Standard Colour #39
88+
40 => 'FFFFCC99', // Standard Colour #40
89+
41 => 'FF3366FF', // Standard Colour #41
90+
42 => 'FF33CCCC', // Standard Colour #42
91+
43 => 'FF99CC00', // Standard Colour #43
92+
44 => 'FFFFCC00', // Standard Colour #44
93+
45 => 'FFFF9900', // Standard Colour #45
94+
46 => 'FFFF6600', // Standard Colour #46
95+
47 => 'FF666699', // Standard Colour #47
96+
48 => 'FF969696', // Standard Colour #48
97+
49 => 'FF003366', // Standard Colour #49
98+
50 => 'FF339966', // Standard Colour #50
99+
51 => 'FF003300', // Standard Colour #51
100+
52 => 'FF333300', // Standard Colour #52
101+
53 => 'FF993300', // Standard Colour #53
102+
54 => 'FF993366', // Standard Colour #54
103+
55 => 'FF333399', // Standard Colour #55
104+
56 => 'FF333333', // Standard Colour #56
105+
];
54106

55107
/**
56108
* ARGB - Alpha RGB.
@@ -335,75 +387,19 @@ public static function changeBrightness($hexColourValue, $adjustPercentage)
335387
*
336388
* @return Color
337389
*/
338-
public static function indexedColor($colorIndex, $background = false): self
390+
public static function indexedColor($colorIndex, $background = false, ?array $palette = null): self
339391
{
340392
// Clean parameter
341393
$colorIndex = (int) $colorIndex;
342394

343-
// Indexed colors
344-
if (self::$indexedColors === null) {
345-
self::$indexedColors = [
346-
1 => 'FF000000', // System Colour #1 - Black
347-
2 => 'FFFFFFFF', // System Colour #2 - White
348-
3 => 'FFFF0000', // System Colour #3 - Red
349-
4 => 'FF00FF00', // System Colour #4 - Green
350-
5 => 'FF0000FF', // System Colour #5 - Blue
351-
6 => 'FFFFFF00', // System Colour #6 - Yellow
352-
7 => 'FFFF00FF', // System Colour #7- Magenta
353-
8 => 'FF00FFFF', // System Colour #8- Cyan
354-
9 => 'FF800000', // Standard Colour #9
355-
10 => 'FF008000', // Standard Colour #10
356-
11 => 'FF000080', // Standard Colour #11
357-
12 => 'FF808000', // Standard Colour #12
358-
13 => 'FF800080', // Standard Colour #13
359-
14 => 'FF008080', // Standard Colour #14
360-
15 => 'FFC0C0C0', // Standard Colour #15
361-
16 => 'FF808080', // Standard Colour #16
362-
17 => 'FF9999FF', // Chart Fill Colour #17
363-
18 => 'FF993366', // Chart Fill Colour #18
364-
19 => 'FFFFFFCC', // Chart Fill Colour #19
365-
20 => 'FFCCFFFF', // Chart Fill Colour #20
366-
21 => 'FF660066', // Chart Fill Colour #21
367-
22 => 'FFFF8080', // Chart Fill Colour #22
368-
23 => 'FF0066CC', // Chart Fill Colour #23
369-
24 => 'FFCCCCFF', // Chart Fill Colour #24
370-
25 => 'FF000080', // Chart Line Colour #25
371-
26 => 'FFFF00FF', // Chart Line Colour #26
372-
27 => 'FFFFFF00', // Chart Line Colour #27
373-
28 => 'FF00FFFF', // Chart Line Colour #28
374-
29 => 'FF800080', // Chart Line Colour #29
375-
30 => 'FF800000', // Chart Line Colour #30
376-
31 => 'FF008080', // Chart Line Colour #31
377-
32 => 'FF0000FF', // Chart Line Colour #32
378-
33 => 'FF00CCFF', // Standard Colour #33
379-
34 => 'FFCCFFFF', // Standard Colour #34
380-
35 => 'FFCCFFCC', // Standard Colour #35
381-
36 => 'FFFFFF99', // Standard Colour #36
382-
37 => 'FF99CCFF', // Standard Colour #37
383-
38 => 'FFFF99CC', // Standard Colour #38
384-
39 => 'FFCC99FF', // Standard Colour #39
385-
40 => 'FFFFCC99', // Standard Colour #40
386-
41 => 'FF3366FF', // Standard Colour #41
387-
42 => 'FF33CCCC', // Standard Colour #42
388-
43 => 'FF99CC00', // Standard Colour #43
389-
44 => 'FFFFCC00', // Standard Colour #44
390-
45 => 'FFFF9900', // Standard Colour #45
391-
46 => 'FFFF6600', // Standard Colour #46
392-
47 => 'FF666699', // Standard Colour #47
393-
48 => 'FF969696', // Standard Colour #48
394-
49 => 'FF003366', // Standard Colour #49
395-
50 => 'FF339966', // Standard Colour #50
396-
51 => 'FF003300', // Standard Colour #51
397-
52 => 'FF333300', // Standard Colour #52
398-
53 => 'FF993300', // Standard Colour #53
399-
54 => 'FF993366', // Standard Colour #54
400-
55 => 'FF333399', // Standard Colour #55
401-
56 => 'FF333333', // Standard Colour #56
402-
];
403-
}
404-
405-
if (isset(self::$indexedColors[$colorIndex])) {
406-
return new self(self::$indexedColors[$colorIndex]);
395+
if (empty($palette)) {
396+
if (isset(self::INDEXED_COLORS[$colorIndex])) {
397+
return new self(self::INDEXED_COLORS[$colorIndex]);
398+
}
399+
} else {
400+
if (isset($palette[$colorIndex])) {
401+
return new self($palette[$colorIndex]);
402+
}
407403
}
408404

409405
return ($background) ? new self(self::COLOR_WHITE) : new self(self::COLOR_BLACK);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
4+
5+
use PhpOffice\PhpSpreadsheet\IOFactory;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class Issue2490Test extends TestCase
9+
{
10+
/**
11+
* @var string
12+
*/
13+
private static $testbook = 'tests/data/Reader/XLSX/issue.2490.xlsx';
14+
15+
public function testPreliminaries(): void
16+
{
17+
$file = 'zip://';
18+
$file .= self::$testbook;
19+
$file .= '#xl/styles.xml';
20+
$data = file_get_contents($file);
21+
// confirm that file contains expected color index tag
22+
if ($data === false) {
23+
self::fail('Unable to read file');
24+
} else {
25+
self::assertStringContainsString('<colors><indexedColors><rgbColor rgb="00000000"/>', $data);
26+
}
27+
}
28+
29+
public function testIssue2490(): void
30+
{
31+
// Spreadsheet with its own color palette.
32+
$filename = self::$testbook;
33+
$reader = IOFactory::createReader('Xlsx');
34+
$spreadsheet = $reader->load($filename);
35+
$sheet = $spreadsheet->getActiveSheet();
36+
self::assertSame('00FFFFFF', $sheet->getCell('A3')->getStyle()->getFill()->getStartColor()->getArgb());
37+
self::assertSame('00F0FBFF', $sheet->getCell('A1')->getStyle()->getFill()->getStartColor()->getArgb());
38+
self::assertSame('00F0F0F0', $sheet->getCell('B1')->getStyle()->getFill()->getStartColor()->getArgb());
39+
$spreadsheet->disconnectWorksheets();
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Style;
4+
5+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class ColorIndexTest extends TestCase
9+
{
10+
/**
11+
* @dataProvider providerColorIndexes
12+
*/
13+
public function testColorIndex(string $expectedResult, string $xml, bool $background = false): void
14+
{
15+
$sxml = simplexml_load_string($xml);
16+
if ($sxml === false) {
17+
self::fail('Unable to parse xml');
18+
} else {
19+
$styles = new Styles();
20+
$result = $styles->readColor($sxml, $background);
21+
self::assertSame($expectedResult, $result);
22+
}
23+
}
24+
25+
public function providerColorIndexes(): array
26+
{
27+
return [
28+
'subtract 7 to return system color 4' => ['FF00FF00', '<fgColor indexed="11"/>'],
29+
'default foreground color when out of range' => ['FF000000', '<color indexed="81"/>'],
30+
'default background color when out of range' => ['FFFFFFFF', '<bgColor indexed="81"/>', true],
31+
'rgb specified' => ['FF123456', '<bgColor rgb="FF123456"/>', true],
32+
];
33+
}
34+
}
14.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)