From ad5532e2f453f39f8fa041b99461935c9234d1df Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 11 Feb 2022 06:42:04 -0800 Subject: [PATCH] Namespacing Phase 2 - Styles (#2471) * WIP Namespacing Phase 2 - Styles This is part 2 of a several-phase process to permit PhpSpreadsheet to handle input Xlsx files which use unexpected namespacing. The first phase, introduced as part of release 1.19.0, essentially handled the reading of data. This phase handles the reading of styles. More phases are planned. It is my intention to leave this in draft status for at least a month. This will give time for additional testing, by me and, I hope, others who might be interested. This fixes the same problem addressed by PR #2458, if it reaches mergeable status before I am ready to take this out of draft status. I do not anticipate any difficult merge conflicts if the other change is merged first. This change is more difficult than I'd hoped. I can't get xpath to work properly with the namespaced style file, even though I don't have difficulties with others. Normally we expect: ```xml ` while most output fill colors as ``. EPPlus actually makes more sense. Regardless, validating length of rgb/argb is a recent development for PhpSpreadsheet, under the assumption that an incorrect length is a user error. This development invalidates that assumption, so restore the previous behavior. In addition, a comment in Colors.php says that the supplied color is "the ARGB value for the colour, or named colour". However, although named colors are accepted, nothing sensible is done with them - they are passed unchanged to the ARGB value, where Excel treats them as black. The routine should either reject the named color, or convert it to the appropriate ARGB value. This change implements the latter. --- src/PhpSpreadsheet/Reader/Xlsx.php | 28 ++- src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 224 ++++++++++++------ src/PhpSpreadsheet/Style/Color.php | 58 +++-- tests/PhpSpreadsheetTests/CommentTest.php | 2 +- .../Reader/Xlsx/Issue2494Test.php | 21 ++ .../Reader/Xlsx/NamespaceNonStdTest.php | 12 +- .../Reader/Xlsx/RichTextTest.php | 53 +++++ tests/PhpSpreadsheetTests/Style/ColorTest.php | 27 +++ tests/data/Reader/XLSX/issue.2494.xlsx | Bin 0 -> 21885 bytes 9 files changed, 318 insertions(+), 107 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2494Test.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/RichTextTest.php create mode 100644 tests/data/Reader/XLSX/issue.2494.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 280e3fe1..bf696a60 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -539,16 +539,14 @@ class Xlsx extends BaseReader if ($xpath === null) { $xmlStyles = self::testSimpleXml(null); } else { - // I think Nonamespace is okay because I'm using xpath. - $xmlStyles = $this->loadZipNonamespace("$dir/$xpath[Target]", $mainNS); + $xmlStyles = $this->loadZip("$dir/$xpath[Target]", $mainNS); } - $xmlStyles->registerXPathNamespace('smm', Namespaces::MAIN); - $fills = self::xpathNoFalse($xmlStyles, 'smm:fills/smm:fill'); - $fonts = self::xpathNoFalse($xmlStyles, 'smm:fonts/smm:font'); - $borders = self::xpathNoFalse($xmlStyles, 'smm:borders/smm:border'); - $xfTags = self::xpathNoFalse($xmlStyles, 'smm:cellXfs/smm:xf'); - $cellXfTags = self::xpathNoFalse($xmlStyles, 'smm:cellStyleXfs/smm:xf'); + $fills = self::extractStyles($xmlStyles, 'fills', 'fill'); + $fonts = self::extractStyles($xmlStyles, 'fonts', 'font'); + $borders = self::extractStyles($xmlStyles, 'borders', 'border'); + $xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf'); + $cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf'); $styles = []; $cellStyles = []; @@ -559,6 +557,7 @@ class Xlsx extends BaseReader if (isset($numFmts) && ($numFmts !== null)) { $numFmts->registerXPathNamespace('sml', $mainNS); } + $this->styleReader->setNamespace($mainNS); if (!$this->readDataOnly/* && $xmlStyles*/) { foreach ($xfTags as $xfTag) { $xf = self::getAttributes($xfTag); @@ -643,6 +642,7 @@ class Xlsx extends BaseReader } } $this->styleReader->setStyleXml($xmlStyles); + $this->styleReader->setNamespace($mainNS); $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles); $dxfs = $this->styleReader->dxfs($this->readDataOnly); $styles = $this->styleReader->styles(); @@ -2088,4 +2088,16 @@ class Xlsx extends BaseReader } } } + + private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array + { + $array = []; + if ($sxml && $sxml->{$node1}->{$node2}) { + foreach ($sxml->{$node1}->{$node2} as $node) { + $array[] = $node; + } + } + + return $array; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 6f01c745..65dac529 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -33,6 +33,37 @@ class Styles extends BaseParserClass /** @var SimpleXMLElement */ private $styleXml; + /** @var string */ + private $namespace = ''; + + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; + } + + /** + * Cast SimpleXMLElement to bool to overcome Scrutinizer problem. + * + * @param mixed $value + */ + private static function castBool($value): bool + { + return (bool) $value; + } + + private function getStyleAttributes(SimpleXMLElement $value): SimpleXMLElement + { + $attr = null; + if (self::castBool($value)) { + $attr = $value->attributes(''); + if ($attr === null || count($attr) === 0) { + $attr = $value->attributes($this->namespace); + } + } + + return Xlsx::testSimpleXml($attr); + } + public function setStyleXml(SimpleXmlElement $styleXml): void { $this->styleXml = $styleXml; @@ -52,48 +83,62 @@ class Styles extends BaseParserClass public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void { - if (isset($fontStyleXml->name, $fontStyleXml->name['val'])) { - $fontStyle->setName((string) $fontStyleXml->name['val']); + if (isset($fontStyleXml->name)) { + $attr = $this->getStyleAttributes($fontStyleXml->name); + if (isset($attr['val'])) { + $fontStyle->setName((string) $attr['val']); + } } - if (isset($fontStyleXml->sz, $fontStyleXml->sz['val'])) { - $fontStyle->setSize((float) $fontStyleXml->sz['val']); + if (isset($fontStyleXml->sz)) { + $attr = $this->getStyleAttributes($fontStyleXml->sz); + if (isset($attr['val'])) { + $fontStyle->setSize((float) $attr['val']); + } } if (isset($fontStyleXml->b)) { - $fontStyle->setBold(!isset($fontStyleXml->b['val']) || self::boolean((string) $fontStyleXml->b['val'])); + $attr = $this->getStyleAttributes($fontStyleXml->b); + $fontStyle->setBold(!isset($attr['val']) || self::boolean((string) $attr['val'])); } if (isset($fontStyleXml->i)) { - $fontStyle->setItalic(!isset($fontStyleXml->i['val']) || self::boolean((string) $fontStyleXml->i['val'])); + $attr = $this->getStyleAttributes($fontStyleXml->i); + $fontStyle->setItalic(!isset($attr['val']) || self::boolean((string) $attr['val'])); } if (isset($fontStyleXml->strike)) { - $fontStyle->setStrikethrough( - !isset($fontStyleXml->strike['val']) || self::boolean((string) $fontStyleXml->strike['val']) - ); + $attr = $this->getStyleAttributes($fontStyleXml->strike); + $fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val'])); } $fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color)); - if (isset($fontStyleXml->u) && !isset($fontStyleXml->u['val'])) { - $fontStyle->setUnderline(Font::UNDERLINE_SINGLE); - } elseif (isset($fontStyleXml->u, $fontStyleXml->u['val'])) { - $fontStyle->setUnderline((string) $fontStyleXml->u['val']); + if (isset($fontStyleXml->u)) { + $attr = $this->getStyleAttributes($fontStyleXml->u); + if (!isset($attr['val'])) { + $fontStyle->setUnderline(Font::UNDERLINE_SINGLE); + } else { + $fontStyle->setUnderline((string) $attr['val']); + } } - - if (isset($fontStyleXml->vertAlign, $fontStyleXml->vertAlign['val'])) { - $verticalAlign = strtolower((string) $fontStyleXml->vertAlign['val']); - if ($verticalAlign === 'superscript') { - $fontStyle->setSuperscript(true); - } elseif ($verticalAlign === 'subscript') { - $fontStyle->setSubscript(true); + if (isset($fontStyleXml->vertAlign)) { + $attr = $this->getStyleAttributes($fontStyleXml->vertAlign); + if (!isset($attr['val'])) { + $verticalAlign = strtolower((string) $attr['val']); + if ($verticalAlign === 'superscript') { + $fontStyle->setSuperscript(true); + } elseif ($verticalAlign === 'subscript') { + $fontStyle->setSubscript(true); + } } } } private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void { - if ($numfmtStyleXml->count() === 0) { + if ((string) $numfmtStyleXml['formatCode'] !== '') { + $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmtStyleXml['formatCode'])); + return; } - $numfmt = Xlsx::getAttributes($numfmtStyleXml); - if ($numfmt->count() > 0 && isset($numfmt['formatCode'])) { + $numfmt = $this->getStyleAttributes($numfmtStyleXml); + if (isset($numfmt['formatCode'])) { $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmt['formatCode'])); } } @@ -103,10 +148,11 @@ class Styles extends BaseParserClass if ($fillStyleXml->gradientFill) { /** @var SimpleXMLElement $gradientFill */ $gradientFill = $fillStyleXml->gradientFill[0]; - if (!empty($gradientFill['type'])) { - $fillStyle->setFillType((string) $gradientFill['type']); + $attr = $this->getStyleAttributes($gradientFill); + if (!empty($attr['type'])) { + $fillStyle->setFillType((string) $attr['type']); } - $fillStyle->setRotation((float) ($gradientFill['degree'])); + $fillStyle->setRotation((float) ($attr['degree'])); $gradientFill->registerXPathNamespace('sml', Namespaces::MAIN); $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)); @@ -121,9 +167,14 @@ class Styles extends BaseParserClass $defaultFillStyle = Fill::FILL_SOLID; } - $patternType = (string) $fillStyleXml->patternFill['patternType'] != '' - ? (string) $fillStyleXml->patternFill['patternType'] - : $defaultFillStyle; + $type = ''; + if ((string) $fillStyleXml->patternFill['patternType'] !== '') { + $type = (string) $fillStyleXml->patternFill['patternType']; + } else { + $attr = $this->getStyleAttributes($fillStyleXml->patternFill); + $type = (string) $attr['patternType']; + } + $patternType = ($type === '') ? $defaultFillStyle : $type; $fillStyle->setFillType($patternType); } @@ -131,8 +182,10 @@ class Styles extends BaseParserClass public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void { - $diagonalUp = self::boolean((string) $borderStyleXml['diagonalUp']); - $diagonalDown = self::boolean((string) $borderStyleXml['diagonalDown']); + $diagonalUp = $this->getAttribute($borderStyleXml, 'diagonalUp'); + $diagonalUp = self::boolean($diagonalUp); + $diagonalDown = $this->getAttribute($borderStyleXml, 'diagonalDown'); + $diagonalDown = self::boolean($diagonalDown); if (!$diagonalUp && !$diagonalDown) { $borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE); } elseif ($diagonalUp && !$diagonalDown) { @@ -150,10 +203,26 @@ class Styles extends BaseParserClass $this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal); } + private function getAttribute(SimpleXMLElement $xml, string $attribute): string + { + $style = ''; + if ((string) $xml[$attribute] !== '') { + $style = (string) $xml[$attribute]; + } else { + $attr = $this->getStyleAttributes($xml); + if (isset($attr[$attribute])) { + $style = (string) $attr[$attribute]; + } + } + + return $style; + } + private function readBorder(Border $border, SimpleXMLElement $borderXml): void { - if (isset($borderXml['style'])) { - $border->setBorderStyle((string) $borderXml['style']); + $style = $this->getAttribute($borderXml, 'style'); + if ($style !== '') { + $border->setBorderStyle((string) $style); } if (isset($borderXml->color)) { $border->getColor()->setARGB($this->readColor($borderXml->color)); @@ -162,25 +231,25 @@ class Styles extends BaseParserClass public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void { - $alignment->setHorizontal((string) $alignmentXml['horizontal']); - $alignment->setVertical((string) $alignmentXml['vertical']); + $horizontal = $this->getAttribute($alignmentXml, 'horizontal'); + $alignment->setHorizontal($horizontal); + $vertical = $this->getAttribute($alignmentXml, 'vertical'); + $alignment->setVertical((string) $vertical); - $textRotation = 0; - if ((int) $alignmentXml['textRotation'] <= 90) { - $textRotation = (int) $alignmentXml['textRotation']; - } elseif ((int) $alignmentXml['textRotation'] > 90) { - $textRotation = 90 - (int) $alignmentXml['textRotation']; + $textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation'); + if ($textRotation > 90) { + $textRotation = 90 - $textRotation; } + $alignment->setTextRotation($textRotation); - $alignment->setTextRotation((int) $textRotation); - $alignment->setWrapText(self::boolean((string) $alignmentXml['wrapText'])); - $alignment->setShrinkToFit(self::boolean((string) $alignmentXml['shrinkToFit'])); - $alignment->setIndent( - (int) ((string) $alignmentXml['indent']) > 0 ? (int) ((string) $alignmentXml['indent']) : 0 - ); - $alignment->setReadOrder( - (int) ((string) $alignmentXml['readingOrder']) > 0 ? (int) ((string) $alignmentXml['readingOrder']) : 0 - ); + $wrapText = $this->getAttribute($alignmentXml, 'wrapText'); + $alignment->setWrapText(self::boolean((string) $wrapText)); + $shrinkToFit = $this->getAttribute($alignmentXml, 'shrinkToFit'); + $alignment->setShrinkToFit(self::boolean((string) $shrinkToFit)); + $indent = (int) $this->getAttribute($alignmentXml, 'indent'); + $alignment->setIndent(max($indent, 0)); + $readingOrder = (int) $this->getAttribute($alignmentXml, 'readingOrder'); + $alignment->setReadOrder(max($readingOrder, 0)); } private static function formatGeneral(string $formatString): string @@ -223,8 +292,8 @@ class Styles extends BaseParserClass // protection if (isset($style->protection)) { - $this->readProtectionLocked($docStyle, $style); - $this->readProtectionHidden($docStyle, $style); + $this->readProtectionLocked($docStyle, $style->protection); + $this->readProtectionHidden($docStyle, $style->protection); } // top-level style settings @@ -235,13 +304,20 @@ class Styles extends BaseParserClass /** * Read protection locked attribute. - * - * @param SimpleXMLElement|stdClass $style */ - public function readProtectionLocked(Style $docStyle, $style): void + public function readProtectionLocked(Style $docStyle, SimpleXMLElement $style): void { - if (isset($style->protection['locked'])) { - if (self::boolean((string) $style->protection['locked'])) { + $locked = ''; + if ((string) $style['locked'] !== '') { + $locked = (string) $style['locked']; + } else { + $attr = $this->getStyleAttributes($style); + if (isset($attr['locked'])) { + $locked = (string) $attr['locked']; + } + } + if ($locked !== '') { + if (self::boolean($locked)) { $docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED); } else { $docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED); @@ -251,13 +327,20 @@ class Styles extends BaseParserClass /** * Read protection hidden attribute. - * - * @param SimpleXMLElement|stdClass $style */ - public function readProtectionHidden(Style $docStyle, $style): void + public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style): void { - if (isset($style->protection['hidden'])) { - if (self::boolean((string) $style->protection['hidden'])) { + $hidden = ''; + if ((string) $style['hidden'] !== '') { + $hidden = (string) $style['hidden']; + } else { + $attr = $this->getStyleAttributes($style); + if (isset($attr['hidden'])) { + $hidden = (string) $attr['hidden']; + } + } + if ($hidden !== '') { + if (self::boolean((string) $hidden)) { $docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED); } else { $docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED); @@ -267,15 +350,18 @@ class Styles extends BaseParserClass public function readColor(SimpleXMLElement $color, bool $background = false): string { - if (isset($color['rgb'])) { - return (string) $color['rgb']; - } elseif (isset($color['indexed'])) { - return Color::indexedColor((int) ($color['indexed'] - 7), $background)->getARGB() ?? ''; - } elseif (isset($color['theme'])) { + $attr = $this->getStyleAttributes($color); + if (isset($attr['rgb'])) { + return (string) $attr['rgb']; + } + if (isset($attr['indexed'])) { + return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? ''; + } + if (isset($attr['theme'])) { if ($this->theme !== null) { - $returnColour = $this->theme->getColourByIndex((int) $color['theme']); - if (isset($color['tint'])) { - $tintAdjust = (float) $color['tint']; + $returnColour = $this->theme->getColourByIndex((int) $attr['theme']); + if (isset($attr['tint'])) { + $tintAdjust = (float) $attr['tint']; $returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust); } diff --git a/src/PhpSpreadsheet/Style/Color.php b/src/PhpSpreadsheet/Style/Color.php index c2d4f749..6fd91c64 100644 --- a/src/PhpSpreadsheet/Style/Color.php +++ b/src/PhpSpreadsheet/Style/Color.php @@ -26,10 +26,24 @@ class Color extends Supervisor const COLOR_DARKGREEN = 'FF008000'; const COLOR_YELLOW = 'FFFFFF00'; const COLOR_DARKYELLOW = 'FF808000'; + const COLOR_MAGENTA = 'FFFF00FF'; + const COLOR_CYAN = 'FF00FFFF'; + + const NAMED_COLOR_TRANSLATIONS = [ + 'Black' => self::COLOR_BLACK, + 'White' => self::COLOR_WHITE, + 'Red' => self::COLOR_RED, + 'Green' => self::COLOR_GREEN, + 'Blue' => self::COLOR_BLUE, + 'Yellow' => self::COLOR_YELLOW, + 'Magenta' => self::COLOR_MAGENTA, + 'Cyan' => self::COLOR_CYAN, + ]; const VALIDATE_ARGB_SIZE = 8; const VALIDATE_RGB_SIZE = 6; - const VALIDATE_COLOR_VALUE = '/^[A-F0-9]{%d}$/i'; + const VALIDATE_COLOR_6 = '/^[A-F0-9]{6}$/i'; + const VALIDATE_COLOR_8 = '/^[A-F0-9]{8}$/i'; /** * Indexed colors array. @@ -66,7 +80,7 @@ class Color extends Supervisor // Initialise values if (!$isConditional) { - $this->argb = $this->validateColor($colorValue, self::VALIDATE_ARGB_SIZE) ? $colorValue : self::COLOR_BLACK; + $this->argb = $this->validateColor($colorValue) ?: self::COLOR_BLACK; } } @@ -135,10 +149,23 @@ class Color extends Supervisor return $this; } - private function validateColor(string $colorValue, int $size): bool + private function validateColor(?string $colorValue): string { - return in_array(ucfirst(strtolower($colorValue)), self::NAMED_COLORS) || - preg_match(sprintf(self::VALIDATE_COLOR_VALUE, $size), $colorValue); + if ($colorValue === null || $colorValue === '') { + return self::COLOR_BLACK; + } + $named = ucfirst(strtolower($colorValue)); + if (array_key_exists($named, self::NAMED_COLOR_TRANSLATIONS)) { + return self::NAMED_COLOR_TRANSLATIONS[$named]; + } + if (preg_match(self::VALIDATE_COLOR_8, $colorValue) === 1) { + return $colorValue; + } + if (preg_match(self::VALIDATE_COLOR_6, $colorValue) === 1) { + return 'FF' . $colorValue; + } + + return ''; } /** @@ -163,9 +190,8 @@ class Color extends Supervisor public function setARGB(?string $colorValue = self::COLOR_BLACK) { $this->hasChanged = true; - if ($colorValue === '' || $colorValue === null) { - $colorValue = self::COLOR_BLACK; - } elseif (!$this->validateColor($colorValue, self::VALIDATE_ARGB_SIZE)) { + $colorValue = $this->validateColor($colorValue); + if ($colorValue === '') { return $this; } @@ -200,21 +226,7 @@ class Color extends Supervisor */ public function setRGB(?string $colorValue = self::COLOR_BLACK) { - $this->hasChanged = true; - if ($colorValue === '' || $colorValue === null) { - $colorValue = '000000'; - } elseif (!$this->validateColor($colorValue, self::VALIDATE_RGB_SIZE)) { - return $this; - } - - if ($this->isSupervisor) { - $styleArray = $this->getStyleArray(['argb' => 'FF' . $colorValue]); - $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); - } else { - $this->argb = 'FF' . $colorValue; - } - - return $this; + return $this->setARGB($colorValue); } /** diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index f21efdf0..e58afad4 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -65,7 +65,7 @@ class CommentTest extends TestCase { $comment = new Comment(); $comment->setFillColor(new Color('RED')); - self::assertEquals('RED', $comment->getFillColor()->getARGB()); + self::assertEquals(Color::COLOR_RED, $comment->getFillColor()->getARGB()); } public function testSetAlignment(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2494Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2494Test.php new file mode 100644 index 00000000..ff400264 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2494Test.php @@ -0,0 +1,21 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertTrue($sheet->getCell('C3')->getStyle()->getFont()->getBold()); + self::assertSame('FFBFBFBF', $sheet->getCell('D8')->getStyle()->getFill()->getStartColor()->getArgb()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php index 5002b5d7..7f55c939 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php @@ -77,9 +77,9 @@ class NamespaceNonStdTest extends \PHPUnit\Framework\TestCase $spreadsheet = $reader->load(self::$testbook); $sheet = $spreadsheet->getSheet(0); self::assertEquals('SylkTest', $sheet->getTitle()); - if (strpos(__FILE__, 'NonStd') !== false) { - self::markTestIncomplete('Not yet ready'); - } + //if (strpos(__FILE__, 'NonStd') !== false) { + // self::markTestIncomplete('Not yet ready'); + //} self::assertEquals('FFFF0000', $sheet->getCell('A1')->getStyle()->getFont()->getColor()->getARGB()); self::assertEquals(Fill::FILL_PATTERN_GRAY125, $sheet->getCell('A2')->getStyle()->getFill()->getFillType()); @@ -168,9 +168,9 @@ class NamespaceNonStdTest extends \PHPUnit\Framework\TestCase $spreadsheet = $reader->load(self::$testbook); $sheet = $spreadsheet->getSheet(1); self::assertEquals('Second', $sheet->getTitle()); - if (strpos(__FILE__, 'NonStd') !== false) { - self::markTestIncomplete('Not yet ready'); - } + //if (strpos(__FILE__, 'NonStd') !== false) { + // self::markTestIncomplete('Not yet ready'); + //} self::assertEquals('center', $sheet->getCell('A2')->getStyle()->getAlignment()->getHorizontal()); self::assertSame('inherit', $sheet->getCell('A2')->getStyle()->getProtection()->getLocked()); self::assertEquals('top', $sheet->getCell('A3')->getStyle()->getAlignment()->getVertical()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RichTextTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RichTextTest.php new file mode 100644 index 00000000..8fb8baca --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RichTextTest.php @@ -0,0 +1,53 @@ +getActiveSheet(); + $richText = new RichText(); + $part1 = $richText->createTextRun('Red'); + $font1 = $part1->getFont(); + if ($font1 !== null) { + $font1->setName('Courier New'); + $font1->getColor()->setArgb('FFFF0000'); + } + $part2 = $richText->createTextRun('Blue'); + $font2 = $part2->getFont(); + if ($font2 !== null) { + $font2->setName('Times New Roman'); + $font2->setItalic(true); + $font2->getColor()->setArgb('FF0000FF'); + } + $sheet->setCellValue('A1', $richText); + + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Xlsx'); + $spreadsheetOld->disconnectWorksheets(); + $rsheet = $spreadsheet->getActiveSheet(); + $value = $rsheet->getCell('A1')->getValue(); + if ($value instanceof RichText) { + $elements = $value->getRichTextElements(); + self::assertCount(2, $elements); + $font1a = $elements[0]->getFont(); + $font2a = $elements[1]->getFont(); + self::assertNotNull($font1a); + self::assertNotNull($font2a); + self::assertSame('Courier New', $font1a->getName()); + self::assertSame('FFFF0000', $font1a->getColor()->getArgb()); + self::assertFalse($font1a->getItalic()); + self::assertSame('Times New Roman', $font2a->getName()); + self::assertSame('FF0000FF', $font2a->getColor()->getArgb()); + self::assertTrue($font2a->getItalic()); + } else { + self::fail('Did not see expected RichText'); + } + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/ColorTest.php b/tests/PhpSpreadsheetTests/Style/ColorTest.php index 9d0da3a2..6575c850 100644 --- a/tests/PhpSpreadsheetTests/Style/ColorTest.php +++ b/tests/PhpSpreadsheetTests/Style/ColorTest.php @@ -157,4 +157,31 @@ class ColorTest extends TestCase self::assertEquals(Color::COLOR_BLACK, $color->getARGB()); self::assertEquals('000000', $color->getRGB()); } + + public function testNamedColors(): void + { + $color = new Color(); + $color->setARGB('Blue'); + self::assertEquals(Color::COLOR_BLUE, $color->getARGB()); + $color->setARGB('black'); + self::assertEquals(Color::COLOR_BLACK, $color->getARGB()); + $color->setARGB('wHite'); + self::assertEquals(Color::COLOR_WHITE, $color->getARGB()); + $color->setRGB('reD'); + self::assertEquals(Color::COLOR_RED, $color->getARGB()); + $color->setRGB('GREEN'); + self::assertEquals(Color::COLOR_GREEN, $color->getARGB()); + $color->setRGB('magenta'); + self::assertEquals(Color::COLOR_MAGENTA, $color->getARGB()); + $color->setRGB('YeLlOw'); + self::assertEquals(Color::COLOR_YELLOW, $color->getARGB()); + $color->setRGB('CYAN'); + self::assertEquals(Color::COLOR_CYAN, $color->getARGB()); + $color->setRGB('123456ab'); + self::assertEquals('123456ab', $color->getARGB()); + self::assertEquals('3456ab', $color->getRGB()); + $color->setARGB('3456cd'); + self::assertEquals('FF3456cd', $color->getARGB()); + self::assertEquals('3456cd', $color->getRGB()); + } } diff --git a/tests/data/Reader/XLSX/issue.2494.xlsx b/tests/data/Reader/XLSX/issue.2494.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f9a856faa01de2efa4396f857e723cb5155a759e GIT binary patch literal 21885 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π%T$@btvE#xK8zZ9a4 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

4>Y>vn%rdJMNky2)N2ffrVM3Fy2lIh|# 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