From 5f42638a82c6cf1a49daffade434ab9231bdd24e Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:21:04 -0800 Subject: [PATCH 1/4] Table Borders Fixes Fix #2402. Fix #2474. Both issues deal with borders around tables when they aren't wanted. There are 3 big issues in the code, and several minor ones. First big issue - Word table styles can have both a `styleId` and a `name`, which are often different from each other, and each of which is used by various Word functions, and what documentation I can find is far from clear on the difference. I have added a `tableStyle` property (for styleId) to Style/Table, and the reader will now preserve both `styleId` and `name`. It will similarly preserve `basedOn`, which in now a private property in Style/Paragraph, but is changed to be a protected property in Style. Second big issue - Word2007 Reader assumes that table style can be specified either by name or by inline declarations, but not both. Guess what? It is now changed to support both. This makes the delta for Reader/Word2007/AbstractPart appear to be much more complicated than it actually is. The change is almost entirely of the form: ``` if (condition) {short_code_block} else {long_code_block} ``` to ``` long_code_block ``` Third big issue. In html, td does not inherit border styles from table. In word, cell border styles are specified in table styles (as insideH/V), so they do, in effect, inherit. This is resolved, as best as I can, by having each td/th without its own style use the table border style. So adding an html border style should produce a consistent result in Html and Docx output. Minor issues: - Html table (not css) attribute border=0 should set borderStyle none on all borders; any other value should set borderStyle single. - PhpWord accepts named colors from html styles. According to the documentation that I can find, Word does not recognize those, but, in practice, it often does. Nevertheless, I have added translation to hex (borrowed from PhpSpreadsheet). If nothing else, this will increase interoperability (e.g. RTF doesn't accept named colors, and html 3-hex-digit short forms are now permitted). If a color is not found in the translation table, it will be left unchanged, so there should be no impact. - Writer/Html/Style/Table now accepts colors as 6 hex digits, as well as strings. - The parsing of border css attributes is not accurate. It rejects legitimate values. One example is `2px solid red`, since PhpWord, unlike html, insists on color before style. It rejects `2px #ff0000 solid` because it doesn't accept colors as hex strings. It does not allow the omission of the size and color attributes, but css does. The parsing is rewritten to try to overcome these deficiencies. Note, BTW, that css `border:0` is not acceptable css (size needs a unit and style is omitted); this was mentioned in one of the issues as not being handled correctly, but, since it is invalid, there should be no expectation of its being handed in any particular way. - Style/Border::hasBorder is expanded to test all of Size, Color, and Style, rather than limiting its test to Size. - Properties insideHStyle and insideVStyle are added to Style/Table. Their Color and Size equivalents already existed. - If border is not specified as an Html or css attribute on a table, it is not the same as specifying html border=0 or css border:none. The end result will be whatever the app that reads the result defaults to. The results may not be consistent between, say, Html and Docx. This is already addressed in part by setting default styling for table and td in the html head section to match the Word defaults. However, there may still be differences; the way to (mostly) avoid them is to specify a table style. --- phpstan-baseline.neon | 5 - src/PhpWord/Reader/Word2007/AbstractPart.php | 65 ++- src/PhpWord/Reader/Word2007/Styles.php | 6 +- src/PhpWord/Shared/Html.php | 83 ++- src/PhpWord/Shared/HtmlColours.php | 549 ++++++++++++++++++ src/PhpWord/Style/AbstractStyle.php | 15 + src/PhpWord/Style/Border.php | 8 + src/PhpWord/Style/Paragraph.php | 26 +- src/PhpWord/Style/Table.php | 113 ++++ src/PhpWord/Writer/HTML/Element/Table.php | 5 +- src/PhpWord/Writer/HTML/Part/Head.php | 3 + src/PhpWord/Writer/HTML/Style/Table.php | 6 +- src/PhpWord/Writer/Word2007/Part/Styles.php | 18 +- .../Writer/Word2007/Style/MarginBorder.php | 18 +- src/PhpWord/Writer/Word2007/Style/Table.php | 3 + .../Reader/Word2007/StyleTableTest.php | 55 ++ tests/PhpWordTests/Shared/Html2402Test.php | 207 +++++++ tests/PhpWordTests/Shared/HtmlTest.php | 4 +- .../Writer/ODText/Part/ContentTest.php | 10 + .../_files/documents/word.2474.docx | Bin 0 -> 27593 bytes 20 files changed, 1087 insertions(+), 112 deletions(-) create mode 100644 src/PhpWord/Shared/HtmlColours.php create mode 100644 tests/PhpWordTests/Reader/Word2007/StyleTableTest.php create mode 100644 tests/PhpWordTests/Shared/Html2402Test.php create mode 100644 tests/PhpWordTests/_files/documents/word.2474.docx diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e7918d9174..c5541c9ddd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -405,11 +405,6 @@ parameters: count: 1 path: src/PhpWord/Shared/Html.php - - - message: "#^Cannot call method setBorderSize\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#" - count: 1 - path: src/PhpWord/Shared/Html.php - - message: "#^Cannot call method setStyleName\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#" count: 1 diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 95799387ed..a92e6d5958 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -592,35 +592,46 @@ protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode) $borders = array_merge($margins, ['insideH', 'insideV']); if ($xmlReader->elementExists('w:tblPr', $domNode)) { + $tblStyleName = ''; if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) { - $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle'); - } else { - $styleNode = $xmlReader->getElement('w:tblPr', $domNode); - $styleDefs = []; - foreach ($margins as $side) { - $ucfSide = ucfirst($side); - $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w']; - } - foreach ($borders as $side) { - $ucfSide = ucfirst($side); - $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz']; - $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color']; - $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val']; - } - $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type']; - $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual']; - $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w']; - $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); - - $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode); - if ($tablePositionNode !== null) { - $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode); - } + $tblStyleName = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle'); + } + $styleNode = $xmlReader->getElement('w:tblPr', $domNode); + $styleDefs = []; - $indentNode = $xmlReader->getElement('w:tblInd', $styleNode); - if ($indentNode !== null) { - $style['indent'] = $this->readTableIndent($xmlReader, $indentNode); - } + foreach ($margins as $side) { + $ucfSide = ucfirst($side); + $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w']; + } + foreach ($borders as $side) { + $ucfSide = ucfirst($side); + $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz']; + $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color']; + $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val']; + } + $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type']; + $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual']; + $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w']; + $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); + + $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode); + if ($tablePositionNode !== null) { + $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode); + } + + $indentNode = $xmlReader->getElement('w:tblInd', $styleNode); + if ($indentNode !== null) { + $style['indent'] = $this->readTableIndent($xmlReader, $indentNode); + } + if ($xmlReader->elementExists('w:basedOn', $domNode)) { + $style['basedOn'] = $xmlReader->getAttribute('w:val', $domNode, 'w:basedOn'); + } + if ($tblStyleName !== '') { + $style['tblStyle'] = $tblStyleName; + } + // this may be unneeded + if ($xmlReader->elementExists('w:name', $domNode)) { + $style['styleName'] = $xmlReader->getAttribute('w:val', $domNode, 'w:name'); } } diff --git a/src/PhpWord/Reader/Word2007/Styles.php b/src/PhpWord/Reader/Word2007/Styles.php index 4566398ad2..a6fd23dd43 100644 --- a/src/PhpWord/Reader/Word2007/Styles.php +++ b/src/PhpWord/Reader/Word2007/Styles.php @@ -63,8 +63,9 @@ public function read(PhpWord $phpWord): void foreach ($nodes as $node) { $type = $xmlReader->getAttribute('w:type', $node); $name = $xmlReader->getAttribute('w:val', $node, 'w:name'); + $styleId = $xmlReader->getAttribute('w:styleId', $node); if (null === $name) { - $name = $xmlReader->getAttribute('w:styleId', $node); + $name = $styleId; } $headingMatches = []; preg_match('/Heading\s*(\d)/i', $name, $headingMatches); @@ -96,7 +97,8 @@ public function read(PhpWord $phpWord): void case 'table': $tStyle = $this->readTableStyle($xmlReader, $node); if (!empty($tStyle)) { - $phpWord->addTableStyle($name, $tStyle); + $newTable = $phpWord->addTableStyle($styleId, $tStyle); + $newTable->setStyleName($name); } break; diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 0a9b23979c..d58871e4a2 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -26,6 +26,7 @@ use PhpOffice\PhpWord\Element\Row; use PhpOffice\PhpWord\Element\Table; use PhpOffice\PhpWord\Settings; +use PhpOffice\PhpWord\SimpleType\Border; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\NumberFormat; use PhpOffice\PhpWord\Style\Paragraph; @@ -37,6 +38,8 @@ */ class Html { + private const SPECIAL_BORDER_WIDTHS = ['thin' => '0.5pt', 'thick' => '3.5pt', 'medium' => '2.0pt']; + protected static $listIndex = 0; protected static $xpath; @@ -142,7 +145,7 @@ protected static function parseInlineStyle($node, $styles = []) break; case 'bgcolor': // tables, rows, cells e.g. - $styles['bgColor'] = trim($val, '# '); + HtmlColours::setArrayColour($styles, 'bgColor', $val); break; case 'valign': @@ -421,9 +424,10 @@ protected static function parseTable($node, $element, &$styles) } $attributes = $node->attributes; - if ($attributes->getNamedItem('border') !== null) { + if ($attributes->getNamedItem('border') !== null && is_object($newElement->getStyle())) { $border = (int) $attributes->getNamedItem('border')->value; - $newElement->getStyle()->setBorderSize(Converter::pixelToTwip($border)); + $newElement->getStyle()->setBorderSize((int) Converter::pixelToTwip($border)); + $newElement->getStyle()->setBorderStyle(($border === 0) ? 'none' : 'single'); } return $newElement; @@ -720,11 +724,11 @@ protected static function parseStyleDeclarations(array $selectors, array $styles break; case 'color': - $styles['color'] = trim($value, '#'); + HtmlColours::setArrayColour($styles, 'color', $value); break; case 'background-color': - $styles['bgColor'] = trim($value, '#'); + HtmlColours::setArrayColour($styles, 'bgColor', $value); break; case 'line-height': @@ -804,7 +808,7 @@ protected static function parseStyleDeclarations(array $selectors, array $styles break; case 'border-width': - $styles['borderSize'] = Converter::cssToPoint($value); + $styles['borderSize'] = Converter::cssToPoint(self::SPECIAL_BORDER_WIDTHS[$value] ?? $value); break; case 'border-style': @@ -834,29 +838,46 @@ protected static function parseStyleDeclarations(array $selectors, array $styles case 'border-bottom': case 'border-right': case 'border-left': - // must have exact order [width color style], e.g. "1px #0011CC solid" or "2pt green solid" - // Word does not accept shortened hex colors e.g. #CCC, only full e.g. #CCCCCC - if (preg_match('/([0-9]+[^0-9]*)\s+(\#[a-fA-F0-9]+|[a-zA-Z]+)\s+([a-z]+)/', $value, $matches)) { - if (false !== strpos($property, '-')) { - $tmp = explode('-', $property); - $which = $tmp[1]; - $which = ucfirst($which); // e.g. bottom -> Bottom - } else { - $which = ''; - } - // Note - border width normalization: - // Width of border in Word is calculated differently than HTML borders, usually showing up too bold. - // Smallest 1px (or 1pt) appears in Word like 2-3px/pt in HTML once converted to twips. - // Therefore we need to normalize converted twip value to cca 1/2 of value. - // This may be adjusted, if better ratio or formula found. - // BC change: up to ver. 0.17.0 was $size converted to points - Converter::cssToPoint($size) - $size = Converter::cssToTwip($matches[1]); + $stylePattern = '/(^|\\s)(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)(\\s|$)/'; + if (!preg_match($stylePattern, $value, $matches)) { + break; + } + $borderStyle = $matches[2]; + $value = preg_replace($stylePattern, ' ', $value) ?? ''; + $borderSize = $borderColor = null; + $sizePattern = '/(^|\\s)([0-9]+([.][0-9]+)?+(%|[a-z]*)|thick|thin|medium)(\\s|$)/'; + if (preg_match($sizePattern, $value, $matches)) { + $borderSize = $matches[2]; + $borderSize = self::SPECIAL_BORDER_WIDTHS[$borderSize] ?? $borderSize; + $value = preg_replace($sizePattern, ' ', $value) ?? ''; + } + $colorPattern = '/(^|\\s)([#][a-fA-F0-9]{6}|[#][a-fA-F0-9]{3}|[a-z][a-z0-9]+)(\\s|$)/'; + if (preg_match($colorPattern, $value, $matches)) { + $borderColor = HtmlColours::convertColour($matches[2]); + } + if (false !== strpos($property, '-')) { + $tmp = explode('-', $property); + $which = $tmp[1]; + $which = ucfirst($which); // e.g. bottom -> Bottom + } else { + $which = ''; + } + // Note - border width normalization: + // Width of border in Word is calculated differently than HTML borders, usually showing up too bold. + // Smallest 1px (or 1pt) appears in Word like 2-3px/pt in HTML once converted to twips. + // Therefore we need to normalize converted twip value to cca 1/2 of value. + // This may be adjusted, if better ratio or formula found. + // BC change: up to ver. 0.17.0 was $size converted to points - Converter::cssToPoint($size) + if ($borderSize !== null) { + $size = Converter::cssToTwip($borderSize); $size = (int) ($size / 2); // valid variants may be e.g. borderSize, borderTopSize, borderLeftColor, etc .. $styles["border{$which}Size"] = $size; // twips - $styles["border{$which}Color"] = trim($matches[2], '#'); - $styles["border{$which}Style"] = self::mapBorderStyle($matches[3]); } + if (!empty($borderColor)) { + $styles["border{$which}Color"] = $borderColor; + } + $styles["border{$which}Style"] = self::mapBorderStyle($borderStyle); break; case 'vertical-align': @@ -1006,6 +1027,8 @@ protected static function mapBorderStyle($cssBorderStyle) case 'dotted': case 'double': return $cssBorderStyle; + case 'hidden': + return 'none'; default: return 'single'; } @@ -1013,14 +1036,14 @@ protected static function mapBorderStyle($cssBorderStyle) protected static function mapBorderColor(&$styles, $cssBorderColor): void { - $numColors = substr_count($cssBorderColor, '#'); + $colors = explode(' ', $cssBorderColor); + $numColors = count($colors); if ($numColors === 1) { - $styles['borderColor'] = trim($cssBorderColor, '#'); - } elseif ($numColors > 1) { - $colors = explode(' ', $cssBorderColor); + HtmlColours::setArrayColour($styles, 'borderColor', $cssBorderColor); + } else { $borders = ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor']; for ($i = 0; $i < min(4, $numColors, count($colors)); ++$i) { - $styles[$borders[$i]] = trim($colors[$i], '#'); + HtmlColours::setArrayColour($styles, $borders[$i], $colors[$i]); } } } diff --git a/src/PhpWord/Shared/HtmlColours.php b/src/PhpWord/Shared/HtmlColours.php new file mode 100644 index 0000000000..40bc0096c6 --- /dev/null +++ b/src/PhpWord/Shared/HtmlColours.php @@ -0,0 +1,549 @@ + 'f0f8ff', + 'antiquewhite' => 'faebd7', + 'antiquewhite1' => 'ffefdb', + 'antiquewhite2' => 'eedfcc', + 'antiquewhite3' => 'cdc0b0', + 'antiquewhite4' => '8b8378', + 'aqua' => '00ffff', + 'aquamarine1' => '7fffd4', + 'aquamarine2' => '76eec6', + 'aquamarine4' => '458b74', + 'azure1' => 'f0ffff', + 'azure2' => 'e0eeee', + 'azure3' => 'c1cdcd', + 'azure4' => '838b8b', + 'beige' => 'f5f5dc', + 'bisque1' => 'ffe4c4', + 'bisque2' => 'eed5b7', + 'bisque3' => 'cdb79e', + 'bisque4' => '8b7d6b', + 'black' => '000000', + 'blanchedalmond' => 'ffebcd', + 'blue' => '0000ff', + 'blue1' => '0000ff', + 'blue2' => '0000ee', + 'blue4' => '00008b', + 'blueviolet' => '8a2be2', + 'brown' => 'a52a2a', + 'brown1' => 'ff4040', + 'brown2' => 'ee3b3b', + 'brown3' => 'cd3333', + 'brown4' => '8b2323', + 'burlywood' => 'deb887', + 'burlywood1' => 'ffd39b', + 'burlywood2' => 'eec591', + 'burlywood3' => 'cdaa7d', + 'burlywood4' => '8b7355', + 'cadetblue' => '5f9ea0', + 'cadetblue1' => '98f5ff', + 'cadetblue2' => '8ee5ee', + 'cadetblue3' => '7ac5cd', + 'cadetblue4' => '53868b', + 'chartreuse1' => '7fff00', + 'chartreuse2' => '76ee00', + 'chartreuse3' => '66cd00', + 'chartreuse4' => '458b00', + 'chocolate' => 'd2691e', + 'chocolate1' => 'ff7f24', + 'chocolate2' => 'ee7621', + 'chocolate3' => 'cd661d', + 'coral' => 'ff7f50', + 'coral1' => 'ff7256', + 'coral2' => 'ee6a50', + 'coral3' => 'cd5b45', + 'coral4' => '8b3e2f', + 'cornflowerblue' => '6495ed', + 'cornsilk1' => 'fff8dc', + 'cornsilk2' => 'eee8cd', + 'cornsilk3' => 'cdc8b1', + 'cornsilk4' => '8b8878', + 'cyan1' => '00ffff', + 'cyan2' => '00eeee', + 'cyan3' => '00cdcd', + 'cyan4' => '008b8b', + 'darkgoldenrod' => 'b8860b', + 'darkgoldenrod1' => 'ffb90f', + 'darkgoldenrod2' => 'eead0e', + 'darkgoldenrod3' => 'cd950c', + 'darkgoldenrod4' => '8b6508', + 'darkgreen' => '006400', + 'darkkhaki' => 'bdb76b', + 'darkolivegreen' => '556b2f', + 'darkolivegreen1' => 'caff70', + 'darkolivegreen2' => 'bcee68', + 'darkolivegreen3' => 'a2cd5a', + 'darkolivegreen4' => '6e8b3d', + 'darkorange' => 'ff8c00', + 'darkorange1' => 'ff7f00', + 'darkorange2' => 'ee7600', + 'darkorange3' => 'cd6600', + 'darkorange4' => '8b4500', + 'darkorchid' => '9932cc', + 'darkorchid1' => 'bf3eff', + 'darkorchid2' => 'b23aee', + 'darkorchid3' => '9a32cd', + 'darkorchid4' => '68228b', + 'darksalmon' => 'e9967a', + 'darkseagreen' => '8fbc8f', + 'darkseagreen1' => 'c1ffc1', + 'darkseagreen2' => 'b4eeb4', + 'darkseagreen3' => '9bcd9b', + 'darkseagreen4' => '698b69', + 'darkslateblue' => '483d8b', + 'darkslategray' => '2f4f4f', + 'darkslategray1' => '97ffff', + 'darkslategray2' => '8deeee', + 'darkslategray3' => '79cdcd', + 'darkslategray4' => '528b8b', + 'darkturquoise' => '00ced1', + 'darkviolet' => '9400d3', + 'deeppink1' => 'ff1493', + 'deeppink2' => 'ee1289', + 'deeppink3' => 'cd1076', + 'deeppink4' => '8b0a50', + 'deepskyblue1' => '00bfff', + 'deepskyblue2' => '00b2ee', + 'deepskyblue3' => '009acd', + 'deepskyblue4' => '00688b', + 'dimgray' => '696969', + 'dodgerblue1' => '1e90ff', + 'dodgerblue2' => '1c86ee', + 'dodgerblue3' => '1874cd', + 'dodgerblue4' => '104e8b', + 'firebrick' => 'b22222', + 'firebrick1' => 'ff3030', + 'firebrick2' => 'ee2c2c', + 'firebrick3' => 'cd2626', + 'firebrick4' => '8b1a1a', + 'floralwhite' => 'fffaf0', + 'forestgreen' => '228b22', + 'fuchsia' => 'ff00ff', + 'gainsboro' => 'dcdcdc', + 'ghostwhite' => 'f8f8ff', + 'gold1' => 'ffd700', + 'gold2' => 'eec900', + 'gold3' => 'cdad00', + 'gold4' => '8b7500', + 'goldenrod' => 'daa520', + 'goldenrod1' => 'ffc125', + 'goldenrod2' => 'eeb422', + 'goldenrod3' => 'cd9b1d', + 'goldenrod4' => '8b6914', + 'gray' => 'bebebe', + 'gray1' => '030303', + 'gray10' => '1a1a1a', + 'gray11' => '1c1c1c', + 'gray12' => '1f1f1f', + 'gray13' => '212121', + 'gray14' => '242424', + 'gray15' => '262626', + 'gray16' => '292929', + 'gray17' => '2b2b2b', + 'gray18' => '2e2e2e', + 'gray19' => '303030', + 'gray2' => '050505', + 'gray20' => '333333', + 'gray21' => '363636', + 'gray22' => '383838', + 'gray23' => '3b3b3b', + 'gray24' => '3d3d3d', + 'gray25' => '404040', + 'gray26' => '424242', + 'gray27' => '454545', + 'gray28' => '474747', + 'gray29' => '4a4a4a', + 'gray3' => '080808', + 'gray30' => '4d4d4d', + 'gray31' => '4f4f4f', + 'gray32' => '525252', + 'gray33' => '545454', + 'gray34' => '575757', + 'gray35' => '595959', + 'gray36' => '5c5c5c', + 'gray37' => '5e5e5e', + 'gray38' => '616161', + 'gray39' => '636363', + 'gray4' => '0a0a0a', + 'gray40' => '666666', + 'gray41' => '696969', + 'gray42' => '6b6b6b', + 'gray43' => '6e6e6e', + 'gray44' => '707070', + 'gray45' => '737373', + 'gray46' => '757575', + 'gray47' => '787878', + 'gray48' => '7a7a7a', + 'gray49' => '7d7d7d', + 'gray5' => '0d0d0d', + 'gray50' => '7f7f7f', + 'gray51' => '828282', + 'gray52' => '858585', + 'gray53' => '878787', + 'gray54' => '8a8a8a', + 'gray55' => '8c8c8c', + 'gray56' => '8f8f8f', + 'gray57' => '919191', + 'gray58' => '949494', + 'gray59' => '969696', + 'gray6' => '0f0f0f', + 'gray60' => '999999', + 'gray61' => '9c9c9c', + 'gray62' => '9e9e9e', + 'gray63' => 'a1a1a1', + 'gray64' => 'a3a3a3', + 'gray65' => 'a6a6a6', + 'gray66' => 'a8a8a8', + 'gray67' => 'ababab', + 'gray68' => 'adadad', + 'gray69' => 'b0b0b0', + 'gray7' => '121212', + 'gray70' => 'b3b3b3', + 'gray71' => 'b5b5b5', + 'gray72' => 'b8b8b8', + 'gray73' => 'bababa', + 'gray74' => 'bdbdbd', + 'gray75' => 'bfbfbf', + 'gray76' => 'c2c2c2', + 'gray77' => 'c4c4c4', + 'gray78' => 'c7c7c7', + 'gray79' => 'c9c9c9', + 'gray8' => '141414', + 'gray80' => 'cccccc', + 'gray81' => 'cfcfcf', + 'gray82' => 'd1d1d1', + 'gray83' => 'd4d4d4', + 'gray84' => 'd6d6d6', + 'gray85' => 'd9d9d9', + 'gray86' => 'dbdbdb', + 'gray87' => 'dedede', + 'gray88' => 'e0e0e0', + 'gray89' => 'e3e3e3', + 'gray9' => '171717', + 'gray90' => 'e5e5e5', + 'gray91' => 'e8e8e8', + 'gray92' => 'ebebeb', + 'gray93' => 'ededed', + 'gray94' => 'f0f0f0', + 'gray95' => 'f2f2f2', + 'gray97' => 'f7f7f7', + 'gray98' => 'fafafa', + 'gray99' => 'fcfcfc', + 'green' => '00ff00', + 'green1' => '00ff00', + 'green2' => '00ee00', + 'green3' => '00cd00', + 'green4' => '008b00', + 'greenyellow' => 'adff2f', + 'honeydew1' => 'f0fff0', + 'honeydew2' => 'e0eee0', + 'honeydew3' => 'c1cdc1', + 'honeydew4' => '838b83', + 'hotpink' => 'ff69b4', + 'hotpink1' => 'ff6eb4', + 'hotpink2' => 'ee6aa7', + 'hotpink3' => 'cd6090', + 'hotpink4' => '8b3a62', + 'indianred' => 'cd5c5c', + 'indianred1' => 'ff6a6a', + 'indianred2' => 'ee6363', + 'indianred3' => 'cd5555', + 'indianred4' => '8b3a3a', + 'ivory1' => 'fffff0', + 'ivory2' => 'eeeee0', + 'ivory3' => 'cdcdc1', + 'ivory4' => '8b8b83', + 'khaki' => 'f0e68c', + 'khaki1' => 'fff68f', + 'khaki2' => 'eee685', + 'khaki3' => 'cdc673', + 'khaki4' => '8b864e', + 'lavender' => 'e6e6fa', + 'lavenderblush1' => 'fff0f5', + 'lavenderblush2' => 'eee0e5', + 'lavenderblush3' => 'cdc1c5', + 'lavenderblush4' => '8b8386', + 'lawngreen' => '7cfc00', + 'lemonchiffon1' => 'fffacd', + 'lemonchiffon2' => 'eee9bf', + 'lemonchiffon3' => 'cdc9a5', + 'lemonchiffon4' => '8b8970', + 'light' => 'eedd82', + 'lightblue' => 'add8e6', + 'lightblue1' => 'bfefff', + 'lightblue2' => 'b2dfee', + 'lightblue3' => '9ac0cd', + 'lightblue4' => '68838b', + 'lightcoral' => 'f08080', + 'lightcyan1' => 'e0ffff', + 'lightcyan2' => 'd1eeee', + 'lightcyan3' => 'b4cdcd', + 'lightcyan4' => '7a8b8b', + 'lightgoldenrod1' => 'ffec8b', + 'lightgoldenrod2' => 'eedc82', + 'lightgoldenrod3' => 'cdbe70', + 'lightgoldenrod4' => '8b814c', + 'lightgoldenrodyellow' => 'fafad2', + 'lightgray' => 'd3d3d3', + 'lightpink' => 'ffb6c1', + 'lightpink1' => 'ffaeb9', + 'lightpink2' => 'eea2ad', + 'lightpink3' => 'cd8c95', + 'lightpink4' => '8b5f65', + 'lightsalmon1' => 'ffa07a', + 'lightsalmon2' => 'ee9572', + 'lightsalmon3' => 'cd8162', + 'lightsalmon4' => '8b5742', + 'lightseagreen' => '20b2aa', + 'lightskyblue' => '87cefa', + 'lightskyblue1' => 'b0e2ff', + 'lightskyblue2' => 'a4d3ee', + 'lightskyblue3' => '8db6cd', + 'lightskyblue4' => '607b8b', + 'lightslateblue' => '8470ff', + 'lightslategray' => '778899', + 'lightsteelblue' => 'b0c4de', + 'lightsteelblue1' => 'cae1ff', + 'lightsteelblue2' => 'bcd2ee', + 'lightsteelblue3' => 'a2b5cd', + 'lightsteelblue4' => '6e7b8b', + 'lightyellow1' => 'ffffe0', + 'lightyellow2' => 'eeeed1', + 'lightyellow3' => 'cdcdb4', + 'lightyellow4' => '8b8b7a', + 'lime' => '00ff00', + 'limegreen' => '32cd32', + 'linen' => 'faf0e6', + 'magenta' => 'ff00ff', + 'magenta2' => 'ee00ee', + 'magenta3' => 'cd00cd', + 'magenta4' => '8b008b', + 'maroon' => 'b03060', + 'maroon1' => 'ff34b3', + 'maroon2' => 'ee30a7', + 'maroon3' => 'cd2990', + 'maroon4' => '8b1c62', + 'medium' => '66cdaa', + 'mediumaquamarine' => '66cdaa', + 'mediumblue' => '0000cd', + 'mediumorchid' => 'ba55d3', + 'mediumorchid1' => 'e066ff', + 'mediumorchid2' => 'd15fee', + 'mediumorchid3' => 'b452cd', + 'mediumorchid4' => '7a378b', + 'mediumpurple' => '9370db', + 'mediumpurple1' => 'ab82ff', + 'mediumpurple2' => '9f79ee', + 'mediumpurple3' => '8968cd', + 'mediumpurple4' => '5d478b', + 'mediumseagreen' => '3cb371', + 'mediumslateblue' => '7b68ee', + 'mediumspringgreen' => '00fa9a', + 'mediumturquoise' => '48d1cc', + 'mediumvioletred' => 'c71585', + 'midnightblue' => '191970', + 'mintcream' => 'f5fffa', + 'mistyrose1' => 'ffe4e1', + 'mistyrose2' => 'eed5d2', + 'mistyrose3' => 'cdb7b5', + 'mistyrose4' => '8b7d7b', + 'moccasin' => 'ffe4b5', + 'navajowhite1' => 'ffdead', + 'navajowhite2' => 'eecfa1', + 'navajowhite3' => 'cdb38b', + 'navajowhite4' => '8b795e', + 'navy' => '000080', + 'navyblue' => '000080', + 'oldlace' => 'fdf5e6', + 'olive' => '808000', + 'olivedrab' => '6b8e23', + 'olivedrab1' => 'c0ff3e', + 'olivedrab2' => 'b3ee3a', + 'olivedrab4' => '698b22', + 'orange' => 'ffa500', + 'orange1' => 'ffa500', + 'orange2' => 'ee9a00', + 'orange3' => 'cd8500', + 'orange4' => '8b5a00', + 'orangered1' => 'ff4500', + 'orangered2' => 'ee4000', + 'orangered3' => 'cd3700', + 'orangered4' => '8b2500', + 'orchid' => 'da70d6', + 'orchid1' => 'ff83fa', + 'orchid2' => 'ee7ae9', + 'orchid3' => 'cd69c9', + 'orchid4' => '8b4789', + 'pale' => 'db7093', + 'palegoldenrod' => 'eee8aa', + 'palegreen' => '98fb98', + 'palegreen1' => '9aff9a', + 'palegreen2' => '90ee90', + 'palegreen3' => '7ccd7c', + 'palegreen4' => '548b54', + 'paleturquoise' => 'afeeee', + 'paleturquoise1' => 'bbffff', + 'paleturquoise2' => 'aeeeee', + 'paleturquoise3' => '96cdcd', + 'paleturquoise4' => '668b8b', + 'palevioletred' => 'db7093', + 'palevioletred1' => 'ff82ab', + 'palevioletred2' => 'ee799f', + 'palevioletred3' => 'cd6889', + 'palevioletred4' => '8b475d', + 'papayawhip' => 'ffefd5', + 'peachpuff1' => 'ffdab9', + 'peachpuff2' => 'eecbad', + 'peachpuff3' => 'cdaf95', + 'peachpuff4' => '8b7765', + 'pink' => 'ffc0cb', + 'pink1' => 'ffb5c5', + 'pink2' => 'eea9b8', + 'pink3' => 'cd919e', + 'pink4' => '8b636c', + 'plum' => 'dda0dd', + 'plum1' => 'ffbbff', + 'plum2' => 'eeaeee', + 'plum3' => 'cd96cd', + 'plum4' => '8b668b', + 'powderblue' => 'b0e0e6', + 'purple' => 'a020f0', + 'rebeccapurple' => '663399', + 'purple1' => '9b30ff', + 'purple2' => '912cee', + 'purple3' => '7d26cd', + 'purple4' => '551a8b', + 'red' => 'ff0000', + 'red1' => 'ff0000', + 'red2' => 'ee0000', + 'red3' => 'cd0000', + 'red4' => '8b0000', + 'rosybrown' => 'bc8f8f', + 'rosybrown1' => 'ffc1c1', + 'rosybrown2' => 'eeb4b4', + 'rosybrown3' => 'cd9b9b', + 'rosybrown4' => '8b6969', + 'royalblue' => '4169e1', + 'royalblue1' => '4876ff', + 'royalblue2' => '436eee', + 'royalblue3' => '3a5fcd', + 'royalblue4' => '27408b', + 'saddlebrown' => '8b4513', + 'salmon' => 'fa8072', + 'salmon1' => 'ff8c69', + 'salmon2' => 'ee8262', + 'salmon3' => 'cd7054', + 'salmon4' => '8b4c39', + 'sandybrown' => 'f4a460', + 'seagreen1' => '54ff9f', + 'seagreen2' => '4eee94', + 'seagreen3' => '43cd80', + 'seagreen4' => '2e8b57', + 'seashell1' => 'fff5ee', + 'seashell2' => 'eee5de', + 'seashell3' => 'cdc5bf', + 'seashell4' => '8b8682', + 'sienna' => 'a0522d', + 'sienna1' => 'ff8247', + 'sienna2' => 'ee7942', + 'sienna3' => 'cd6839', + 'sienna4' => '8b4726', + 'silver' => 'c0c0c0', + 'skyblue' => '87ceeb', + 'skyblue1' => '87ceff', + 'skyblue2' => '7ec0ee', + 'skyblue3' => '6ca6cd', + 'skyblue4' => '4a708b', + 'slateblue' => '6a5acd', + 'slateblue1' => '836fff', + 'slateblue2' => '7a67ee', + 'slateblue3' => '6959cd', + 'slateblue4' => '473c8b', + 'slategray' => '708090', + 'slategray1' => 'c6e2ff', + 'slategray2' => 'b9d3ee', + 'slategray3' => '9fb6cd', + 'slategray4' => '6c7b8b', + 'snow1' => 'fffafa', + 'snow2' => 'eee9e9', + 'snow3' => 'cdc9c9', + 'snow4' => '8b8989', + 'springgreen1' => '00ff7f', + 'springgreen2' => '00ee76', + 'springgreen3' => '00cd66', + 'springgreen4' => '008b45', + 'steelblue' => '4682b4', + 'steelblue1' => '63b8ff', + 'steelblue2' => '5cacee', + 'steelblue3' => '4f94cd', + 'steelblue4' => '36648b', + 'tan' => 'd2b48c', + 'tan1' => 'ffa54f', + 'tan2' => 'ee9a49', + 'tan3' => 'cd853f', + 'tan4' => '8b5a2b', + 'teal' => '008080', + 'thistle' => 'd8bfd8', + 'thistle1' => 'ffe1ff', + 'thistle2' => 'eed2ee', + 'thistle3' => 'cdb5cd', + 'thistle4' => '8b7b8b', + 'tomato1' => 'ff6347', + 'tomato2' => 'ee5c42', + 'tomato3' => 'cd4f39', + 'tomato4' => '8b3626', + 'turquoise' => '40e0d0', + 'turquoise1' => '00f5ff', + 'turquoise2' => '00e5ee', + 'turquoise3' => '00c5cd', + 'turquoise4' => '00868b', + 'violet' => 'ee82ee', + 'violetred' => 'd02090', + 'violetred1' => 'ff3e96', + 'violetred2' => 'ee3a8c', + 'violetred3' => 'cd3278', + 'violetred4' => '8b2252', + 'wheat' => 'f5deb3', + 'wheat1' => 'ffe7ba', + 'wheat2' => 'eed8ae', + 'wheat3' => 'cdba96', + 'wheat4' => '8b7e66', + 'white' => 'ffffff', + 'whitesmoke' => 'f5f5f5', + 'yellow' => 'ffff00', + 'yellow1' => 'ffff00', + 'yellow2' => 'eeee00', + 'yellow3' => 'cdcd00', + 'yellow4' => '8b8b00', + 'yellowgreen' => '9acd32', + ]; + + public static function colourNameLookup(string $colorName): string + { + return self::COLOUR_MAP[$colorName] ?? ''; + } + + public static function convertColour(string $colorName): string + { + $colorName = trim($colorName); + if (preg_match('/^[#][a-fA-F0-9]{6}$/', $colorName) === 1) { + return substr($colorName, 1); + } + if (preg_match('/^[#][a-fA-F0-9]{3}$/', $colorName) === 1) { + return "{$colorName[1]}{$colorName[1]}{$colorName[2]}{$colorName[2]}{$colorName[3]}{$colorName[3]}"; + } + + return self::COLOUR_MAP[$colorName] ?? $colorName; + } + + public static function setArrayColour(array &$array, string $index, string $colorName): void + { + $array[$index] = self::convertColour($colorName); + } +} diff --git a/src/PhpWord/Style/AbstractStyle.php b/src/PhpWord/Style/AbstractStyle.php index 4e5def618d..1fbcdcd3d4 100644 --- a/src/PhpWord/Style/AbstractStyle.php +++ b/src/PhpWord/Style/AbstractStyle.php @@ -50,6 +50,9 @@ abstract class AbstractStyle */ protected $aliases = []; + /** @var string */ + protected $basedOn = ''; + /** * Is this an automatic style? (Used primarily in OpenDocument driver). * @@ -83,6 +86,18 @@ public function setStyleName($value) return $this; } + public function getBasedOn(): string + { + return $this->basedOn; + } + + public function setBasedOn(string $value): self + { + $this->basedOn = $value; + + return $this; + } + /** * Get index number. * diff --git a/src/PhpWord/Style/Border.php b/src/PhpWord/Style/Border.php index 28e340c040..8be7298840 100644 --- a/src/PhpWord/Style/Border.php +++ b/src/PhpWord/Style/Border.php @@ -528,6 +528,14 @@ public function setBorderBottomStyle($value = null) public function hasBorder() { $borders = $this->getBorderSize(); + if ($borders !== array_filter($borders, 'is_null')) { + return true; + } + $borders = $this->getBorderColor(); + if ($borders !== array_filter($borders, 'is_null')) { + return true; + } + $borders = $this->getBorderStyle(); return $borders !== array_filter($borders, 'is_null'); } diff --git a/src/PhpWord/Style/Paragraph.php b/src/PhpWord/Style/Paragraph.php index c77617403d..e5932d3774 100644 --- a/src/PhpWord/Style/Paragraph.php +++ b/src/PhpWord/Style/Paragraph.php @@ -69,7 +69,7 @@ class Paragraph extends Border * * @var string */ - private $basedOn = 'Normal'; + protected $basedOn = 'Normal'; /** * Style for next paragraph. @@ -273,30 +273,6 @@ public function setAlignment($value) return $this; } - /** - * Get parent style ID. - * - * @return string - */ - public function getBasedOn() - { - return $this->basedOn; - } - - /** - * Set parent style ID. - * - * @param string $value - * - * @return self - */ - public function setBasedOn($value = 'Normal') - { - $this->basedOn = $value; - - return $this; - } - /** * Get style for next paragraph. * diff --git a/src/PhpWord/Style/Table.php b/src/PhpWord/Style/Table.php index 3adb1a38f5..0b5bb14f54 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -110,6 +110,20 @@ class Table extends Border */ private $borderInsideVColor; + /** + * Border style inside horizontal. + * + * @var string + */ + protected $borderInsideHStyle = ''; + + /** + * Border style inside vertical. + * + * @var string + */ + protected $borderInsideVStyle = ''; + /** * Shading. * @@ -168,6 +182,9 @@ class Table extends Border */ private $bidiVisual; + /** @var string */ + private $tblStyle = ''; + /** * Create new table style. * @@ -260,6 +277,42 @@ public function getBorderSize() ]; } + /** + * Get border style. + * + * @return string[] + */ + public function getBorderStyle() + { + return [ + $this->getBorderTopStyle(), + $this->getBorderLeftStyle(), + $this->getBorderRightStyle(), + $this->getBorderBottomStyle(), + $this->getBorderInsideHStyle(), + $this->getBorderInsideVStyle(), + ]; + } + + /** + * Set border style. + * + * @param string $value + * + * @return self + */ + public function setBorderStyle($value = null) + { + $this->setBorderTopStyle($value); + $this->setBorderLeftStyle($value); + $this->setBorderRightStyle($value); + $this->setBorderBottomStyle($value); + $this->setBorderInsideHStyle($value); + $this->setBorderInsideVStyle($value); + + return $this; + } + /** * Set TLRBHV Border Size. * @@ -315,6 +368,26 @@ public function setBorderColor($value = null) return $this; } + /** + * Get border style inside horizontal. + * + * @return string + */ + public function getBorderInsideHStyle() + { + return (string) $this->getTableOnlyProperty('borderInsideHStyle'); + } + + /** + * Get border style inside horizontal. + * + * @return string + */ + public function getBorderInsideVStyle() + { + return (string) $this->getTableOnlyProperty('borderInsideVStyle'); + } + /** * Get border size inside horizontal. * @@ -337,6 +410,34 @@ public function setBorderInsideHSize($value = null) return $this->setTableOnlyProperty('borderInsideHSize', $value); } + /** + * Set border style inside horizontal. + * + * @param string $value + * + * @return self + */ + public function setBorderInsideHStyle($value = '') + { + $this->setTableOnlyProperty('borderInsideHStyle', $value, false); + + return $this; + } + + /** + * Set border style inside horizontal. + * + * @param ?string $value + * + * @return self + */ + public function setBorderInsideVStyle($value = null) + { + $this->setTableOnlyProperty('borderInsideVStyle', $value, false); + + return $this; + } + /** * Get border color inside horizontal. * @@ -791,4 +892,16 @@ public function setBidiVisual($bidi) return $this; } + + public function getTblStyle(): string + { + return $this->tblStyle; + } + + public function setTblStyle(string $tblStyle): self + { + $this->tblStyle = $tblStyle; + + return $this; + } } diff --git a/src/PhpWord/Writer/HTML/Element/Table.php b/src/PhpWord/Writer/HTML/Element/Table.php index c7a23d2fe1..742d09ffd5 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -41,7 +41,8 @@ public function write() $rows = $this->element->getRows(); $rowCount = count($rows); if ($rowCount > 0) { - $content .= 'getTableStyle($this->element->getStyle()) . '>' . PHP_EOL; + $tableCss = $this->getTableStyle($this->element->getStyle()); + $content .= '' . PHP_EOL; for ($i = 0; $i < $rowCount; ++$i) { /** @var \PhpOffice\PhpWord\Element\Row $row Type hint */ @@ -53,7 +54,7 @@ public function write() $rowCellCount = count($rowCells); for ($j = 0; $j < $rowCellCount; ++$j) { $cellStyle = $rowCells[$j]->getStyle(); - $cellStyleCss = $this->getTableStyle($cellStyle); + $cellStyleCss = $this->getTableStyle($cellStyle) ?: $tableCss; $cellBgColor = $cellStyle->getBgColor(); $cellFgColor = null; if ($cellBgColor && $cellBgColor !== 'auto') { diff --git a/src/PhpWord/Writer/HTML/Part/Head.php b/src/PhpWord/Writer/HTML/Part/Head.php index 0f3f86e3d2..734283549d 100644 --- a/src/PhpWord/Writer/HTML/Part/Head.php +++ b/src/PhpWord/Writer/HTML/Part/Head.php @@ -119,6 +119,9 @@ private function writeStyles(): string 'td' => [ 'border' => '1px solid black', ], + 'th' => [ + 'border' => '1px solid black', + ], ] as $selector => $style) { $styleWriter = new GenericStyleWriter($style); $css .= $selector . ' {' . $styleWriter->write() . '}' . PHP_EOL; diff --git a/src/PhpWord/Writer/HTML/Style/Table.php b/src/PhpWord/Writer/HTML/Style/Table.php index d2c318a69f..24284786c0 100644 --- a/src/PhpWord/Writer/HTML/Style/Table.php +++ b/src/PhpWord/Writer/HTML/Style/Table.php @@ -54,7 +54,7 @@ public function write() if ($outval === 'single') { $outval = 'solid'; } - if (is_string($outval) && 1 == preg_match('/^[a-z]+$/', $outval)) { + if (is_string($outval) && 1 === preg_match('/^[a-z]+$/', $outval)) { $css['border-' . lcfirst($direction) . '-style'] = $outval; } } @@ -62,7 +62,9 @@ public function write() $method = 'getBorder' . $direction . 'Color'; if (method_exists($style, $method)) { $outval = $style->{$method}(); - if (is_string($outval) && 1 == preg_match('/^[a-z]+$/', $outval)) { + if (is_string($outval) && 1 === preg_match('/^[a-zA-Z0-9]{6}$/', $outval)) { + $css['border-' . lcfirst($direction) . '-color'] = "#$outval"; + } elseif (is_string($outval) && 1 === preg_match('/^[a-z][a-z0-9]+$/', $outval)) { $css['border-' . lcfirst($direction) . '-color'] = $outval; } } diff --git a/src/PhpWord/Writer/Word2007/Part/Styles.php b/src/PhpWord/Writer/Word2007/Part/Styles.php index 2112fd3ce6..e715f2944f 100644 --- a/src/PhpWord/Writer/Word2007/Part/Styles.php +++ b/src/PhpWord/Writer/Word2007/Part/Styles.php @@ -59,12 +59,13 @@ public function write() if ($styleName == 'Normal') { continue; } + $name = $style->getStyleName(); // Get style class and execute if the private method exists $styleClass = substr(get_class($style), strrpos(get_class($style), '\\') + 1); $method = "write{$styleClass}Style"; if (method_exists($this, $method)) { - $this->$method($xmlWriter, $styleName, $style); + $this->$method($xmlWriter, $styleName, $style, $name); } } } @@ -163,7 +164,7 @@ private function writeDefaultStyles(XMLWriter $xmlWriter, $styles): void * * @param string $styleName */ - private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $style): void + private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $style, string $name): void { $paragraphStyle = $style->getParagraph(); $styleType = $style->getStyleType(); @@ -206,7 +207,7 @@ private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $sty if (null !== $paragraphStyle) { if ($paragraphStyle->getStyleName() != null) { $xmlWriter->writeElementBlock('w:basedOn', 'w:val', $paragraphStyle->getStyleName()); - } elseif ($paragraphStyle->getBasedOn() != null) { + } elseif ($paragraphStyle->getBasedOn() !== '') { $xmlWriter->writeElementBlock('w:basedOn', 'w:val', $paragraphStyle->getBasedOn()); } } @@ -229,7 +230,7 @@ private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $sty * * @param string $styleName */ - private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, ParagraphStyle $style): void + private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, ParagraphStyle $style, string $name): void { $xmlWriter->startElement('w:style'); $xmlWriter->writeAttribute('w:type', 'paragraph'); @@ -241,7 +242,7 @@ private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, Paragraph // Parent style $basedOn = $style->getBasedOn(); - $xmlWriter->writeElementIf(null !== $basedOn, 'w:basedOn', 'w:val', $basedOn); + $xmlWriter->writeElementIf('' !== $basedOn, 'w:basedOn', 'w:val', $basedOn); // Next paragraph style $next = $style->getNext(); @@ -259,15 +260,18 @@ private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, Paragraph * * @param string $styleName */ - private function writeTableStyle(XMLWriter $xmlWriter, $styleName, TableStyle $style): void + private function writeTableStyle(XMLWriter $xmlWriter, $styleName, TableStyle $style, string $name): void { $xmlWriter->startElement('w:style'); $xmlWriter->writeAttribute('w:type', 'table'); $xmlWriter->writeAttribute('w:customStyle', '1'); $xmlWriter->writeAttribute('w:styleId', $styleName); $xmlWriter->startElement('w:name'); - $xmlWriter->writeAttribute('w:val', $styleName); + $xmlWriter->writeAttribute('w:val', $name ?: $styleName); $xmlWriter->endElement(); + $basedOn = $style->getBasedOn(); + $xmlWriter->writeElementIf('' !== $basedOn, 'w:basedOn', 'w:val', $basedOn); + $xmlWriter->startElement('w:uiPriority'); $xmlWriter->writeAttribute('w:val', '99'); $xmlWriter->endElement(); diff --git a/src/PhpWord/Writer/Word2007/Style/MarginBorder.php b/src/PhpWord/Writer/Word2007/Style/MarginBorder.php index 8d08eec3cc..15cf0e4eeb 100644 --- a/src/PhpWord/Writer/Word2007/Style/MarginBorder.php +++ b/src/PhpWord/Writer/Word2007/Style/MarginBorder.php @@ -64,14 +64,12 @@ public function write(): void $sides = ['top', 'left', 'right', 'bottom', 'insideH', 'insideV']; foreach ($this->sizes as $i => $size) { - if ($size !== null) { - $color = null; - if (isset($this->colors[$i])) { - $color = $this->colors[$i]; - } - $style = $this->styles[$i] ?? 'single'; - $this->writeSide($xmlWriter, $sides[$i], $this->sizes[$i], $color, $style); + $color = null; + if (isset($this->colors[$i])) { + $color = $this->colors[$i]; } + $style = $this->styles[$i] ?? 'single'; + $this->writeSide($xmlWriter, $sides[$i], $this->sizes[$i], $color, $style); } } @@ -79,8 +77,8 @@ public function write(): void * Write side. * * @param string $side - * @param int $width - * @param string $color + * @param ?int $width + * @param ?string $color * @param string $borderStyle */ private function writeSide(XMLWriter $xmlWriter, $side, $width, $color = null, $borderStyle = 'solid'): void @@ -93,7 +91,7 @@ private function writeSide(XMLWriter $xmlWriter, $side, $width, $color = null, $ } } $xmlWriter->writeAttribute('w:val', $borderStyle); - $xmlWriter->writeAttribute('w:sz', $width); + $xmlWriter->writeAttributeIf($width != null, 'w:sz', $width); $xmlWriter->writeAttributeIf($color != null, 'w:color', $color); if (!empty($this->attributes)) { if (isset($this->attributes['space'])) { diff --git a/src/PhpWord/Writer/Word2007/Style/Table.php b/src/PhpWord/Writer/Word2007/Style/Table.php index 05cec492ca..ea600a2403 100644 --- a/src/PhpWord/Writer/Word2007/Style/Table.php +++ b/src/PhpWord/Writer/Word2007/Style/Table.php @@ -63,6 +63,8 @@ private function writeStyle(XMLWriter $xmlWriter, TableStyle $style): void { // w:tblPr $xmlWriter->startElement('w:tblPr'); + $tblStyle = $style->getTblStyle(); + $xmlWriter->writeElementIf($tblStyle !== '', 'w:tblStyle', 'w:val', $tblStyle); // Table alignment if ('' !== $style->getAlignment()) { @@ -139,6 +141,7 @@ private function writeBorder(XMLWriter $xmlWriter, TableStyle $style): void $styleWriter = new MarginBorder($xmlWriter); $styleWriter->setSizes($style->getBorderSize()); $styleWriter->setColors($style->getBorderColor()); + $styleWriter->setStyles($style->getBorderStyle()); $styleWriter->write(); $xmlWriter->endElement(); // w:tblBorders diff --git a/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php b/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php new file mode 100644 index 0000000000..fd1a69c0b8 --- /dev/null +++ b/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php @@ -0,0 +1,55 @@ +load($file); + self::assertSame('Times New Roman', $phpWord->getDefaultFontName()); + + $elements = $phpWord->getSection(0)->getElements(); + self::assertInstanceOf(Table::class, $elements[2]); + $style = $elements[2]->getStyle(); + self::assertIsObject($style); + self::assertSame('Tablaconcuadrcula', $style->getTblStyle()); + self::assertSame('none', $style->getBorderTopStyle()); + $baseStyle = Style::getStyle('Tablaconcuadrcula'); + self::assertInstanceOf(TableStyle::class, $baseStyle); + self::assertSame('Table Grid', $baseStyle->getStyleName()); + self::assertSame('Tablanormal', $baseStyle->getBasedOn()); + self::assertSame('single', $baseStyle->getBorderTopStyle()); + } +} diff --git a/tests/PhpWordTests/Shared/Html2402Test.php b/tests/PhpWordTests/Shared/Html2402Test.php new file mode 100644 index 0000000000..b934d96855 --- /dev/null +++ b/tests/PhpWordTests/Shared/Html2402Test.php @@ -0,0 +1,207 @@ + + + + header a + header b + header c + + + + 12 + This is bold text6 + + +HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('none', $style->getBorderBottomStyle()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('FF0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + $writer = new HtmlWriter($phpWord); + $content = $writer->getContent(); + $substring = 'table-layout: auto; border-top-style: none; border-top-width: 0pt; border-left-style: none; border-left-width: 0pt; border-bottom-style: none; border-bottom-width: 0pt; border-right-style: none; border-right-width: 0pt;'; + $count = substr_count($content, $substring); + $expected = substr_count($content, ' + + + + + + + + + + + +
header aheader bheader c
12
This is bold text6
+HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('none', $style->getBorderBottomStyle()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('ff0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + $writer = new HtmlWriter($phpWord); + $content = $writer->getContent(); + $substring = 'table-layout: auto; border-top-style: none; border-left-style: none; border-bottom-style: none; border-right-style: none;'; + $count = substr_count($content, $substring); + $expected = substr_count($content, ' + + + + + + + + + + + +
header aheader bheader c
12
This is bold text6
+HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('none', $style->getBorderBottomStyle()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('ff0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + } + + public function testParseTableStyleBorder2px(): void + { + $html = << + + + header a + header b + header c + + + + 12 + This is bold text6 + + +HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('dashed', $style->getBorderBottomStyle()); + self::assertSame('dashed', $style->getBorderInsideHStyle()); + self::assertSame('dashed', $style->getBorderInsideVStyle()); + self::assertSame(15, $style->getBorderBottomSize()); + self::assertSame('00ff00', $style->getBorderBottomColor()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('ff0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + $writer = new HtmlWriter($phpWord); + $content = $writer->getContent(); + + $substring = 'table-layout: auto; border-top-style: dashed; border-top-color: #00ff00; border-top-width: 0.75pt; border-left-style: dashed; border-left-color: #00ff00; border-left-width: 0.75pt; border-bottom-style: dashed; border-bottom-color: #00ff00; border-bottom-width: 0.75pt; border-right-style: dashed; border-right-color: #00ff00; border-right-width: 0.75pt;'; + $count = substr_count($content, $substring); + $expected = substr_count($content, 'elementExists($xpath)); - self::assertEquals('red', $doc->getElement($xpath)->getAttribute('w:fill')); + self::assertEquals('ff0000', $doc->getElement($xpath)->getAttribute('w:fill')); } /** @@ -1016,7 +1016,7 @@ public function testParseHorizontalRule(): void self::assertTrue($doc->elementExists($xpath)); self::assertEquals('single', $doc->getElement($xpath)->getAttribute('w:val')); self::assertEquals((int) (5 * 15 / 2), $doc->getElement($xpath)->getAttribute('w:sz')); - self::assertEquals('lightblue', $doc->getElement($xpath)->getAttribute('w:color')); + self::assertEquals('add8e6', $doc->getElement($xpath)->getAttribute('w:color')); $xpath = '/w:document/w:body/w:p[4]/w:pPr/w:spacing'; self::assertTrue($doc->elementExists($xpath)); diff --git a/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php b/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php index f86507ce06..834b4d4fbb 100644 --- a/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php +++ b/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWordTests\Writer\ODText\Part; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWordTests\TestHelperDOCX; @@ -28,11 +29,20 @@ */ class ContentTest extends \PHPUnit\Framework\TestCase { + /** @var string */ + private $defaultFontName; + /** * Executed before each method of the class. */ + protected function setUp(): void + { + $this->defaultFontName = Settings::getDefaultFontName(); + } + protected function tearDown(): void { + Settings::setDefaultFontName($this->defaultFontName); TestHelperDOCX::clear(); } diff --git a/tests/PhpWordTests/_files/documents/word.2474.docx b/tests/PhpWordTests/_files/documents/word.2474.docx new file mode 100644 index 0000000000000000000000000000000000000000..8ecbaef2b3bb00517c31c2087cefd094963f2963 GIT binary patch literal 27593 zcmeFYRct0vlD7Go?J_enmzkNFxy;PW%*@Qp%*=_59}| zrIe0R%1}hS@hBqXr9ePY0N?;f002M;P|A|`ECd1o7`|VT0g%9&Lbf(e#x_p6%IZ@spuiM)0O0TY|L6QKw!l!bjNBkSOvttPd+1NqCKEe;$m;#R5yUtQq!nuW z)-W&1Th^y*`;}_Jx@zRYJw?jqj~=-!R=f=_wZthxvP3iOZK8f0v`YnO}l(;`rHxEYi8sOrXDxJ!8lm=}_++&n#@ecTX3PlfdKZ=aAZU_!K(*V6E z&vfk%aQ*u5RIsc#8v>Lj;Eiwa9@>e*^x?*>&IX4n^oRj-iUh-dH_B~kyPJh%lPj~B zmk6^JfcG_VD@JVsoi~x2$t=KD8^c~GsEu!fV)@MG`x z>i|ALi z&Cc)mqS!5b<$VSJwY@1-5Z@Ol0O0Ej1R($4VjC|WtM&TZVP(HV7y3K4bsdbY9O-EP z@%%qA{l7Sn|I4peCG=Z-$29+S;78zGx58RKMxiX7;p{rbDg>magf#N{AB&aG5AHvI zfVEHc#U|z#5@$UevqYVDlC*EIQdD3gx}lc;>I`bWb-4kOgS(0u+?MV3U^0)MOg_em zB`C)OBh^tOX0X8HU&7L+`j8KQ3O^l|LYos&&MX>|G#2LAtE@by`7q_BGb}Azh--R6 zRPc!Ogy42eWBU9s;j2$&qKS(g*lN%oQa(#;V~n<-e5A&*qNgLxiZ`hGfpyb0cji&p zTRI#bObwL}6;Ai&Wuk?o@p&9^gvrNxO{m_ayK9S`r7dv(t#JPuAH~MXm^Y#TOC3St9E9O=Wf%p5m@G80orG%iJd?sIfu zooW!;q3s44KZx?p3Vu1fo#xJT5*QdI&P$v5jYjCDCSrTj2;*Uh3M?}Z4?ANaPyc8^ zSqUlz*Q7NRjK1aLIcZU8(c+0_Xs9_vuO(3$Ay&2GmK+73b=}eS;XGM zGBjKSR|nzO%MV_o=wusbD*8ne#{>zg(JX#NKaU@E9}-{1I)*RWuwFDruSk_W?-J0M zWm4B>#61jb^LJdF!FTmQ6);^NhHj4(2|_PZ`+JAo6EhD{oF!;6#sk@l*cegfqO7D2 z+k3DvFQM1sp!iIHQWrKFRHg$LdL$aQ16_j9LVau4wfLpWf_mH%ycFss-guaEI`b+H zuDE}%Bg9JAv5!^e%)1lEnYy{Rxy2nbgt#v9Rpny7Xz|^dh>=TJi3ls4_GebEx@CeO zI6geyK?d@1LCjPsxEhV0nAu3E$P|VOOdw9Bro�v5Mn(nYgT%6ZP325V~XzAmL0M z$cdWAxIh#>ebo4{Y8}FrEDZN-T+@4;Uxx4MLt<*B%c|1@A+vM11qxbu;*p zr;bEAP98NC`yAG&J?nAqYI`_sHq!pVpN2SeNVAZVGJzIz%Af~)B-uENq??uQn3gh6 zR3B(tvuM#d^gGIuKwI|Z;LGXf5>fu1(8OroQ0mL(q5cN7;H~<4Ne-Y=J+@1Eg&oT(r|_4HAbV7Tr2> zNy!ser8n(s8z0oy${#75%<2!xBk^5Y{g5y+q|gr870Y}^YiE4g7TQ!)84g@j2Z_8p zQ%Cn>*_2d&lX-8dB9`vFN1!JRNDqAv@f1;G8AU43$=52+5`L;+&v?c$WoPJw4cs@< zY~!_$CT5@3@|+ZHX4N*W$*khf;d2%!o_|=SOJA&VXXUILco?o@mD9f2w6NpbL~^vu zZjSbhLQy%%%n0s3Hw}N;X?mGn1=l$0N1l5)u6urPtZi<^biLw`Sv_;}P{+&05 zv3MVw(HqL}6pFn+ncaRWLl;%=Mb*`KwY7qAJs2&Jf0~)^tng{ZiuVEkkF?jNx^A;a zkMNQFlh5xuwL)3)!fh!uS-x|gH*^SvM6zJVX8q$e8dp*%jGRI8Lw)-Mo_`XXWZTIUOByEKParQil9Kx-GEqgrCm&X~~^(`(umY-R#3>h(n*u@8*{m*ZEWuAJ=fIIWQ$u z+oq;rsl=lONg{5UPCZ8*EX)L8{&kksB*BtYW^zz4H&=y(Hcp8$dK9i*o~%hwa_B&i zZaNq-fl>Cu8TVDh0#Gku=zy5|3~d}?$i4?`m*W^Nzvvfq91lhC84aT<2+R%lE5-7+ z%3`Wr(lXl881UDR*+m9BP3Xy{w z(24@_m_VO=?zxf~5gSnk38f)YqXoPPFMEH%2#VUqPfySsr^Q3*$xhVZ z4;Y2opX{-sC(xUvsC(&2P)m4YVeWYk5R~eBw-p*D(uaqkR>6$a04RF!beMybCje1W zM-ylJapDxJjB;h%g#4e#m`V0h2TT)@O5A}Cz!Z4FFa|}Q6(Sp5Un1_iojONH=fnig z)65KRC@b0wZ4y=^kQ-N74Y$L+)>pun^iowZ}6KXc^OEP&g=oj zG(Xq3)`P7tOPV~d|LD3TYBjG^#>jfAhHssjuBpU&)(JdeXN-hya~%^43l?a;MiO+~ z%IhXf9mpo6tDkPo&U3t1^R~5@7w>oPgAngH=H2^{R7TMUH6#z-OD8CAj?%D zkyOc7xrMF7p1?T$ z(cNt9E08ZfX$lC6)?4N5?o#XKCy#?$ExEMjryVD&61c$UXcYlb{3HLIcOq3BjUrxH z?oJm0uK*HTXf`&r5m+pi(_bQ;wwjcb8_n}Rtf{J^*_TKQ4)tS3_y$}UDg-CE9B9mu zi%yWy;N9!9dB!W!Pz+4=Z|5qi?=cgeayj!f&{G67;Zt}XczR&|B`3Xf0<^&{asdXl z#a~%-WMK?ERKCcnguyH)}rG!Mv7wrG^?&Ro6((lP}8 z`59*|c=y!!S7+>J9@~TnOv@KvuH%K{-Y}_F>ATQ#N9z3>h5-%7CqoMa>7f?)e9JwJA`eg^rCKSg9 zV#NK>O=!V^*&MQ50Ys4o#`p_XywbghYP$M3{+;mD1<#y=#QG>6d?XwWo*g|Xy+2JV zqRBm3Og!FeVtBw0Z2r_TqxjVz?W&_cFR6Q~ZnR7TSX(p^sx|H|%xI!4{|0xI6x#Bm zU`cafP*RtzI>2Z@g=NB;kmFqXRFrCDs3rlQeVS6gBMK{tgvtIunzY=k5+qxSDNq80 z!yCm9c`Cy__(rs3tygkAe=P)#{r9`awb?KAPlh>1v5OLsrU(YI_ihtIC>HWIO}AfW z9{rD^Xu04!@Bm9`Q5`f`LnS7HtA=%l`FQgsy9(|4tS0Cxwg&JXwi1Z2I|)eN)j#^! zRM+*&MTJIKHLJvC&Ekci<*eq^@zRcVOMA3U=(hInT8iGO+~~M8A2^roRJFk$^XYvR ziH*ASi^oy!5^?k@se3nnN85yJ)9XizG7KArDP8+MGlfhu@8Yv2d^%TRs#W#2--I0_ zOGonNqLUu*=UF8Qth|^U6{^D9;k7C!6tS)O%yfN@merFi;dVaZTJ9p| z*yUlzECl~*$csT?g*yg!ixA!^idyKJi8MtLXAZh+T_Z{`=&6kVFqY z`0ewT-#-8UK#{SH(SLFIb%%8Vgpg~l1=m@-Yl1OTnG0zp^6U-ggeM@#0W@TL4E~gl zCotU2(JAq98E<`(ov%OIe?(ec$x}SU3Yby7KmxGw-GU4em3;pAZTI4lQ|*`sxe}3g-K0SaHJKu6iM0=5$6*i?L$b(13cZFXR0W}VItGFT8uXT?^=Xx1 zg!t^B%PB%hH?^F5i@k?2m)X56#-dRrY1V^G6UfFf9xO5j^v7{%4XU7{!~oRc2lFB` z;6!Qi8Y@6DqJh;bIg9?X&!Zlr?ThQDHsOe6xJXpm7fst6Y=Y2|?7G;|p$Pr%sQ@t& z?>2TY=tshQl~l5rPC2EsfGK#l4ifpbgz$$MPg+fo%pgH_KZA&sK#9BJAyM>NI#$wq ziTOvuljbsQYsi6-qP0c!8FHs$4>G%9P$rRueEi~X1-oqdRb>d)6cdO+So&5ZSpj2#&Mk>iWf)ME=d(fqc>GQJRui`>I2)PYG3WH%8(M|{V+QsIN{)oOf@*vC!W=(@KaK7Q(B3BQ6BaOi}kjh$0J8Zd>QEpj6X^VmqkMQ z5_aH=F=TYT+81%yJ22#A90_khM{kq?J;f3P!Xqg{+!;vtC)O!fqG69H5nqCbF0nIT z!mBQ^w;lOcVPI%Hrk^C0v=drzjT08m7u{li$zwQ5pAx@;=c~){u$KeHHw80fET%3XKH|?Q zm!1{zPe&qu-+>H-+qHcrqAvV<+HiR87wa9m@*{!vA@aZ(Tz($_eaGLislDlwZH%|TdnMxVXdNbg*;)9$$OC&D}OW2}7*3U4JSuDXR z)uK?buGYA&$@R^D9*7-)m3hn>ae#^P^AD~I z0K3CqKS0l*jo>1~8m2{(SOo$ZWQrTTBGu0z?}02dQR$xAN`nlRwR zf?d3}c?T7eaxL^R8m!Ktg^F1c7H@t>p#Vw=?S}41?Vh9r-o1AO=wuAy%HPy`*VcL!{SrpPsU3H%pQg=9b`bs|(M zV3EBFG6-%hStXw% zvOk*F6cUZ`n6ETxZZ_n7^ILNhEREBEqb_09RIP4Y$wBt3EoY_FXr zI)GdO$a@)QCEy@X!;N8)h%Y*+IF#wci^QP^Pq`%FIXNr%Yd(J_<{3uzVa0$4J_-W0 z%@|7z6M6{!`zz2~9$%gf(3^EAI2<7LjH0;>U+yu@p|5EYZ}g#+l;ePJB!V+-{}U*f5c8}`s6XW0 z5&-USQi>lnUP<{Ucu*jHhS5)I3!t;dE-rF#jbs`J3iHC2z?5W=96R1AkvRJ@6`J7g zTu(L}JV(3SeN9Tw7Bg|Mq83wLhv$^ z<_~@?vnx{wo|}wwSCsyn;!2OuYcJZkei1-gd?(_!E#!|ugp?65+oJuC3`s45dU|Rv zxj#H6eHDSpF<+d^LrVQTFYwE-Zko86z5uP}>##Z83*+D~JgLEmqe=2SAQ{Cy^-f72 zyLsub;_ujC4lCqtkmcZVb~DBOytMA6jbDi-@S*NHR1Ff0+j9muZ zK5>o3I@(3UX68)xv?GotgMm<=%OfvpH?pS*b z?`%%8H0_0ru`C`rTy`kHVlp9=MMs?w0Y>`)*>>k5k;YCwpD4U?|A^Yokh1D7t{N~T zI_RZJ#Vuz^TR5hy5xP(_g=gFa-tXY}zT z2WawZ$l!)8wbFQvUsvwOF4He%VZY1s+CBw&_9SIApxX16--*PVx1y;em%D6rNaWTmhJoOPBo{-bos|{PxYjVRBx2Fa_lCCWaOQLv zW7Vg;+m%O>v8f=%6CygLlsxI zB(AJ)^jcBiH<^$dQYdv36q6ZY{s_G<$A@NdIYYFPRP|_p zPj;Z45sfIyx{;N=L{B6sK65We9(2?VJDb;h{0loXyp#<2OvI$Y_RA%bidC6Kc6=o9 zwA`|a*g{hp-gwKjU9#OAE6n4LDKvq`kG#xHreprPniCx^h*b=}|{!3Ad6jN4uU#FMPqh z<)Z$u#zp)i%ul%OXQa{)2eBO7zS3UDw2%8!l@e5ow_(}C)3GWV zOJKBV&ZiQho-LA&lFJG8j*Ei;R>Q$~V{@qr{=Qf4Rq2xZg~*y{6s}s2h^mW>DyggCXP>}3ct_78(JX2!09ovn`H*2P1>)v`2XfxD^v(TkY_MSdf0`rd(5@S<{@mFLRv3{LS3K4rXe`+iV*K;4$h=zG` z+aRv$T;bKl$DkX&7*UyoT%)KdS91!P3JXK2lDD0kg1FsONTP0cy5tb$e+ntErA|-@ z#nhmrWE%}{MzsLj>xgEjk23tp3Re=izBvy_?>slM_1wSpiluu(p_vkm@5G`iVK2FZ zjr7A6a&Xx<$FdF>*BlBl#cX`!#j90OLb3|fSI)eGo$}+VlQQ(^GF#piPq3er;apA{ zU!?u^>t1e?|hGMPi39BylSojpaRgQ^5ZaAK(%Ov&jE7MH%mH1fC&fleEceu4;V@`o=eaVQIrwN z=Z#k98@{K7(>+KFylC!gu6TUhb^&9f}7t-OhV#_#p!f!M@ z(m>NOPL+56%?OFPy~+A=X$x(S)t%WXZ7zZK;1s<({`$9lRLbADd`SNupcwxHpj2b` zJ5c?$K@RvZ;Ns8?e+~aku{{D$Fm6Y0x6}c49{Y9e5ch?b&`_#%O>1Hbt7h=w<~F@c zSpGlq)5^c)rzhNh^3xjKfAG`d$p1%v;{PA{$zJUmQP>9v)I=O9w(N?o5CcZ?5m3+r z3arQzcTyYQ!^Cl9NpcrIhXeX*w%yN(09(IiMe!U!^Ob@((@>Ur!#F z`X0I1XfPxCCduyRF4vh}qOLNo42zE2FpNbk)C@d%BlP09Y#z~YpQ`40^X)Z|;(g$;??AL*?!IR;H zT#>-gxLyO=ywx!w^Up{RL>NUhDzn=O;=iws#yqw$0OkTR%s<^2pb;1KW|_^VH^;Qe zxa_N1ry%VrIpX&TINNKzIHJky@Ht37rx&r#tgfUVDY4|@_a>2{hmn2qGhzAn_|*4r z@oDdcc5q=+GuXUsv?k~esYnZ#pJU`}twZxqW=Ysw`Ar4BA4=#~wfm~}#U!9_mPKnO z3z$u6>c1>ZeE0afum3?$FMj`xo&Zt{POZKT3?#Jy`!odg^yxr5%~a z>4T_%$8i7&v(N*XA+z;rN)`#pKGg14{-q(8Xebq7S6eDF3PU9&Hg|w*$z|@Ja|OHmnf+f!mRO+X+XX z7@#agg}yWD0S$+stR4=(&7afD-tdFx(9|ZN`!Obg89P0HJ0Sen1(-Ve#DYPtvE;NH zDuMSwOUQu--WW*}+f4#mgiHd-gTw*;f^Mn{z#E8glzCPH3JLYQEiw}2OE-pqQj4IS zJp8o&pZJ6m`JB^-sj-8RwMU`wijRU&Wi!G8!;}_S=k6LXlheDk`G4aR$G^oVPwS$m zeEm>=|M~PY4;idC3f=-B+>LE4tgy7ZPo=FeSU<>463FpC$HK(2-=N+US%e^#e;h+-?Z z!92M96MFc-3x5n$lHH-|Xz0NgRcDvD+*oL7B#Vr^jlOZ``djP(zR*g@j5X(ZQgck~ zBtM)Z8gCZuwFA?jR+DUvhqMmcyuwESySEZVelGMJ0+({@i6rG*5hMk-MZwM+Us98h zo}T($4vgoBkJ3L8=DkCCP^pjS=~)HVO(Q4W*KfV)5?n6F+}Qg)XJP=-NU|)4cS>QK zopb!tZg%pO%r10$w^CWxK>X%P)o^x1cjca?$9I{vCOZe|~}4 z63kqF(4&G09D!x#-t4f&Llk*Cx2C}|R^|h8xYT@xQiIW~xNlnF3~RkOXrkSBjMkhw zE@LSE~7)U!D z2_5!MyBZR>x;=_a$sAXpUS(>g!lvuflxb&4<51TJi#`kziWNDjoDA&GLuItxDsq1yy|lE$?f8sD&wr2G*bjqO{FeIb z#>nFw?JR5^d73}zIjO9honU}SmohQ$Iu>bEm3GfS9;-iT#W^^qbv%95R!A`|JG}n^ z1+<^vgP77DxT-D<=_pVGKHOzG2dbRDdU7hMLZ>Z>aE#yn+99_D6epxAKtx5id7 z{C#sT$l5fg=DcUKJj=fZEMu{)pZ!VIfg6SS>9Xd36RxjG-3RmFijL%yM>>eX`&pju zaku^6*V5ZJ<4SCNbm{~3ba69q5*>nedA zCz|${xnTPvu;@HBF(H$fXj6tB2*~xc00+)>*hM>X^|NXOiDLsg+;a!s^Eoa!sf)mq zZb9GVBqpAebcB4`EX>^m4%(>>)yu7M7{qbttU^({B3b|@MBjhD8pO&=__O!L6~;nH|Epu=rWwy6O3@25@T438BsUP(S52X(3J`i z?4&7G&l$1lruPF6df@qON*GNc{a88{C@hAyWPcTPcLV1T12Z)mpwq&fJv+(ox=bUE$e!MOUr_XXJZ z7a%UWKo5~rdvPkyyGrKXM*8x((x)QRQr&qI(euJQK-ctK3T3KSvRZH2^F?WOiiP>b zKLMx}4|?cy^u(j`l5NO7@76nEunz^!(OS>&B-|;?ojIUFQ2|)MU~qp~lBCmsvnT?)%>p8YjjdW?5PN;sO?bQfkB z@N(|A;hyQAUmjt6G%P#vN)1VTKfgOr|Js0hgSZet_?~r%#sUCP|FZ$*WM*t_O!v>{ zKf6#D>YFy}tSCMBb*= zGJ!hRhi!gX%`EDl;~9_`uNKMFp6me+&tioAz0V$uJ&wbtJb%hQ&+?NRVlNTdw%oYm z@ry_!YZHIlO1zh-J_n-Ji{}@FUVKgo^K^~j`?_G|M#3CNjC%s(3-G2xHtQAbc}PI+P#%DRmANs5);ednU7OyW$uI4Ua;fYJ)#*!Y;a_)&3Aeb!Si3?QY#`L&UX17;IKoSDLra? zL=v}gdiH*EGW0DRw4@1>-cz~vl=oU*f7;Wi0b)?t^9ITzE}01U9fm>2x#ylF9UhQ7 zE1)aI_+2-avzlQZwW5;Yb={t~w^G|a-QJJ)r=@2bZnj%u5y0IKR@9585&2P|0wfI0toJgMHozqYv3@ zKN8YOA8b7CSgEvRFtnTN?FNxoN_}u2+|CDet6ynS0-^CV$EK=bx`G-?xu;zVy?rcU zms2#Z^`U!L+5}6iHfyC!FkXu4S44uR;*_CV&qtM{ipw?)JY-$t{8cpT~dmg*UjHbP9? z1`XmN(asm?>Z{xCdVMA|^*mbe+|tWgeZ}Uko9!8s>_D0tigQY#dCl7KdNR77pv=rkjH>LBw2B&BYBl|%y$DZAnpm*im*ZlK^;Z%*av zxnWkfvTF1LBJWk57)Damp<5sTDYR<4j4VU6b!;>iJ<1vL>2sO8il`lS z>i;Y>l#ln!hc`Zl=&9sGOT1D`)$}D%|1qQhL3P3U=?8UWfI9`sWN2?&&_=3SS#9=V z(z03*H_6n-sUXcjx7*QGVmK+pw3(Bvtt(@aQ9-i#vgq9!e?~9tI0IB5E7R)Z)h4v1 z*F5JPsY1EAuCY?tWV20ae-F~1*Md1&P=di#X02gu0nvqkF<6~?)WftlTiX9rp#mZ7 zT)Wtuor>)As#WtI-&I2!wv+L*BV`6s;jz)79JHfZG1U?X4d%HU-G?2rzyxFI-G;pe zth%q(c~GE|6>{*GAr9;NT-{$qa1kSSV(O8c(PPX8t!~e`ORj6(yR!^$suK6?Gfz8h z?DJ>3=%dePI>5weml+#*_b=N&HIEvB5;PdFoy|Inc*~-fWlR}S1$FD0T@QW5;1Geo zP?|OdA<7n*UWNu^n&q1{&^>v&5QABU}IA4T^8pJo*pXcf~9s9%VonbhjqneRc>Om5$ zb46aO1YbEslo~CiU3KJX=#fC_1>uHgPg{LWezMtp1J{{XmCD;0DtpH@1*7Ezv!~E;@DUZ!V2&MC@ zL~9w4ovjXy=bJN|%jmEv1>ni9}4#dhQMrN5=Y!iH+6it2Y5- z!{;|fw$fbn!EVzn3DWw%^{g^{(`_y#!1z`RxZ^FE)l1u!uziD82Ujy__NuD)ZJvAG zhV9SGsB&1>_O|k7*;6tr0s&_hE_GJZ$oIA|JX0S~N4zxl%wT6j*tt2Lw7e#MK8E~9 zN4tis?}^bYX1uLRX!wo9xfe;teMo=3r@5JCQN8V7;!rt9oOw>pbnu;?hYI=&{-!DI zjcpiu$0(?Fhq53)G;eTRmSk`%luWawvPC<4sQPp9XM-8}GI+4P2NS^Y?H9UZ914BY zQNGx_zIAuocGK@ed5T%RwpJ~8!0f_RYxhH3CN;EEcZBz*6H5;4k$}E-1Sea5< zH}9{H<}GW_^?^JtSZJjOqr00wc+yP29(watr`Ssk2JQo0IIpAETR1~k4b~r~f8DnY zd~QB5Jdi>dGnrphUt+IkZg1YR$zwpa6460q4;3<npu` zT)ZMt$E3gTalbG@5X{iu+w$+7k1QLBK8uO3!)I6-iT+ByR5{IZPL%=D`qWQMe(ScU z7l3NE2Xms!?#ADLA3gzQJ;P_du4noR=${q(!;&;+ZS=OatUJx}_;Ek2#k+vc%NAkd z^8%qfz91jVAo7t|3gRKk9m{B6a7YzYCcX=Ks6ed{WG}!GVV7Ri2B#`WNLaIFb*lN4d6D zoTuh*B*?!eF>@-~`zh@BQwWKX*)igWh{85XlR`v|STLT1{I>EaB%T7QLr9}4X`kgr zps=wU*c%=^2WRYz-vb4_*GX6#gwH54=DsksCZ-8D@JcHer^{g3!Os{$ZW*k>nzaLQpuVV0m62 zS~xvT5^F!3NF@u=sn}c?DXY+&q!F^ZiW!YTq6}zQOMf^t%^Q1Y>9E zKTa|}M;#<6aGx05$F`v{WRMlEPQAd!EpA3Jt7clRjG2*39cb&&G-8?#(_PwZcDc!B>E+datTj5eY8(y;eXu}h|*sxY+W$Iz_JfCzl^4yr(v0wqxuCDwU zXt)g%CA*SYaIrRYF>-t+*_qk6xd0g|3ElYhIJRteZ@57>jkgZf?b?wIH+PRPP&z2u zO_}*=sXlXe)wVvZ5$zH?lV_;(?sEK_J$ulx_jFgDQ%BTmX@A!R$1-Dyj@;llc^wWy z)v0W=s`N7wXz(3|%L-$G}P092B;$6K@raXfEC~Bj-d;v|(dZ zwy{^rZKL!gqkYPlIvK?3ZaquQ&rD7Bh?&?xFWbuTt<1d%%r8-9F#WFCHmkccH4HtW z?*WqrOxpbpCLBCZuI-QyoumDa@h}&N%?zWC2oRe!3A>KWb2L}%`&psU$Oc{7juvW* z4**oi!Xo{%xl=LzWxX$hOJ7L;r4;mF_Q~my!H1xGRjg_cjB1cOl-3sSa_9ZjR;l@t z%Ic%$f<>w4xygksu_!w=XKxT=P^kiKa4nJhnh%6q#zDg}8=3jUeP4N3xEmt@*Ps&mXdF7GU@vMyP$I6A^KcymcX!pnD(B%E$M;io9R{T%7rT@6(RFj00M~<(Kfg z3x*7!02n$uI@wyQSzFPWI~iO5lhOzZNB|Uh1mBnb=Tj9Y02e@yAW|#n0f?YgL+Fb; z`6ZLl=AbkVkXgf=j{&!}T{?l%l0EuJ{i}5S>$-s}$P5a*ehfNZWSQMe3^JM@sk7yZ zN~Befz$E;)+GBKDdaC5OnaK_3Q4^qKQ@r0&)-a7Ex{#%g-0|Lzx>ZDBeCug?9_4|N zZ%FJ3KN%PzL|^{h?)1Xa^(sl*ofjrWsc}KmPS6zbj->;|&0p*iIZG{GyN8ZKL~}xS z_vzp67sIM{NAG_Br^>fIF#g3J@(#9kj{m=mk`?#QWz>-C!0y1g&F1x@d>Z?Ra`Y2t ztGdc__He?Mz{{xUde_`R*-d-$TM6;)*t7f4?AcDZIkyqL&V0Wnr5+|5y_?thJ&?S# zqVt1;Y?B@Ctjv+nj>uA3GR#Ey*Ni5Sc72ELAKMy7c&Y3Ac#aw=FMT4O z`R_zIlAo{?mD;?>m{F8d`3@}5_-3l6R&1#oOs|p&3%|_kc zjmXTFIop?}n=+>kH8OL83o`k=U=y*2%##Zt%DAxr4>bC< zsS=YI;?1Y065q$oVvJ>l7RAP&a2L%vNdAWAo=_jR-7*nDQhs0Ala&hCyC>}3>vNYE zVN<|Sk;6fbgCh%@pJC$OYxD>b-j)%iZzfrQY>oGrSP4xkf zr1PZq%_^=R`AJ)8V`C5HI^{$M4G$$%nZZ)6$8gLbHQ}A~yhNBD`P=r2EAEXB(Jpsr zvn>=yC(U9enY9*b5b*}-dHoNm{r=xnM8YgChIP6vj$fsH)aTi!*YC_#fco!Nt(T=7Ht}T@>mU>JZ4JCX|`g4T!E6p$c1u zBBSbXt}()z0=`C1G+&Q#bvBbPCne#|-Z0Plww0*r1nw(Qw;DO^3E-*h7oCl;mtuPh zr9*~obLVaH(SYxs*i(I`4aG>1jd7nEljnTMX!Eo$L zGejymZ29BZRj79JP}G z7L)*be4MsWPj6#AYu}%gIuxX!38~f%+wK=OA08)LhhEax3H9g+b&Z{YUgTeBXVw&* zj~DwBW!sK$myN=_wpX*(TD9KHUx{v5EN)R5r$C_2wb~qkkefJrY0X=?peL!mpQaW; z6wHr~h#tHcrs7&buq5^V)CMopRCW6DN;0fx1T0qaCx+YW1;eX#fU?vg533zwY&1st zh7VOekio|0i_6urO^ZJ_T6#hkeXTU)rsWe2-J-+J|ffc(b z(I3=O5DX~Qg~}C$A%_NixSCmTkkZCK%30;>fL|i-e!cK$Z>`OOV`N6}J04bVf1aOS z1yYP@`T=d1_^H0p$F)|gb+gy?e!hJvfmjx-CGu8rmN&ns24Bp0HJ!kwK3<46!&#T^ z_NuYBcriQp7pusNJWEHpG4QP(7{}gndu&|D3sOJE*spq)mgGff%E7p{Qc!k7D#=f$WC_k}}29 zU}q7@rXtB?Nji{8>iKQjZj#^%c@NB^$$>6m4~Ytu@&_U}6MG+b51GY!gRk5wLqr$c zO+YnpcBLUr6od1WMl}53&r`J#;2LY2nI`j@kb3Kd&uG+<;E#r1G@^N1$FK5&6M|DG zhP5q1FBV>@W=kRJS*Y3McVdBz@B=3=yJeT5&)M4ZE-v>~QA2EbCcCMFzBjEnT*0ja5Ql6YTw_B~yyp1dmlP$?FgUt|p$ zy&t{P)}6?!m?u0n&9;fR&Ofw-0PVyzA+{(vUsN|%m7OK_bufZmpYLks1G~K)oi~c>9tu~ zCr`RgJ12VK9&WsjFkE*AG{~T#>jgL4RXzk(;!RLdD-6h>6-8wKJARsw(cb6bAgdp9GbV?&#(nyDdfHcw|Ih6D_-m6#T-rs-l&GW2h zIA`;&v(A})_E~4`wcg#_#-QFZ|QjDX-<3Qq82K++o& z^P`@%R=UzU4qlWCFN^rXaS}YyQlIQ;7?@of=_&kGXspHE`{Jy?Q)qTWM6^>-m+bo<8tAKN1)3egY;nUhFk!1Vcu>UnL5EFnMYzwVOF-(PlfjXpr zmmfMgyIKETd$^-B>o~`Y?YUTVjW{X1E6P7+o*TUr5;N8nqHQGBNh9eOBt8f+ zls-?n_PGwZ`h5Q(Kdd|s4asYhJWFQPhyY^>7|%wMc^LM^+i&$De(c;sgbMOfEBmKX z_v8TN6{~yB$LFWp6MYQ5jxiE6>qsJGBBR#FD;VClvu7A~l~;^!H6Fsm>6TZ0^O<8pGq&azLUPZ#E>wF#Rxwoqp&B>UuH~&_WEM%)7|41o z>QC9-)Jx1`n9rNzy6;ERPsfy2A@62k%)lz-yyzECHZdG2uK%Egm0+#k9SCFI)e&a~ zj`pTU$nnU{eEsdE-gQk#9gGye4=fA^+7>nXiiFV)k~B^rkUw<`ArOcVp=E~Z!_uEa zi@cUO_BB0@RzM@h+JZ_+J3oTgUV*tVNA=tKyiSMmqe%D|eG$wL@W|>n%-!s_`8TwZ<2fD2|LuHUuKy(y)rYOk6Dv=9NzhI*5KAr$C6+dH(@r)SJZxSqax)MQ zarAt+3Z%M8Csn{RaKLZ&MBUQtMs?t&RPvj75Owc*2!4&k6XMn?JZg(O$h2XSA;9*vm#2UIAqOfE#s zib&OeaZy0Bt)J)eA+dFBbM4%X*oSU?nM11+a)1BD1<^{__KhX{{_7OlbA5I#`*eEY z{3r4hfjQ9<`h}Kb80o3U=G!JwToav`nIc}hZ(nXt{ zs0Qi!c64V-N3-X}a$ra{QyKIJx`yM}9exoR2>2@F3%3CzRaQ`^xK2vyAl4;eNcxiZ z%9lu&2>OTrtM)hScoy6W$z$f}xNk>=w@RxO^xVhI(~rMz$mO6-&9V*{;Vlo97|nU_ z?oq8fUVGb%neMPrQr#mIo!{gcUkb=wWuyIS@{k}Z#bAijXjWijRN3AtZM-q-vgqY_ z)VpdujS>In$g&|{F3PgbAsM3+nk8G>oT|0=dsR(#KVXFd}5kcF^O60`Y z*UIL;zkOjLr>s~rk0IH(>@Y}SzKY?Hkl$3zGrop_OWUk{vuoK*L!_Lif|l(~_k3O* z6a4UNJ8WYxlI+#N>FGpBIsZYTzV@QmONu#2O$i{+@CqjVNEn(8p5&f&K+!sSE_UsF z@a$7>Uu^zpx|^nml0Ly;Cyg@IVJ}WhFF>QCX)p_UU+>srSep3!$$M!FH+BVi5z66S zOC35ga9ZTgm<%>{Zij7H)#Ih_LnobYxXB~k z8W{||kxO8jEz0j7zXgyJ6w@3D82WEGv>|^ydnu9})^SvBn1A3Uqp=!}3cntT1Qi6R zz{0P``+VU$Ak^>NU*>EJSnOB!b(6V=%Ax17{^H`S8p+t_ypjukeP9?jiCsupzh%eT zk!Yj~K+}HnmJoIVznDis`eGFcV)JZ2C=h-OdzWqveq2HrPpYU#3cs}V_FHg`kP=UI>RYv)R?Q#i)2<@<`j?}A_Oeg4pPoC(~44Og!y z5=72uN|L`oVZJOw0%7!4Kd?IrNnTY{kp8@0t>u`)HANHYA=AOD;4Z`Tu2j0kHoA#s z%QJ_Pv~K$2+vO_4MTZnTo7W^MHe9*k#<57`bsCC8s=iemaUO_KX#!_3mAeSIs#7l`*4uOwGzHcqccB)`%d`Kw4sj*k(7&PlHma|$zD9p)(?Cvz=Y}Qs1oGsqVnL?-;(70g%q!U1uF0P znIs$>{%HL9Pd%R&nD$l+FyVI#Fz^2@mhdhHDH#}dBrT@Va2Sb&>R5nceJ^bKjvLwkb+;9|3hn$3pye{Q(TV#Q=BW* z&gJDl4Ekvp=KZyT$8&v(l*luRlyFdyNb%ghmYEefGrB6uKRLa=K`C!k&KT@zkt>k) zE+_)Il-yv@r8;?0aEs>hE1pP-Ve&>2ftWufh`8W@<|$syz+azdUpr9XcrKwNANyfh z4&eGwoW{dhF$LPt*~$1)?UxUY$w^1OAg=}LDPuqL3$%FxIyZR{7?Kp>W3QFO`ML-H zu-4orNF1-O%Um4OvYz!3iSP4vo5RnT!pO$<3-O}ch51|Srpi7erV$7o6=YdMER-Ns z{(y+lL~bE~-s#Q*Pm5~m%x~2lOTjhbSwyws4T@;__Fzkx6?vZ5j&F$p&_fj&b5CI2`+-a?K==t$A(^nm%ah~@ zF%TzBd`;wAk2DSSH_XIGvNRv%k`>d!wpVESYOAKM6w${1&l3qRPDn0raa9q5TC81T zoJR~|txcWZ#E>pg!sA6H?|otpA7OqI^2bp=0*8-`2vorcm5j&?Gv8MOR2!|feP}; z#w#UtHPL3qGRn}@w~*Zz+wBzRwGR(h)JJK(!Az@({ZFHvMDWqdFS{x@gy8jXmC=YP z9*l=giDIg0Jh_H7$S`ZH**?;f!6GY~W%$a=ZYYY-KjS4N%OgPVo0CCX_c(q`t!Rbz zGraVGvKCGm+lag+=yu}k*6|5Y?izz-83W>Q8rPG8GQgZ$v{qGf3@JSs>`CL&;@yCl zm;g9ea7@IKwPR;Ghav@QFznQ+gviGIt6Tb_Wg3)-OivVEjP4su)H+)g)pQJ;J!`&I zq6nYY{)D(KFV#B6O@$QAYSW5fMyf)QTTB-1qINx3@y@7u!e$2KW*tRIrj8~(%hnx* z-iTI6E}8=`sh-KWH+)l2 zuFvE zJFl8o>W`e-B-vyU%&4HjpXRrCUf-DER8Z!jE}nK9MQ`vVo14Y(b77@vR0?bOt1<3} zvaR%K(w~hT!S>lUo_vq2SK7i#P5qgKE>^2lSGne!i)v;d(~WuQN8H(kMJO_|DgcNq zmS(hfkj(RI%9Xu!t*Y#%N7VVX%JkFWXhPB;{5_gMiSqEx6OKc@`46&Yvjwe@`#cJ_ zd`#q0)zfP#8cm+N;<5fIg+4rhvnm^r^EX>l4kmF~VDCvI-6-|erhFA&>@5KteI(C>~GVUs!i6U9wU7mK+rXYq9!G(*hFhffaT$P9sl@5 z_~k}Xd@GbnGW1(H*2yqf_HHj=DPUst2^BQRM1ZD=JN1T42YI}?9xUxLn=<3olB~k z7K6d8#F(iXT4t7-;>hToO5#+IkUHzgREAI+P|`)J<-{=WZpM>xovyS0{`#mDqRl~` z-W5lpQd^kWColv*XZQXMWFr6Tu4HidD_AkqO@ zYp~&h`SOc5H1P26ltnD8frkUoX&*Xx7#Q52p$S(LL*?I6PEz9(?Vv<4J&W|aLh4O? z{0qq@YOyk-p9;sgRkhCUJrM0NZ=-zZFs9JT<$&!vE#6Gv=^5n{%%SMTXJRP$XebJmM~w>ZrE6Gq3;X0I#F29HPP+_t{RJlTwQZM?2N`tbovWTz^w3X`2 z=GvV_9#w+p)Q=5IgC+9QN83%1<~$2;Y@gEYC^s>`V%-9$SQ1=tS`CzSb>%lDc{4yh zTBLhj**%v^hEDzJ3#@n^2hMQcJ`l@!k6y+h$?e@`(WAO1#H`Dmz{WyN-{kyNuoDTX znN^W`o~t7KrNrhF)0McdAfoW`3xhSI+$*KyQQ`0{xDlYqClujKJvV3xP<6@(@ofIS zUhK*za_5)#Z_Q1dWDGQOBOsu6vyksmr+@$W6ADT~u+WGVJhbNTH(D%PXB7iOYm@I2 zS95BA(PFh^RJ%T&t0M&TMkcG-Og)V)(q-9o+4Vu1EDyKNmrgfG^oVS3UoiEEBqMoR z0GQ+`P2-Mw0In4Si?%q)53^0OxRM8}s2l`;&7hj16vjtd3k|-s7ZV|Bj2KIz=O;*g zHixFT*L6P2XIAXMRIi3!vSB&69GqgI6DFe2MP#6!7UFA9_{`r}y)#GqCF;P(fn^%} zlD6bw3;C076wHvh5J~b*sY-uY9-piw~t(3a4KP$*0IQlarPFXU6zE*fv;6VPJioQayM}LsxUT^>TL&9}L zT%^a*N|B;-XM&7Wtab!I+h87VIIt$fU|Z}sv=PE`boLT|Af(Ih9ZaUuJuFaf@FdzHJjolXY*yRJMY1N=CjI zt2v^xu9Cn+*UeKJWJqBhhzWM`L{x8H$yjW;`gT);Y@$F-q3!#atW{Kthgj14o$uqJ z1!-K=z;VGV`dwOgNviS0D_iV&wk&!hvw#Lia(;jBQfRhxtp(x-uA%AbhvQ)-L%N^z zd4X&x2wactaCb>|&ey}r1c^&zcs3H)9(i(nd6s;gj|7@8IMVyiTc&CORy+whUeZEu z8!RZLA(Y4PulaFyBRj{x?ieT`_y6-4LNDFxm_@5u4osg##7l~>1+wb{!a@h_+IZ2@ z=W2=J%@$)kidM3K#>qZ!vFV63T3iJ8k*$`?s67>^i@@vQhZ$Z)(g6xH){h_Ww&h!` zXc(HJ31uivD6J}|L=0MOy!n=;)S|-H=4nrUpEyY4(X={KE2WTxo)Yl{x*!oKxSa8P z6+qAFZy?*O!tRKo!g2b>#Yagt&KYyvi!&xisTjDWCoY6sojn3T#HU_kmRDc_Y%aft zH(IBtnA)Bn{l?5 z8#e_1@k8v~X5@#X{uhDZVREHQS8C1-^lPtI;`_c4g{LesNdX1Ehe>bw-+V;Jp6?HG zwJ1?QIGz&sr1o=|PQ*X8sK%810{=(_{v}uE$AYeWERAIiH;OReNR2*V)jEx!=?ln2 zGwfP7mrI2#kV^#FIU!k7o;rz3EYyx~h?NKo94>RzHpn?YuOSC*T9}}9TSx9Fqp`p@`0o9a2B%-^6?owPToiGg0y26IW^ zJ!00Ueo(hS!V1iufY7u0g_g-c=S&w%NDz5j?#CJUK}0s4m*C4eUrX6|P z7#f}P(Wmxr=gHfM=fHY(jalweN|3Pn`FZq$*4)L|{EJZjh$}iNqBB7r5_TGm`kQ!{ zPd;cB1-&;*`VW_0k<%o}r<>Y@Z#!?;<;*UY-M#_yX5c%w5V$NeoF15bgLc#Z@Kl_| z1l&8IEez1UN)GL(zcRM|rjz|AZ|ncLWWRUUAD^&~6(P_?0T7n1mloR1?NaBS_haSD zGCB*%pf?grNk20hP`9*LY*~NH?7Ybke0!<*zS$qB!&#MYo>^%5oRADnR35?W^4bzM zq^rA~4R^GSjHq^gL(1brmkiS34o-2l1TRNuu3hQ$5FraFjw4~cmGJkOus|FP!HbJ;%?{JE6IPvH0OIp`sOTUg^R z@a`G|zo1vpim!jIIB*yKCl&H9Fbqr_`VaX3AV$8c=`NS;FI{9%n$LeT+up_BFoRU51fgXbGA>(0}ob+(qB5ult35&G`d; zx7zNmio5j{zf|<_{ZR3HwZ&cdpP8e-z%Vc@(A>aZqtZX)(Om_9rds~4K=|=p1-}w6 z?`pZ5arjG*$CDpt@kjpQUHsjIgkShn>7Q5OpY(*g;JYD%Utm7XAK*Iygu5E@8bV-!hfM*V9cOi`@dZByYN4qp5NiThQGmoxIlN&e?E$SM=u!v e{tN!sgXy^}Jan-5o^*@@^AQdPX4Ulj*8c%+K=Wn* literal 0 HcmV?d00001 From 17030ba7538b18b367462409206666b44bfc47e2 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:48:08 -0800 Subject: [PATCH 2/4] Add Pseudo Change Log Entry --- docs/changes/1.x/1.3.0.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/changes/1.x/1.3.0.md diff --git a/docs/changes/1.x/1.3.0.md b/docs/changes/1.x/1.3.0.md new file mode 100644 index 0000000000..cb8e11eecc --- /dev/null +++ b/docs/changes/1.x/1.3.0.md @@ -0,0 +1,15 @@ +# [1.3.0](https://github.com/PHPOffice/PHPWord/tree/1.3.0) (WIP) + +[Full Changelog](https://github.com/PHPOffice/PHPWord/compare/1.2.0...1.3.0) + +## Enhancements + +### Bug fixes + +- MsDoc Reader : Correct Font Size Calculation by [@oleibman](https://github.com/oleibman) Issue [#2526](https://github.com/PHPOffice/PHPWord/issues/2526) PR [#2531](https://github.com/PHPOffice/PHPWord/pull/2531) +- Html Reader : Process Titles as Headings not Paragraphs [@0b10011](https://github.com/0b10011) and [@oleibman](https://github.com/oleibman) Issue [#1692](https://github.com/PHPOffice/PHPWord/issues/1692) PR [#2533](https://github.com/PHPOffice/PHPWord/pull/2533) +- Table Borders Fixes by [@oleibman](https://github.com/oleibman) Issue [#2402](https://github.com/PHPOffice/PHPWord/issues/2402) Issue [#2474](https://github.com/PHPOffice/PHPWord/issues/2474) PR [#2535](https://github.com/PHPOffice/PHPWord/pull/2535) + +### Miscellaneous + +### BC Breaks From a4178cb9eaef526e4d89597e1e0bb993c3f7ebc3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 29 Dec 2023 17:57:41 -0800 Subject: [PATCH 3/4] Html Font background-color Code to support it was missing, and is now added. --- src/PhpWord/Writer/HTML/Style/Font.php | 7 +++++++ tests/PhpWordTests/Shared/Html2402Test.php | 1 + 2 files changed, 8 insertions(+) diff --git a/src/PhpWord/Writer/HTML/Style/Font.php b/src/PhpWord/Writer/HTML/Style/Font.php index eb59d02d1e..29b35687a7 100644 --- a/src/PhpWord/Writer/HTML/Style/Font.php +++ b/src/PhpWord/Writer/HTML/Style/Font.php @@ -73,6 +73,13 @@ public function write() } elseif ($style->isRTL() === false) { $css['direction'] = 'ltr'; } + $shading = $style->getShading(); + if ($shading !== null) { + $fill = $shading->getFill(); + if (!empty($fill)) { + $css['background-color'] = preg_match('/^[0-9a-fA-F]{6}$/', $fill) ? "#$fill" : $fill; + } + } return $this->assembleCss($css); } diff --git a/tests/PhpWordTests/Shared/Html2402Test.php b/tests/PhpWordTests/Shared/Html2402Test.php index b934d96855..f3f7f78c0a 100644 --- a/tests/PhpWordTests/Shared/Html2402Test.php +++ b/tests/PhpWordTests/Shared/Html2402Test.php @@ -73,6 +73,7 @@ public function testParseTableBorder0(): void self::assertSame($expected, $count); $substring2 = 'border-top-style: dotted; border-top-color: #FF0000; border-left-style: dotted; border-left-color: #FF0000; border-bottom-style: dotted; border-bottom-color: #FF0000; border-right-style: dotted; border-right-color: #FF0000;'; self::assertSame(1, substr_count($content, $substring2)); + self::assertStringContainsString('style="background-color: #00FF00;">header c', $content); } public function testParseTableStyleBorderNone(): void From 7c4eb2102fa4a928e62a4b5d8f9a6adaead79360 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 21 May 2024 21:45:31 -0700 Subject: [PATCH 4/4] Update Html.php --- src/PhpWord/Shared/Html.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 5f50e644c3..f64dc30466 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -147,7 +147,7 @@ protected static function parseInlineStyle($node, $styles = []) break; case 'bgcolor': // tables, rows, cells e.g. - HtmlColours::setArrayColour($styles, 'bgColor', $val); + HtmlColours::setArrayColour($styles, 'bgColor', self::convertRgb($val)); break; case 'valign': @@ -726,11 +726,11 @@ protected static function parseStyleDeclarations(array $selectors, array $styles break; case 'color': - HtmlColours::setArrayColour($styles, 'color', $value); + HtmlColours::setArrayColour($styles, 'color', self::convertRgb($value)); break; case 'background-color': - HtmlColours::setArrayColour($styles, 'bgColor', $value); + HtmlColours::setArrayColour($styles, 'bgColor', self::convertRgb($value)); break; case 'line-height':