From 9cf526a920460cad15ff40f3f4c7264e0b082ca4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 23 Feb 2022 22:09:22 -0800 Subject: [PATCH] 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. --- src/PhpSpreadsheet/Reader/Xlsx.php | 19 +++ src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 14 +- src/PhpSpreadsheet/Style/Color.php | 138 +++++++++--------- .../Reader/Xlsx/Issue2490Test.php | 41 ++++++ .../Style/ColorIndexTest.php | 34 +++++ tests/data/Reader/XLSX/issue.2490.xlsx | Bin 0 -> 14793 bytes 6 files changed, 174 insertions(+), 72 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php create mode 100644 tests/PhpSpreadsheetTests/Style/ColorIndexTest.php create mode 100644 tests/data/Reader/XLSX/issue.2490.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index c9134c8e..eca8d7b8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -541,6 +541,8 @@ class Xlsx extends BaseReader $xmlStyles = $this->loadZip("$dir/$xpath[Target]", $mainNS); } + $palette = self::extractPalette($xmlStyles); + $this->styleReader->setWorkbookPalette($palette); $fills = self::extractStyles($xmlStyles, 'fills', 'fill'); $fonts = self::extractStyles($xmlStyles, 'fonts', 'font'); $borders = self::extractStyles($xmlStyles, 'borders', 'border'); @@ -2103,4 +2105,21 @@ class Xlsx extends BaseReader return $array; } + + private static function extractPalette(?SimpleXMLElement $sxml): array + { + $array = []; + if ($sxml && $sxml->colors->indexedColors) { + foreach ($sxml->colors->indexedColors->rgbColor as $node) { + if ($node !== null) { + $attr = $node->attributes(); + if (isset($attr['rgb'])) { + $array[] = (string) $attr['rgb']; + } + } + } + } + + return (count($array) === 64) ? $array : []; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index b4c718d5..2c15c515 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -24,6 +24,9 @@ class Styles extends BaseParserClass */ private $theme; + /** @var array */ + private $workbookPalette = []; + /** @var array */ private $styles = []; @@ -41,6 +44,11 @@ class Styles extends BaseParserClass $this->namespace = $namespace; } + public function setWorkbookPalette(array $palette): void + { + $this->workbookPalette = $palette; + } + /** * Cast SimpleXMLElement to bool to overcome Scrutinizer problem. * @@ -355,7 +363,11 @@ class Styles extends BaseParserClass return (string) $attr['rgb']; } if (isset($attr['indexed'])) { - return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? ''; + if (empty($this->workbookPalette)) { + return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? ''; + } + + return Color::indexedColor((int) ($attr['indexed']), $background, $this->workbookPalette)->getARGB() ?? ''; } if (isset($attr['theme'])) { if ($this->theme !== null) { diff --git a/src/PhpSpreadsheet/Style/Color.php b/src/PhpSpreadsheet/Style/Color.php index 6fd91c64..802869a1 100644 --- a/src/PhpSpreadsheet/Style/Color.php +++ b/src/PhpSpreadsheet/Style/Color.php @@ -45,12 +45,64 @@ class Color extends Supervisor const VALIDATE_COLOR_6 = '/^[A-F0-9]{6}$/i'; const VALIDATE_COLOR_8 = '/^[A-F0-9]{8}$/i'; - /** - * Indexed colors array. - * - * @var array - */ - protected static $indexedColors; + private const INDEXED_COLORS = [ + 1 => 'FF000000', // System Colour #1 - Black + 2 => 'FFFFFFFF', // System Colour #2 - White + 3 => 'FFFF0000', // System Colour #3 - Red + 4 => 'FF00FF00', // System Colour #4 - Green + 5 => 'FF0000FF', // System Colour #5 - Blue + 6 => 'FFFFFF00', // System Colour #6 - Yellow + 7 => 'FFFF00FF', // System Colour #7- Magenta + 8 => 'FF00FFFF', // System Colour #8- Cyan + 9 => 'FF800000', // Standard Colour #9 + 10 => 'FF008000', // Standard Colour #10 + 11 => 'FF000080', // Standard Colour #11 + 12 => 'FF808000', // Standard Colour #12 + 13 => 'FF800080', // Standard Colour #13 + 14 => 'FF008080', // Standard Colour #14 + 15 => 'FFC0C0C0', // Standard Colour #15 + 16 => 'FF808080', // Standard Colour #16 + 17 => 'FF9999FF', // Chart Fill Colour #17 + 18 => 'FF993366', // Chart Fill Colour #18 + 19 => 'FFFFFFCC', // Chart Fill Colour #19 + 20 => 'FFCCFFFF', // Chart Fill Colour #20 + 21 => 'FF660066', // Chart Fill Colour #21 + 22 => 'FFFF8080', // Chart Fill Colour #22 + 23 => 'FF0066CC', // Chart Fill Colour #23 + 24 => 'FFCCCCFF', // Chart Fill Colour #24 + 25 => 'FF000080', // Chart Line Colour #25 + 26 => 'FFFF00FF', // Chart Line Colour #26 + 27 => 'FFFFFF00', // Chart Line Colour #27 + 28 => 'FF00FFFF', // Chart Line Colour #28 + 29 => 'FF800080', // Chart Line Colour #29 + 30 => 'FF800000', // Chart Line Colour #30 + 31 => 'FF008080', // Chart Line Colour #31 + 32 => 'FF0000FF', // Chart Line Colour #32 + 33 => 'FF00CCFF', // Standard Colour #33 + 34 => 'FFCCFFFF', // Standard Colour #34 + 35 => 'FFCCFFCC', // Standard Colour #35 + 36 => 'FFFFFF99', // Standard Colour #36 + 37 => 'FF99CCFF', // Standard Colour #37 + 38 => 'FFFF99CC', // Standard Colour #38 + 39 => 'FFCC99FF', // Standard Colour #39 + 40 => 'FFFFCC99', // Standard Colour #40 + 41 => 'FF3366FF', // Standard Colour #41 + 42 => 'FF33CCCC', // Standard Colour #42 + 43 => 'FF99CC00', // Standard Colour #43 + 44 => 'FFFFCC00', // Standard Colour #44 + 45 => 'FFFF9900', // Standard Colour #45 + 46 => 'FFFF6600', // Standard Colour #46 + 47 => 'FF666699', // Standard Colour #47 + 48 => 'FF969696', // Standard Colour #48 + 49 => 'FF003366', // Standard Colour #49 + 50 => 'FF339966', // Standard Colour #50 + 51 => 'FF003300', // Standard Colour #51 + 52 => 'FF333300', // Standard Colour #52 + 53 => 'FF993300', // Standard Colour #53 + 54 => 'FF993366', // Standard Colour #54 + 55 => 'FF333399', // Standard Colour #55 + 56 => 'FF333333', // Standard Colour #56 + ]; /** * ARGB - Alpha RGB. @@ -335,75 +387,19 @@ class Color extends Supervisor * * @return Color */ - public static function indexedColor($colorIndex, $background = false): self + public static function indexedColor($colorIndex, $background = false, ?array $palette = null): self { // Clean parameter $colorIndex = (int) $colorIndex; - // Indexed colors - if (self::$indexedColors === null) { - self::$indexedColors = [ - 1 => 'FF000000', // System Colour #1 - Black - 2 => 'FFFFFFFF', // System Colour #2 - White - 3 => 'FFFF0000', // System Colour #3 - Red - 4 => 'FF00FF00', // System Colour #4 - Green - 5 => 'FF0000FF', // System Colour #5 - Blue - 6 => 'FFFFFF00', // System Colour #6 - Yellow - 7 => 'FFFF00FF', // System Colour #7- Magenta - 8 => 'FF00FFFF', // System Colour #8- Cyan - 9 => 'FF800000', // Standard Colour #9 - 10 => 'FF008000', // Standard Colour #10 - 11 => 'FF000080', // Standard Colour #11 - 12 => 'FF808000', // Standard Colour #12 - 13 => 'FF800080', // Standard Colour #13 - 14 => 'FF008080', // Standard Colour #14 - 15 => 'FFC0C0C0', // Standard Colour #15 - 16 => 'FF808080', // Standard Colour #16 - 17 => 'FF9999FF', // Chart Fill Colour #17 - 18 => 'FF993366', // Chart Fill Colour #18 - 19 => 'FFFFFFCC', // Chart Fill Colour #19 - 20 => 'FFCCFFFF', // Chart Fill Colour #20 - 21 => 'FF660066', // Chart Fill Colour #21 - 22 => 'FFFF8080', // Chart Fill Colour #22 - 23 => 'FF0066CC', // Chart Fill Colour #23 - 24 => 'FFCCCCFF', // Chart Fill Colour #24 - 25 => 'FF000080', // Chart Line Colour #25 - 26 => 'FFFF00FF', // Chart Line Colour #26 - 27 => 'FFFFFF00', // Chart Line Colour #27 - 28 => 'FF00FFFF', // Chart Line Colour #28 - 29 => 'FF800080', // Chart Line Colour #29 - 30 => 'FF800000', // Chart Line Colour #30 - 31 => 'FF008080', // Chart Line Colour #31 - 32 => 'FF0000FF', // Chart Line Colour #32 - 33 => 'FF00CCFF', // Standard Colour #33 - 34 => 'FFCCFFFF', // Standard Colour #34 - 35 => 'FFCCFFCC', // Standard Colour #35 - 36 => 'FFFFFF99', // Standard Colour #36 - 37 => 'FF99CCFF', // Standard Colour #37 - 38 => 'FFFF99CC', // Standard Colour #38 - 39 => 'FFCC99FF', // Standard Colour #39 - 40 => 'FFFFCC99', // Standard Colour #40 - 41 => 'FF3366FF', // Standard Colour #41 - 42 => 'FF33CCCC', // Standard Colour #42 - 43 => 'FF99CC00', // Standard Colour #43 - 44 => 'FFFFCC00', // Standard Colour #44 - 45 => 'FFFF9900', // Standard Colour #45 - 46 => 'FFFF6600', // Standard Colour #46 - 47 => 'FF666699', // Standard Colour #47 - 48 => 'FF969696', // Standard Colour #48 - 49 => 'FF003366', // Standard Colour #49 - 50 => 'FF339966', // Standard Colour #50 - 51 => 'FF003300', // Standard Colour #51 - 52 => 'FF333300', // Standard Colour #52 - 53 => 'FF993300', // Standard Colour #53 - 54 => 'FF993366', // Standard Colour #54 - 55 => 'FF333399', // Standard Colour #55 - 56 => 'FF333333', // Standard Colour #56 - ]; - } - - if (isset(self::$indexedColors[$colorIndex])) { - return new self(self::$indexedColors[$colorIndex]); + if (empty($palette)) { + if (isset(self::INDEXED_COLORS[$colorIndex])) { + return new self(self::INDEXED_COLORS[$colorIndex]); + } + } else { + if (isset($palette[$colorIndex])) { + return new self($palette[$colorIndex]); + } } return ($background) ? new self(self::COLOR_WHITE) : new self(self::COLOR_BLACK); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php new file mode 100644 index 00000000..182581e1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php @@ -0,0 +1,41 @@ +', $data); + } + } + + public function testIssue2490(): void + { + // Spreadsheet with its own color palette. + $filename = self::$testbook; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('00FFFFFF', $sheet->getCell('A3')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('00F0FBFF', $sheet->getCell('A1')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('00F0F0F0', $sheet->getCell('B1')->getStyle()->getFill()->getStartColor()->getArgb()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/ColorIndexTest.php b/tests/PhpSpreadsheetTests/Style/ColorIndexTest.php new file mode 100644 index 00000000..22a7f56c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/ColorIndexTest.php @@ -0,0 +1,34 @@ +readColor($sxml, $background); + self::assertSame($expectedResult, $result); + } + } + + public function providerColorIndexes(): array + { + return [ + 'subtract 7 to return system color 4' => ['FF00FF00', ''], + 'default foreground color when out of range' => ['FF000000', ''], + 'default background color when out of range' => ['FFFFFFFF', '', true], + 'rgb specified' => ['FF123456', '', true], + ]; + } +} diff --git a/tests/data/Reader/XLSX/issue.2490.xlsx b/tests/data/Reader/XLSX/issue.2490.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..67e5cd05d0b804af7d956fee668a2688084fc6f8 GIT binary patch literal 14793 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 literal 0 HcmV?d00001