Skip to content

Commit ed41166

Browse files
committed
Add support for rgb(), hsl(), and any ordering of values in border when reading HTML (closes PHPOffice#1636)
1 parent 8dad5c7 commit ed41166

File tree

3 files changed

+360
-22
lines changed

3 files changed

+360
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ This release marked the addition of strict typing and return type declarations (
3131
- Escape arrays of replacements in `TemplateProcessor` @0b10011 #1669
3232
- Escape text provided for `<title>` when exporting to HTML @0b10011
3333
- Export `<h1>` instead of `<p class="Heading1">` for headings @0b10011 #1692
34+
- Add support for `rgb()`, `hsl()`, and any ordering of values in `border` when reading HTML @0b10011 #1636
3435

3536
### Miscellaneous
3637
-

src/PhpWord/Shared/Html.php

Lines changed: 229 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
use PhpOffice\PhpWord\Style\BorderSide;
3333
use PhpOffice\PhpWord\Style\BorderStyle;
3434
use PhpOffice\PhpWord\Style\Colors\BasicColor;
35+
use PhpOffice\PhpWord\Style\Colors\Hex;
36+
use PhpOffice\PhpWord\Style\Colors\Rgb;
3537
use PhpOffice\PhpWord\Style\Image;
3638
use PhpOffice\PhpWord\Style\Lengths\Absolute;
3739
use PhpOffice\PhpWord\Style\Lengths\Length;
@@ -638,10 +640,8 @@ private static function mapStyleDeclaration(DOMNode $node, string $property, str
638640
}
639641
break;
640642
case 'border':
641-
if (!$inherited && preg_match('/([0-9]+[^0-9]*)\s+(\#[a-fA-F0-9]+)\s+([a-z]+)/', $value, $matches)) {
642-
self::mapBorderColor($node, $styles, $matches[2]);
643-
self::mapBorderWidth($node, $styles, $matches[1]);
644-
self::mapBorderStyle($node, $styles, $matches[3]);
643+
if (!$inherited) {
644+
self::mapBorder($node, $value, $styles);
645645
}
646646
break;
647647
case 'visibility':
@@ -652,6 +652,144 @@ private static function mapStyleDeclaration(DOMNode $node, string $property, str
652652
}
653653
}
654654

655+
protected function mapBorder(DOMNode $node, string $fullValue, array &$styles)
656+
{
657+
$values = self::readValues($fullValue);
658+
659+
foreach ($values as $value) {
660+
if (array_key_exists($value, self::$borderStyles)) {
661+
self::mapBorderStyle($node, $styles, $value);
662+
} elseif (self::isValidBorderColor($value)) {
663+
self::mapBorderColor($node, $styles, $value);
664+
} elseif (self::isValidBorderWidth($value)) {
665+
self::mapBorderWidth($node, $styles, $value);
666+
} else {
667+
trigger_error(sprintf('Invalid border value `%s`', $value), E_USER_WARNING);
668+
}
669+
}
670+
}
671+
672+
protected static $rgbRegex;
673+
674+
protected static function getRgbRegex(): string
675+
{
676+
if (self::$rgbRegex === null) {
677+
$clrNumber = '[1-2]?[0-9]{1,2}';
678+
$clrPercent = '(?:100|[0-9]{1,2}(?:\.[0-9]+)?)\\%';
679+
$clr = "((?:$clrNumber)|(?:$clrPercent))";
680+
$sep = '[, ]\s*';
681+
$alpha = '(?:[0-1]|0?\\.[0-9]+)';
682+
self::$rgbRegex = "/^rgba?\\($clr$sep$clr$sep$clr(?:$sep$alpha)?\\)$/";
683+
}
684+
685+
return self::$rgbRegex;
686+
}
687+
688+
protected static $hslRegex;
689+
690+
protected static function getHslRegex(): string
691+
{
692+
if (self::$hslRegex === null) {
693+
$hue = '(?:0|[1-2]?[1-9]?[0-9]|3[0-5][0-9]|360)';
694+
$pct = '(?:100|[0-9]{1,2}(?:\.[0-9]+)?)\\%';
695+
$sep = '[, ]\s*';
696+
$alpha = '(?:[0-1]|0?\\.[0-9]+)';
697+
self::$hslRegex = "/^hsla?\\(($hue)$sep($pct)$sep($pct)(?:$sep$alpha)?\\)$/";
698+
}
699+
700+
return self::$hslRegex;
701+
}
702+
703+
protected static function isValidBorderColor(string $color): bool
704+
{
705+
if (array_key_exists($color, self::$colorKeywords)) {
706+
return true;
707+
} elseif (Hex::isValid(ltrim($color, '#'))) {
708+
return true;
709+
} elseif (preg_match(self::getRgbRegex(), $color)) {
710+
return true;
711+
} elseif (preg_match(self::getHslRegex(), $color)) {
712+
return true;
713+
}
714+
715+
return false;
716+
}
717+
718+
protected static function isValidBorderWidth(string $size): bool
719+
{
720+
return array_key_exists($size, self::$namedWidths)
721+
?: self::cssToAbsolute($size)->isSpecified();
722+
}
723+
724+
protected static function readValues(string $fullValue): array
725+
{
726+
$values = array();
727+
$offset = 0;
728+
while (true) {
729+
$readValue = self::readNextValue($fullValue, $offset);
730+
if ($readValue === null) {
731+
break;
732+
}
733+
$values[] = $readValue;
734+
}
735+
736+
return $values;
737+
}
738+
739+
protected static function readNextValue(string $values, int &$offset)
740+
{
741+
$value = '';
742+
$length = mb_strlen($values);
743+
$afterSpace = false;
744+
$depth = 0;
745+
for ($i = 0; $i < $length; $i += 1) {
746+
$ch = mb_substr($values, $offset, 1);
747+
$offset += 1;
748+
if ($ch === '') {
749+
break;
750+
} elseif (preg_match('/\\s/', $ch)) {
751+
if ($depth > 0) {
752+
$value .= ' ';
753+
} elseif ($value !== '') {
754+
$afterSpace = true;
755+
}
756+
continue;
757+
} elseif ($ch === ',') {
758+
if ($afterSpace) {
759+
break;
760+
}
761+
$value .= $ch;
762+
continue;
763+
} elseif ($ch === '(') {
764+
$afterSpace = false;
765+
$value .= $ch;
766+
$depth += 1;
767+
continue;
768+
} elseif ($ch === ')' && $depth > 0) {
769+
$value .= $ch;
770+
$depth -= 1;
771+
if ($depth === 0) {
772+
break;
773+
}
774+
continue;
775+
} elseif (preg_match('/[0-9a-zA-Z%,#.]/', $ch)) {
776+
if ($afterSpace) {
777+
$offset -= 1; // Backup one char
778+
break;
779+
}
780+
781+
$value .= $ch;
782+
continue;
783+
}
784+
785+
if ($value !== '') {
786+
break;
787+
}
788+
}
789+
790+
return $value === '' ? null : $value;
791+
}
792+
655793
/**
656794
* Parse image node
657795
*
@@ -753,6 +891,19 @@ protected static function parseImage($node, $element)
753891
return $newElement;
754892
}
755893

894+
protected static $borderStyles = array(
895+
'none' => 'none',
896+
'hidden' => 'none',
897+
'dotted' => 'dotted',
898+
'dashed' => 'dashed',
899+
'solid' => 'single',
900+
'double' => 'double',
901+
'groove' => 'threeDEngrave',
902+
'ridge' => 'threeDEmboss',
903+
'inset' => 'inset',
904+
'outset' => 'outset',
905+
);
906+
756907
/**
757908
* Transforms a CSS border style into a word border style
758909
* @param mixed $styles
@@ -761,22 +912,9 @@ protected static function mapBorderStyle(DOMNode $node, &$styles, string $cssSty
761912
{
762913
$cssStyles = self::expandBorderSides($node, $cssStyles);
763914

764-
$mapping = array(
765-
'none' => 'none',
766-
'hidden' => 'none',
767-
'dotted' => 'dotted',
768-
'dashed' => 'dashed',
769-
'solid' => 'single',
770-
'double' => 'double',
771-
'groove' => 'threeDEngrave',
772-
'ridge' => 'threeDEmboss',
773-
'inset' => 'inset',
774-
'outset' => 'outset',
775-
);
776-
777915
foreach ($cssStyles as $side => $cssStyle) {
778916
$existingBorder = self::getBorderSide($styles, $side);
779-
$existingBorder->setStyle(new BorderStyle($mapping[$cssStyle] ?? 'single'));
917+
$existingBorder->setStyle(new BorderStyle(self::$borderStyles[$cssStyle] ?? 'single'));
780918
}
781919
}
782920

@@ -943,17 +1081,86 @@ protected static function mapBorderColor(DOMNode $node, &$styles, string $cssCol
9431081
if (array_key_exists($cssColor, self::$colorKeywords)) {
9441082
$cssColor = self::$colorKeywords[$cssColor];
9451083
}
946-
$existingBorder->setColor(BasicColor::fromMixed(ltrim($cssColor, '#')));
1084+
if (preg_match(self::getRgbRegex(), $cssColor, $matches)) {
1085+
$colors = array($matches[1], $matches[2], $matches[3]);
1086+
foreach ($colors as &$color) {
1087+
if (strpos($color, '%') !== false) {
1088+
$color = (int) round((float) $color * 2.55);
1089+
} else {
1090+
$color = (int) $color;
1091+
}
1092+
}
1093+
$color = new Rgb(...$colors);
1094+
} elseif (preg_match(self::getHslRegex(), $cssColor, $matches)) {
1095+
$hue = (float) ($matches[1]) / 360;
1096+
$sat = (float) ($matches[2]) / 100;
1097+
$light = (float) ($matches[3]) / 100;
1098+
1099+
$color = new Rgb(...self::hslToRgb($hue, $sat, $light));
1100+
} else {
1101+
$color = BasicColor::fromMixed(ltrim($cssColor, '#'));
1102+
}
1103+
$existingBorder->setColor($color);
9471104
}
9481105
}
9491106

1107+
/**
1108+
* @see https://www.w3.org/TR/css-color-3/#hsl-color
1109+
*/
1110+
protected function hslToRgb(float $hue, float $sat, float $light)
1111+
{
1112+
$m2 = $light < .5 ? $light * ($sat + $light) : $light + $sat - $light * $sat;
1113+
$m1 = $light * 2 - $m2;
1114+
1115+
return array(
1116+
self::hueToRgb($m1, $m2, $hue + 1 / 3),
1117+
self::hueToRgb($m1, $m2, $hue),
1118+
self::hueToRgb($m1, $m2, $hue - 1 / 3),
1119+
);
1120+
}
1121+
1122+
protected function hueToRgb(float $m1, float $m2, float $hue): int
1123+
{
1124+
if ($hue < 0) {
1125+
$hue += 1;
1126+
} elseif ($hue > 1) {
1127+
$hue -= 1;
1128+
}
1129+
1130+
if ($hue * 6 < 1) {
1131+
return (int) round(($m1 + ($m2 - $m1) * $hue * 6) * 255);
1132+
} elseif ($hue * 2 < 1) {
1133+
return (int) round(($m2) * 255);
1134+
} elseif ($hue * 3 < 2) {
1135+
return (int) round(($m1 + ($m2 - $m1) * (2 / 3 - $hue) * 6) * 255);
1136+
}
1137+
1138+
return (int) round($m1 * 255);
1139+
}
1140+
1141+
protected static $namedWidths = array(
1142+
'thin' => '1px',
1143+
'medium' => '3px',
1144+
'thick' => '5px',
1145+
);
1146+
9501147
protected static function mapBorderWidth(DOMNode $node, &$styles, string $cssSizes)
9511148
{
9521149
$cssSizes = self::expandBorderSides($node, $cssSizes);
953-
9541150
foreach ($cssSizes as $side => $cssSize) {
9551151
$existingBorder = self::getBorderSide($styles, $side);
956-
$existingBorder->setSize(self::cssToAbsolute($cssSize));
1152+
1153+
if (array_key_exists($cssSize, self::$namedWidths)) {
1154+
$cssSize = self::$namedWidths[$cssSize];
1155+
}
1156+
1157+
$absolute = self::cssToAbsolute($cssSize);
1158+
1159+
if (!$absolute->isSpecified()) {
1160+
$absolute = new Absolute(0);
1161+
}
1162+
1163+
$existingBorder->setSize($absolute);
9571164
}
9581165
}
9591166

@@ -970,7 +1177,7 @@ protected static function expandBorderSides(DOMNode $node, string $valuesString)
9701177
}
9711178

9721179
$sides = $sideMapping[$node->nodeName];
973-
$values = explode(' ', $valuesString);
1180+
$values = self::readValues($valuesString);
9741181
if (count($values) > count($sides)) {
9751182
trigger_error(sprintf('Provided `%s` style `%s` had more than %d values', $node->nodeName, $valuesString, count($sides)), E_USER_WARNING);
9761183
$values = array_slice($values, 0, count($sides));

0 commit comments

Comments
 (0)