1212use LibreSign \XObjectTemplate \Pdf \Svg \SvgColorResolver ;
1313use PHPUnit \Framework \Attributes \DataProvider ;
1414use PHPUnit \Framework \TestCase ;
15+ use ReflectionMethod ;
1516
1617final class SvgColorResolverTest extends TestCase
1718{
@@ -41,7 +42,7 @@ public function testResolveFillColorFallsBackToStyleClassAncestorAndDefault(): v
4142 self ::assertSame ('rgb(10,20,30) ' , $ resolver ->resolveFillColor ($ styleElement , []));
4243
4344 $ classElement = $ this ->createElement ('path ' , [
44- 'class ' => 'primary secondary ' ,
45+ 'class ' => ' primary secondary ' ,
4546 ]);
4647 self ::assertSame ('#112233 ' , $ resolver ->resolveFillColor ($ classElement , ['secondary ' => '#112233 ' ]));
4748
@@ -93,27 +94,49 @@ public function testExtractValueFromStyleAttributeReturnsRequestedProperty(
9394 self ::assertSame ($ expected , $ result );
9495 }
9596
97+ public function testResolveColorAttributeRemainsPublicForFactoryCollaborators (): void
98+ {
99+ $ method = new ReflectionMethod (SvgColorResolver::class, 'resolveColorAttribute ' );
100+
101+ self ::assertTrue ($ method ->isPublic ());
102+ }
103+
96104 /**
97105 * @return iterable<string, array{style: string, property: string, expected: ?string, useColorExtractor?: bool}>
98106 */
99107 public static function provideExtractValueFromStyleAttributeScenarios (): iterable
100108 {
101- yield 'extract stroke-width from style ' => [
109+ yield 'extract generic property ' => [
102110 'style ' => 'fill:#fff; stroke-width: 2.5 ; ' ,
103111 'property ' => 'stroke-width ' ,
104112 'expected ' => '2.5 ' ,
105113 ];
114+
106115 yield 'extract fill color case-insensitive ' => [
107116 'style ' => ' FiLl : #fff ; ' ,
108117 'property ' => 'fill ' ,
109118 'expected ' => '#fff ' ,
110119 'useColorExtractor ' => true ,
111120 ];
121+
122+ yield 'extract dotted property name ' => [
123+ 'style ' => 'fill.opacity: 0.5 ' ,
124+ 'property ' => 'fill.opacity ' ,
125+ 'expected ' => '0.5 ' ,
126+ ];
127+
128+ yield 'extract value containing colon characters ' => [
129+ 'style ' => 'fill:url(http://example.com/a:b.svg) ' ,
130+ 'property ' => 'fill ' ,
131+ 'expected ' => 'url(http://example.com/a:b.svg) ' ,
132+ ];
133+
112134 yield 'empty style returns null ' => [
113135 'style ' => '' ,
114136 'property ' => 'fill ' ,
115137 'expected ' => null ,
116138 ];
139+
117140 yield 'missing property returns null ' => [
118141 'style ' => 'stroke:#000 ' ,
119142 'property ' => 'fill ' ,
@@ -138,10 +161,29 @@ public static function provideNormalizeColorScenarios(): iterable
138161 yield 'none sentinel ' => ['input ' => 'none ' , 'expected ' => 'none ' ];
139162 yield 'short hex ' => ['input ' => '#abc ' , 'expected ' => '#abc ' ];
140163 yield 'long hex uppercased and trimmed ' => ['input ' => ' #AABBCC ' , 'expected ' => '#aabbcc ' ];
141- yield 'rgb notation with clamping ' => ['input ' => 'rgb(300,-1,12) ' , 'expected ' => null ];
164+ yield 'hex with invalid prefix ' => ['input ' => 'x#abc ' , 'expected ' => null ];
165+ yield 'hex with invalid suffix ' => ['input ' => '#abcx ' , 'expected ' => null ];
166+ yield 'hex with invalid medium length ' => ['input ' => '#1234 ' , 'expected ' => null ];
167+ yield 'rgb notation rejects negative channel ' => ['input ' => 'rgb(300,-1,12) ' , 'expected ' => null ];
142168 yield 'rgb notation valid ' => ['input ' => 'rgb(255, 0, 12) ' , 'expected ' => '#ff000c ' ];
169+ yield 'rgb notation clamps overflowing red ' => ['input ' => 'rgb(256,0,0) ' , 'expected ' => '#ff0000 ' ];
170+ yield 'rgb notation preserves max green ' => ['input ' => 'rgb(0,255,0) ' , 'expected ' => '#00ff00 ' ];
171+ yield 'rgb notation clamps overflowing green ' => ['input ' => 'rgb(0,256,0) ' , 'expected ' => '#00ff00 ' ];
172+ yield 'rgb notation preserves zero blue ' => ['input ' => 'rgb(0,0,0) ' , 'expected ' => '#000000 ' ];
173+ yield 'rgb notation clamps overflowing blue ' => ['input ' => 'rgb(0,0,256) ' , 'expected ' => '#0000ff ' ];
174+ yield 'rgb notation rejects prefixed content ' => ['input ' => 'prefix rgb(255,0,12) ' , 'expected ' => null ];
175+ yield 'rgb notation rejects suffixed content ' => ['input ' => 'rgb(255,0,12) suffix ' , 'expected ' => null ];
176+ yield 'rgb notation rejects missing closing parenthesis ' => ['input ' => 'rgb(255,0,12 ' , 'expected ' => null ];
177+ yield 'rgb notation rejects missing opening marker ' => ['input ' => '255,0,12) ' , 'expected ' => null ];
178+ yield 'rgb notation rejects contaminated channel ' => ['input ' => 'rgb(12x,0,0) ' , 'expected ' => null ];
179+ yield 'named black ' => ['input ' => 'black ' , 'expected ' => '#000000 ' ];
180+ yield 'named white ' => ['input ' => 'white ' , 'expected ' => '#ffffff ' ];
181+ yield 'named red ' => ['input ' => 'red ' , 'expected ' => '#ff0000 ' ];
182+ yield 'named green ' => ['input ' => 'green ' , 'expected ' => '#008000 ' ];
143183 yield 'named gray alias ' => ['input ' => 'grey ' , 'expected ' => '#808080 ' ];
184+ yield 'named gray canonical ' => ['input ' => 'gray ' , 'expected ' => '#808080 ' ];
144185 yield 'named blue ' => ['input ' => 'blue ' , 'expected ' => '#0000ff ' ];
186+ yield 'named yellow ' => ['input ' => 'yellow ' , 'expected ' => '#ffff00 ' ];
145187 yield 'invalid color ' => ['input ' => 'chartreuse-ish ' , 'expected ' => null ];
146188 }
147189
0 commit comments