32
32
use PhpOffice \PhpWord \Style \BorderSide ;
33
33
use PhpOffice \PhpWord \Style \BorderStyle ;
34
34
use PhpOffice \PhpWord \Style \Colors \BasicColor ;
35
+ use PhpOffice \PhpWord \Style \Colors \Hex ;
36
+ use PhpOffice \PhpWord \Style \Colors \Rgb ;
35
37
use PhpOffice \PhpWord \Style \Image ;
36
38
use PhpOffice \PhpWord \Style \Lengths \Absolute ;
37
39
use PhpOffice \PhpWord \Style \Lengths \Length ;
@@ -638,10 +640,8 @@ private static function mapStyleDeclaration(DOMNode $node, string $property, str
638
640
}
639
641
break ;
640
642
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 );
645
645
}
646
646
break ;
647
647
case 'visibility ' :
@@ -652,6 +652,144 @@ private static function mapStyleDeclaration(DOMNode $node, string $property, str
652
652
}
653
653
}
654
654
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
+
655
793
/**
656
794
* Parse image node
657
795
*
@@ -753,6 +891,19 @@ protected static function parseImage($node, $element)
753
891
return $ newElement ;
754
892
}
755
893
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
+
756
907
/**
757
908
* Transforms a CSS border style into a word border style
758
909
* @param mixed $styles
@@ -761,22 +912,9 @@ protected static function mapBorderStyle(DOMNode $node, &$styles, string $cssSty
761
912
{
762
913
$ cssStyles = self ::expandBorderSides ($ node , $ cssStyles );
763
914
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
-
777
915
foreach ($ cssStyles as $ side => $ cssStyle ) {
778
916
$ existingBorder = self ::getBorderSide ($ styles , $ side );
779
- $ existingBorder ->setStyle (new BorderStyle ($ mapping [$ cssStyle ] ?? 'single ' ));
917
+ $ existingBorder ->setStyle (new BorderStyle (self :: $ borderStyles [$ cssStyle ] ?? 'single ' ));
780
918
}
781
919
}
782
920
@@ -943,17 +1081,86 @@ protected static function mapBorderColor(DOMNode $node, &$styles, string $cssCol
943
1081
if (array_key_exists ($ cssColor , self ::$ colorKeywords )) {
944
1082
$ cssColor = self ::$ colorKeywords [$ cssColor ];
945
1083
}
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 );
947
1104
}
948
1105
}
949
1106
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
+
950
1147
protected static function mapBorderWidth (DOMNode $ node , &$ styles , string $ cssSizes )
951
1148
{
952
1149
$ cssSizes = self ::expandBorderSides ($ node , $ cssSizes );
953
-
954
1150
foreach ($ cssSizes as $ side => $ cssSize ) {
955
1151
$ 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 );
957
1164
}
958
1165
}
959
1166
@@ -970,7 +1177,7 @@ protected static function expandBorderSides(DOMNode $node, string $valuesString)
970
1177
}
971
1178
972
1179
$ sides = $ sideMapping [$ node ->nodeName ];
973
- $ values = explode ( ' ' , $ valuesString );
1180
+ $ values = self :: readValues ( $ valuesString );
974
1181
if (count ($ values ) > count ($ sides )) {
975
1182
trigger_error (sprintf ('Provided `%s` style `%s` had more than %d values ' , $ node ->nodeName , $ valuesString , count ($ sides )), E_USER_WARNING );
976
1183
$ values = array_slice ($ values , 0 , count ($ sides ));
0 commit comments