From 6b81066db640dcb30dfd34daf17cf587796d1888 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Wed, 17 Nov 2021 20:14:44 -0800 Subject: [PATCH] Xlsx Reader Theme Support Broken After 17.1 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 bcdba204ff..92887e8aaa 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 59403c6424..e40e02593e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -19,6 +19,7 @@ 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\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 @@ public function load(string $filename, int $flags = 0): Spreadsheet // 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 @@ public function load(string $filename, int $flags = 0): Spreadsheet $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 @@ public function load(string $filename, int $flags = 0): Spreadsheet // 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 @@ public function load(string $filename, int $flags = 0): Spreadsheet // 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 @@ public function load(string $filename, int $flags = 0): Spreadsheet $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 @@ public function load(string $filename, int $flags = 0): Spreadsheet } $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 @@ public function load(string $filename, int $flags = 0): Spreadsheet 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 @@ private function parseRichText(?SimpleXMLElement $is) $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 123588182b..a302cc5692 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -17,17 +17,14 @@ public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXm $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 @@ public function load($readDataOnly = false): void } } - 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 72e66a9911..550c52c0e9 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -13,35 +13,44 @@ 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 @@ public static function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyl !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 @@ public static function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyl } } - 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 @@ private static function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLEle } } - 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 @@ public static function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyl } $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 @@ public static function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyl } } - 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 @@ public static function readBorderStyle(Borders $borderStyle, SimpleXMLElement $b $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 @@ public static function readAlignmentStyle(Alignment $alignment, SimpleXMLElement ); } - 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 @@ public static function readProtectionLocked(Style $docStyle, $style): void } } - 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 @@ public static function readProtectionHidden(Style $docStyle, $style): void } } - 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 @@ public static function readColor($color, $background = false) 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 @@ public function dxfs($readDataOnly = false) 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 0000000000..2218ace14f --- /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 0000000000..6630a3fe61 --- /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 0000000000..870ea6ab04 --- /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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