From 290c18e4dbc67387feb8e14ebfea6e543029ec19 Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 26 Nov 2021 09:38:09 -0800 Subject: [PATCH] Xlsx Reader Theme Support Broken After 17.1 (#2403) Fix #2387. Fix #2075. There was substantial refactoring of Writer Xlsx styles in 18.0. An existing static property `$theme` was intended to be shared by both Writer Xlsx and the new Writer Xlsx Styles. However, the initialization of the property in the latter happened later than it should have. This PR makes that initialization happen as soon as the theme has been read. Also, declaring that property as static seems questionable; I have made it an instance member. This small re-factoring makes it possible to now support Themes in tab colors. Since this PR changes Reader/Xlsx/Styles, add type-hinting throughout that module to eliminate Phpstan/Scrutinizer problems. I also removed method readStyle from Reader/Xlsx, since it was essentially duplicated in Reader/Xlsx/Styles. And I added a small number of tests to ensure that Styles is 100% covered. All of this is necessary in preparation for Namespacing phase 2. --- phpstan-baseline.neon | 100 --------------- src/PhpSpreadsheet/Reader/Xlsx.php | 72 +++-------- .../Reader/Xlsx/SheetViewOptions.php | 13 +- src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 117 +++++++++++------- .../Reader/Xlsx/ColorTabTest.php | 22 ++++ .../Reader/Xlsx/CoverageGapsTest.php | 43 +++++++ .../Reader/Xlsx/Issue2387Test.php | 26 ++++ tests/data/Reader/XLSX/colortabs.xlsx | Bin 0 -> 15210 bytes tests/data/Reader/XLSX/issue.2387.xlsx | Bin 0 -> 10089 bytes 9 files changed, 186 insertions(+), 207 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/ColorTabTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/CoverageGapsTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php create mode 100644 tests/data/Reader/XLSX/colortabs.xlsx create mode 100644 tests/data/Reader/XLSX/issue.2387.xlsx diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 52912ab9..04b2a61f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4460,106 +4460,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:dxfs\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:dxfs\\(\\) has parameter \\$readDataOnly with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:getArrayItem\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:getArrayItem\\(\\) has parameter \\$array with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:getArrayItem\\(\\) has parameter \\$key with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:readColor\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:readColor\\(\\) has parameter \\$background with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:readColor\\(\\) has parameter \\$color with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:readProtectionHidden\\(\\) has parameter \\$style with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:readProtectionLocked\\(\\) has parameter \\$style with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:readStyle\\(\\) has parameter \\$style with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:setStyleBaseData\\(\\) has parameter \\$cellStyles with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:setStyleBaseData\\(\\) has parameter \\$styles with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:styles\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Parameter \\#1 \\$hexColourValue of static method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Color\\:\\:changeBrightness\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Parameter \\#2 \\$alignmentXml of static method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:readAlignmentStyle\\(\\) expects SimpleXMLElement, object given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:\\$cellStyles has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:\\$styleXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:\\$styles has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - - - message: "#^Static property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Styles\\:\\:\\$theme \\(PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Theme\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Theme\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Styles.php - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index c8b9126c..4417fe7d 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -19,6 +19,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Settings; @@ -34,7 +35,6 @@ use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; -use stdClass; use Throwable; use XMLReader; use ZipArchive; @@ -50,18 +50,14 @@ class Xlsx extends BaseReader */ private $referenceHelper; - /** - * Xlsx\Theme instance. - * - * @var Xlsx\Theme - */ - private static $theme; - /** * @var ZipArchive */ private $zip; + /** @var Styles */ + private $styleReader; + /** * Create a new Xlsx Reader instance. */ @@ -406,6 +402,8 @@ class Xlsx extends BaseReader // Read the theme first, because we need the colour scheme when reading the styles [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS); + $theme = null; + $this->styleReader = new Styles(); foreach ($wbRels->Relationship as $relx) { $rel = self::getAttributes($relx); $relTarget = (string) $rel['Target']; @@ -438,7 +436,8 @@ class Xlsx extends BaseReader $themeColours[$themePos] = (string) $xmlColourData['val']; } } - self::$theme = new Xlsx\Theme($themeName, $colourSchemeName, $themeColours); + $theme = new Theme($themeName, $colourSchemeName, $themeColours); + $this->styleReader->setTheme($theme); break; } @@ -599,7 +598,7 @@ class Xlsx extends BaseReader // add style to cellXf collection $objStyle = new Style(); - self::readStyle($objStyle, $style); + $this->styleReader->readStyle($objStyle, $style); if ($addingFirstCellXf) { $excel->removeCellXfByIndex(0); // remove the default style $addingFirstCellXf = false; @@ -634,7 +633,7 @@ class Xlsx extends BaseReader // add style to cellStyleXf collection $objStyle = new Style(); - self::readStyle($objStyle, $cellStyle); + $this->styleReader->readStyle($objStyle, $cellStyle); if ($addingFirstCellStyleXf) { $excel->removeCellStyleXfByIndex(0); // remove the default style $addingFirstCellStyleXf = false; @@ -642,10 +641,10 @@ class Xlsx extends BaseReader $excel->addCellStyleXf($objStyle); } } - $styleReader = new Styles($xmlStyles); - $styleReader->setStyleBaseData(self::$theme, $styles, $cellStyles); - $dxfs = $styleReader->dxfs($this->readDataOnly); - $styles = $styleReader->styles(); + $this->styleReader->setStyleXml($xmlStyles); + $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles); + $dxfs = $this->styleReader->dxfs($this->readDataOnly); + $styles = $this->styleReader->styles(); $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS); $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS); @@ -718,7 +717,7 @@ class Xlsx extends BaseReader } $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheet); - $sheetViewOptions->load($this->getReadDataOnly()); + $sheetViewOptions->load($this->getReadDataOnly(), $this->styleReader); (new ColumnAndRowAttributes($docSheet, $xmlSheet)) ->load($this->getReadFilter(), $this->getReadDataOnly()); @@ -1618,45 +1617,6 @@ class Xlsx extends BaseReader return $excel; } - /** - * @param SimpleXMLElement|stdClass $style - */ - private static function readStyle(Style $docStyle, $style): void - { - $docStyle->getNumberFormat()->setFormatCode($style->numFmt); - - // font - if (isset($style->font)) { - Styles::readFontStyle($docStyle->getFont(), $style->font); - } - - // fill - if (isset($style->fill)) { - Styles::readFillStyle($docStyle->getFill(), $style->fill); - } - - // border - if (isset($style->border)) { - Styles::readBorderStyle($docStyle->getBorders(), $style->border); - } - - // alignment - if (isset($style->alignment)) { - Styles::readAlignmentStyle($docStyle->getAlignment(), $style->alignment); - } - - // protection - if (isset($style->protection)) { - Styles::readProtectionLocked($docStyle, $style); - Styles::readProtectionHidden($docStyle, $style); - } - - // top-level style settings - if (isset($style->quotePrefix)) { - $docStyle->setQuotePrefix((bool) $style->quotePrefix); - } - } - /** * @return RichText */ @@ -1685,7 +1645,7 @@ class Xlsx extends BaseReader $objText->getFont()->setSize((float) $attr['val']); } if (isset($run->rPr->color)) { - $objText->getFont()->setColor(new Color(Styles::readColor($run->rPr->color))); + $objText->getFont()->setColor(new Color($this->styleReader->readColor($run->rPr->color))); } if (isset($run->rPr->b)) { $attr = $run->rPr->b->attributes(); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php index 12358818..a302cc56 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -17,17 +17,14 @@ class SheetViewOptions extends BaseParserClass $this->worksheetXml = $worksheetXml; } - /** - * @param bool $readDataOnly - */ - public function load($readDataOnly = false): void + public function load(bool $readDataOnly, Styles $styleReader): void { if ($this->worksheetXml === null) { return; } if (isset($this->worksheetXml->sheetPr)) { - $this->tabColor($this->worksheetXml->sheetPr); + $this->tabColor($this->worksheetXml->sheetPr, $styleReader); $this->codeName($this->worksheetXml->sheetPr); $this->outlines($this->worksheetXml->sheetPr); $this->pageSetup($this->worksheetXml->sheetPr); @@ -42,10 +39,10 @@ class SheetViewOptions extends BaseParserClass } } - private function tabColor(SimpleXMLElement $sheetPr): void + private function tabColor(SimpleXMLElement $sheetPr, Styles $styleReader): void { - if (isset($sheetPr->tabColor, $sheetPr->tabColor['rgb'])) { - $this->worksheet->getTabColor()->setARGB((string) $sheetPr->tabColor['rgb']); + if (isset($sheetPr->tabColor)) { + $this->worksheet->getTabColor()->setARGB($styleReader->readColor($sheetPr->tabColor)); } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 72e66a99..550c52c0 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -13,35 +13,44 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; use PhpOffice\PhpSpreadsheet\Style\Style; use SimpleXMLElement; +use stdClass; class Styles extends BaseParserClass { /** * Theme instance. * - * @var Theme + * @var ?Theme */ - private static $theme; + private $theme; + /** @var array */ private $styles = []; + /** @var array */ private $cellStyles = []; + /** @var SimpleXMLElement */ private $styleXml; - public function __construct(SimpleXMLElement $styleXml) + public function setStyleXml(SimpleXmlElement $styleXml): void { $this->styleXml = $styleXml; } - public function setStyleBaseData(?Theme $theme = null, $styles = [], $cellStyles = []): void + public function setTheme(Theme $theme): void { - self::$theme = $theme; + $this->theme = $theme; + } + + public function setStyleBaseData(?Theme $theme = null, array $styles = [], array $cellStyles = []): void + { + $this->theme = $theme; $this->styles = $styles; $this->cellStyles = $cellStyles; } - public static function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void + public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void { if (isset($fontStyleXml->name, $fontStyleXml->name['val'])) { $fontStyle->setName((string) $fontStyleXml->name['val']); @@ -60,7 +69,7 @@ class Styles extends BaseParserClass !isset($fontStyleXml->strike['val']) || self::boolean((string) $fontStyleXml->strike['val']) ); } - $fontStyle->getColor()->setARGB(self::readColor($fontStyleXml->color)); + $fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color)); if (isset($fontStyleXml->u) && !isset($fontStyleXml->u['val'])) { $fontStyle->setUnderline(Font::UNDERLINE_SINGLE); @@ -78,7 +87,7 @@ class Styles extends BaseParserClass } } - private static function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void + private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void { if ($numfmtStyleXml->count() === 0) { return; @@ -89,7 +98,7 @@ class Styles extends BaseParserClass } } - public static function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml): void + public function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml): void { if ($fillStyleXml->gradientFill) { /** @var SimpleXMLElement $gradientFill */ @@ -99,16 +108,16 @@ class Styles extends BaseParserClass } $fillStyle->setRotation((float) ($gradientFill['degree'])); $gradientFill->registerXPathNamespace('sml', Namespaces::MAIN); - $fillStyle->getStartColor()->setARGB(self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color)); - $fillStyle->getEndColor()->setARGB(self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color)); + $fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color)); + $fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color)); } elseif ($fillStyleXml->patternFill) { $defaultFillStyle = Fill::FILL_NONE; if ($fillStyleXml->patternFill->fgColor) { - $fillStyle->getStartColor()->setARGB(self::readColor($fillStyleXml->patternFill->fgColor, true)); + $fillStyle->getStartColor()->setARGB($this->readColor($fillStyleXml->patternFill->fgColor, true)); $defaultFillStyle = Fill::FILL_SOLID; } if ($fillStyleXml->patternFill->bgColor) { - $fillStyle->getEndColor()->setARGB(self::readColor($fillStyleXml->patternFill->bgColor, true)); + $fillStyle->getEndColor()->setARGB($this->readColor($fillStyleXml->patternFill->bgColor, true)); $defaultFillStyle = Fill::FILL_SOLID; } @@ -120,7 +129,7 @@ class Styles extends BaseParserClass } } - public static function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void + public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void { $diagonalUp = self::boolean((string) $borderStyleXml['diagonalUp']); $diagonalDown = self::boolean((string) $borderStyleXml['diagonalDown']); @@ -134,24 +143,24 @@ class Styles extends BaseParserClass $borderStyle->setDiagonalDirection(Borders::DIAGONAL_BOTH); } - self::readBorder($borderStyle->getLeft(), $borderStyleXml->left); - self::readBorder($borderStyle->getRight(), $borderStyleXml->right); - self::readBorder($borderStyle->getTop(), $borderStyleXml->top); - self::readBorder($borderStyle->getBottom(), $borderStyleXml->bottom); - self::readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal); + $this->readBorder($borderStyle->getLeft(), $borderStyleXml->left); + $this->readBorder($borderStyle->getRight(), $borderStyleXml->right); + $this->readBorder($borderStyle->getTop(), $borderStyleXml->top); + $this->readBorder($borderStyle->getBottom(), $borderStyleXml->bottom); + $this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal); } - private static function readBorder(Border $border, SimpleXMLElement $borderXml): void + private function readBorder(Border $border, SimpleXMLElement $borderXml): void { if (isset($borderXml['style'])) { $border->setBorderStyle((string) $borderXml['style']); } if (isset($borderXml->color)) { - $border->getColor()->setARGB(self::readColor($borderXml->color)); + $border->getColor()->setARGB($this->readColor($borderXml->color)); } } - public static function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void + public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void { $alignment->setHorizontal((string) $alignmentXml['horizontal']); $alignment->setVertical((string) $alignmentXml['vertical']); @@ -174,43 +183,53 @@ class Styles extends BaseParserClass ); } - private function readStyle(Style $docStyle, $style): void + /** + * Read style. + * + * @param SimpleXMLElement|stdClass $style + */ + public function readStyle(Style $docStyle, $style): void { if ($style->numFmt instanceof SimpleXMLElement) { - self::readNumberFormat($docStyle->getNumberFormat(), $style->numFmt); + $this->readNumberFormat($docStyle->getNumberFormat(), $style->numFmt); } else { $docStyle->getNumberFormat()->setFormatCode($style->numFmt); } if (isset($style->font)) { - self::readFontStyle($docStyle->getFont(), $style->font); + $this->readFontStyle($docStyle->getFont(), $style->font); } if (isset($style->fill)) { - self::readFillStyle($docStyle->getFill(), $style->fill); + $this->readFillStyle($docStyle->getFill(), $style->fill); } if (isset($style->border)) { - self::readBorderStyle($docStyle->getBorders(), $style->border); + $this->readBorderStyle($docStyle->getBorders(), $style->border); } - if (isset($style->alignment->alignment)) { - self::readAlignmentStyle($docStyle->getAlignment(), $style->alignment); + if (isset($style->alignment)) { + $this->readAlignmentStyle($docStyle->getAlignment(), $style->alignment); } // protection if (isset($style->protection)) { - self::readProtectionLocked($docStyle, $style); - self::readProtectionHidden($docStyle, $style); + $this->readProtectionLocked($docStyle, $style); + $this->readProtectionHidden($docStyle, $style); } // top-level style settings if (isset($style->quotePrefix)) { - $docStyle->setQuotePrefix(true); + $docStyle->setQuotePrefix((bool) $style->quotePrefix); } } - public static function readProtectionLocked(Style $docStyle, $style): void + /** + * Read protection locked attribute. + * + * @param SimpleXMLElement|stdClass $style + */ + public function readProtectionLocked(Style $docStyle, $style): void { if (isset($style->protection['locked'])) { if (self::boolean((string) $style->protection['locked'])) { @@ -221,7 +240,12 @@ class Styles extends BaseParserClass } } - public static function readProtectionHidden(Style $docStyle, $style): void + /** + * Read protection hidden attribute. + * + * @param SimpleXMLElement|stdClass $style + */ + public function readProtectionHidden(Style $docStyle, $style): void { if (isset($style->protection['hidden'])) { if (self::boolean((string) $style->protection['hidden'])) { @@ -232,18 +256,18 @@ class Styles extends BaseParserClass } } - public static function readColor($color, $background = false) + public function readColor(SimpleXMLElement $color, bool $background = false): string { if (isset($color['rgb'])) { return (string) $color['rgb']; } elseif (isset($color['indexed'])) { - return Color::indexedColor($color['indexed'] - 7, $background)->getARGB(); + return Color::indexedColor((int) ($color['indexed'] - 7), $background)->getARGB() ?? ''; } elseif (isset($color['theme'])) { - if (self::$theme !== null) { - $returnColour = self::$theme->getColourByIndex((int) $color['theme']); + if ($this->theme !== null) { + $returnColour = $this->theme->getColourByIndex((int) $color['theme']); if (isset($color['tint'])) { $tintAdjust = (float) $color['tint']; - $returnColour = Color::changeBrightness($returnColour, $tintAdjust); + $returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust); } return 'FF' . $returnColour; @@ -253,7 +277,7 @@ class Styles extends BaseParserClass return ($background) ? 'FFFFFFFF' : 'FF000000'; } - public function dxfs($readDataOnly = false) + public function dxfs(bool $readDataOnly = false): array { $dxfs = []; if (!$readDataOnly && $this->styleXml) { @@ -285,13 +309,20 @@ class Styles extends BaseParserClass return $dxfs; } - public function styles() + public function styles(): array { return $this->styles; } - private static function getArrayItem($array, $key = 0) + /** + * Get array item. + * + * @param mixed $array (usually array, in theory can be false) + * + * @return stdClass + */ + private static function getArrayItem($array, int $key = 0) { - return $array[$key] ?? null; + return is_array($array) ? ($array[$key] ?? null) : null; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ColorTabTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ColorTabTest.php new file mode 100644 index 00000000..2218ace1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ColorTabTest.php @@ -0,0 +1,22 @@ +load($filename); + + // theme color + self::assertSame('FF548135', $spreadsheet->getSheet(0)->getTabColor()->getArgb()); + // rgb color + self::assertSame('FFFFC000', $spreadsheet->getSheet(1)->getTabColor()->getArgb()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/CoverageGapsTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/CoverageGapsTest.php new file mode 100644 index 00000000..6630a3fe --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/CoverageGapsTest.php @@ -0,0 +1,43 @@ +getActiveSheet(); + $sheet + ->getStyle('A1') + ->getBorders() + ->setDiagonalDirection(Borders::DIAGONAL_BOTH) + ->getDiagonal() + ->setBorderStyle(Border::BORDER_DASHDOTDOT); + $sheet + ->getStyle('A2') + ->getProtection() + ->setLocked(Protection::PROTECTION_PROTECTED); + $sheet + ->getStyle('A3') + ->getAlignment() + ->setTextRotation(Alignment::TEXTROTATION_STACK_EXCEL); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame(Borders::DIAGONAL_BOTH, $rsheet->getStyle('A1')->getBorders()->getDiagonalDirection()); + self::assertSame(Border::BORDER_DASHDOTDOT, $rsheet->getStyle('A1')->getBorders()->getDiagonal()->getBorderStyle()); + self::assertSame(Protection::PROTECTION_PROTECTED, $rsheet->getStyle('A2')->getProtection()->getLocked()); + self::assertSame(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET, $rsheet->getStyle('A3')->getAlignment()->getTextRotation()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php new file mode 100644 index 00000000..870ea6ab --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php @@ -0,0 +1,26 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('335593', $sheet->getCell('B2')->getStyle()->getFont()->getColor()->getRgb()); + self::assertSame(Fill::FILL_NONE, $sheet->getCell('B2')->getStyle()->getFill()->getFillType()); + self::assertSame('FFFFFF', $sheet->getCell('C2')->getStyle()->getFont()->getColor()->getRgb()); + self::assertSame('000000', $sheet->getCell('C2')->getStyle()->getFill()->getStartColor()->getRgb()); + self::assertSame(Fill::FILL_SOLID, $sheet->getCell('C2')->getStyle()->getFill()->getFillType()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/colortabs.xlsx b/tests/data/Reader/XLSX/colortabs.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..84863ab45e3c45b3762dfd55cb76ff8988b8c314 GIT binary patch literal 15210 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLSX/issue.2387.xlsx b/tests/data/Reader/XLSX/issue.2387.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..891ee31dbf4a2ad4f93518f1635973c0dcdbed7e GIT binary patch literal 10089 zcmeHNcY+3Y2n3hl&O5t%@7-*6 z-@oAA{%~fdyX%?L=l48S)m2AP1_t&803Lt{002k5eC8XN$C1we$>61BB) zGPZHjed}&#?5NG)W^F~12MbM=1AvCy|G(qEcm>Kp!!}(^=;Bw2x6k7m-WlxVmC`^* z2qe)f?; z&i5aDY&C_8x0A*@&N0Ck;&`d68)cFXP%p3vVYdwHg4w3hs)D8^?)aG5_lA~gxsUs~ z2tyCS^`V|!|3_EYXz4w+Ik$7zDD_t~S+l!8W~5*P%iNZ5T*}hI&?A!q;#Jxl_MU%S z!YG|ewXB+kvW-XU^F&%6ee%;@)p{j2g&bZIdQM|i(>WXLd((J*?xRGAvunSbE+M0+ z!(g2z6hs=JI0qfe{!WLMws!GOpku6e;EbiUD(>i^<^+Qn)W&(@#lI?�@RBfY%`t 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