Skip to content

Commit 8fc4e25

Browse files
committed
test: harden svg arc/color mutation and rector compliance
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 37cdea7 commit 8fc4e25

2 files changed

Lines changed: 289 additions & 82 deletions

File tree

tests/Unit/Pdf/Svg/SvgArcConverterTest.php

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414

1515
final class SvgArcConverterTest extends TestCase
1616
{
17+
/**
18+
* @param array<int, mixed> $arguments
19+
*/
20+
private static function invokePrivateMethod(object $object, string $methodName, array $arguments = []): mixed
21+
{
22+
$reflection = new \ReflectionClass($object);
23+
$method = $reflection->getMethod($methodName);
24+
$method->setAccessible(true);
25+
26+
return $method->invokeArgs($object, $arguments);
27+
}
28+
1729
/**
1830
* @param array<int, float> $expected
1931
* @param array<int, float> $actual
@@ -60,6 +72,252 @@ private static function createCurveGenerationParams(
6072
);
6173
}
6274

75+
public function testNormalizeArcRadiiReturnsSameInstanceWhenScaleIsExactlyOne(): void
76+
{
77+
$converter = new SvgArcConverter();
78+
$params = new ArcParams(
79+
0.0,
80+
0.0,
81+
2.0,
82+
0.0,
83+
1.0,
84+
1.0,
85+
1.0,
86+
0.0,
87+
0,
88+
1,
89+
);
90+
91+
$normalized = self::invokePrivateMethod($converter, 'normalizeArcRadii', [$params]);
92+
93+
self::assertInstanceOf(ArcParams::class, $normalized);
94+
self::assertSame($params, $normalized);
95+
}
96+
97+
public function testNormalizeArcRadiiMatchesExpectedScaleForRotatedArc(): void
98+
{
99+
$converter = new SvgArcConverter();
100+
101+
$cosTh = cos(deg2rad(45.0));
102+
$sinTh = sin(deg2rad(45.0));
103+
$params = new ArcParams(
104+
0.0,
105+
0.0,
106+
6.0,
107+
2.0,
108+
1.0,
109+
2.0,
110+
$cosTh,
111+
$sinTh,
112+
0,
113+
1,
114+
);
115+
116+
$normalized = self::invokePrivateMethod($converter, 'normalizeArcRadii', [$params]);
117+
118+
self::assertInstanceOf(ArcParams::class, $normalized);
119+
self::assertNotSame($params, $normalized);
120+
121+
$deltaX2 = ($params->fromX - $params->toX) / 2.0;
122+
$deltaY2 = ($params->fromY - $params->toY) / 2.0;
123+
$primeX = $params->cosTh * $deltaX2 + $params->sinTh * $deltaY2;
124+
$primeY = -$params->sinTh * $deltaX2 + $params->cosTh * $deltaY2;
125+
$radiusX2 = $params->radiusX * $params->radiusX;
126+
$radiusY2 = $params->radiusY * $params->radiusY;
127+
$scale = ($primeX * $primeX) / $radiusX2 + ($primeY * $primeY) / $radiusY2;
128+
$scaleFactor = sqrt($scale);
129+
130+
self::assertGreaterThan(1.0, $scale);
131+
self::assertEqualsWithDelta($params->radiusX * $scaleFactor, $normalized->radiusX, 1.0E-12);
132+
self::assertEqualsWithDelta($params->radiusY * $scaleFactor, $normalized->radiusY, 1.0E-12);
133+
}
134+
135+
public function testCalculateArcCenterUsesZeroSquareRootWhenDenominatorBucketIsZero(): void
136+
{
137+
$converter = new SvgArcConverter();
138+
$params = new ArcParams(
139+
2.0E-6,
140+
2.0E-6,
141+
0.0,
142+
0.0,
143+
1.0,
144+
1.0,
145+
1.0,
146+
0.0,
147+
0,
148+
1,
149+
);
150+
151+
[$centerX, $centerY] = self::invokePrivateMethod($converter, 'calculateArcCenter', [$params]);
152+
153+
self::assertEqualsWithDelta(1.0E-6, $centerX, 1.0E-15);
154+
self::assertEqualsWithDelta(1.0E-6, $centerY, 1.0E-15);
155+
}
156+
157+
public function testCalculateArcCenterKeepsMidpointWhenDenominatorBucketRoundsUpButFloorIsZero(): void
158+
{
159+
$converter = new SvgArcConverter();
160+
$params = new ArcParams(
161+
0.0,
162+
0.0,
163+
1.5491933384829667E-5,
164+
0.0,
165+
1.0,
166+
1.0,
167+
1.0,
168+
0.0,
169+
0,
170+
1,
171+
);
172+
173+
[$centerX, $centerY] = self::invokePrivateMethod($converter, 'calculateArcCenter', [$params]);
174+
175+
self::assertEqualsWithDelta(7.745966692414834E-6, $centerX, 1.0E-15);
176+
self::assertEqualsWithDelta(0.0, $centerY, 1.0E-15);
177+
}
178+
179+
public function testCalculateArcAnglesUsesHalfPiBranchAndSweepAdjustmentForNearZeroMagnitude(): void
180+
{
181+
$converter = new SvgArcConverter();
182+
$params = new ArcParams(
183+
0.0,
184+
0.0,
185+
0.0,
186+
0.0,
187+
1.0,
188+
1.0,
189+
1.0,
190+
0.0,
191+
0,
192+
0,
193+
);
194+
195+
[$startAngle, $deltaAngle] = self::invokePrivateMethod($converter, 'calculateArcAngles', [$params, 0.0, 0.0]);
196+
197+
self::assertEqualsWithDelta(0.0, $startAngle, 1.0E-12);
198+
self::assertEqualsWithDelta(-(3.0 * M_PI / 2.0), $deltaAngle, 1.0E-12);
199+
}
200+
201+
public function testCalculateArcAnglesUsesPiBranchForStableMagnitude(): void
202+
{
203+
$converter = new SvgArcConverter();
204+
$params = new ArcParams(
205+
2.0,
206+
0.0,
207+
0.0,
208+
0.0,
209+
1.0,
210+
1.0,
211+
1.0,
212+
0.0,
213+
0,
214+
1,
215+
);
216+
217+
[$startAngle, $deltaAngle] = self::invokePrivateMethod($converter, 'calculateArcAngles', [$params, 0.0, 0.0]);
218+
219+
self::assertEqualsWithDelta(0.0, $startAngle, 1.0E-12);
220+
self::assertEqualsWithDelta(M_PI, $deltaAngle, 1.0E-12);
221+
}
222+
223+
public function testCalculateArcAnglesUsesHalfPiWhenMagnitudeBucketRoundsUpButFloorIsZero(): void
224+
{
225+
$converter = new SvgArcConverter();
226+
$params = new ArcParams(
227+
0.0,
228+
0.0,
229+
1.5491933384829667E-5,
230+
0.0,
231+
1.0,
232+
1.0,
233+
1.0,
234+
0.0,
235+
0,
236+
1,
237+
);
238+
239+
[$startAngle, $deltaAngle] = self::invokePrivateMethod($converter, 'calculateArcAngles', [$params, 0.0, 0.0]);
240+
241+
self::assertEqualsWithDelta(M_PI, $startAngle, 1.0E-12);
242+
self::assertEqualsWithDelta(M_PI / 2.0, $deltaAngle, 1.0E-12);
243+
}
244+
245+
public function testGenerateArcCurvesUsesCeilToDetermineSegmentCount(): void
246+
{
247+
$converter = new SvgArcConverter();
248+
$deltaAngle = 1.1 * (M_PI / 2.0);
249+
$params = self::createCurveGenerationParams(0.0, 0.0, 10.0, 7.0, 1.0, 0.0, 0.0, $deltaAngle);
250+
251+
$curves = self::invokePrivateMethod(
252+
$converter,
253+
'generateArcCurves',
254+
[$params, 0.0, 0.0, 0.0, $deltaAngle],
255+
);
256+
257+
self::assertIsArray($curves);
258+
self::assertCount(2, $curves);
259+
}
260+
261+
public function testGenerateArcCurvesStillReturnsSingleCurveForZeroDeltaAngle(): void
262+
{
263+
$converter = new SvgArcConverter();
264+
$deltaAngle = 0.0;
265+
$params = self::createCurveGenerationParams(0.0, 0.0, 10.0, 7.0, 1.0, 0.0, 0.0, $deltaAngle);
266+
267+
$curves = self::invokePrivateMethod(
268+
$converter,
269+
'generateArcCurves',
270+
[$params, 0.0, 0.0, 0.0, $deltaAngle],
271+
);
272+
273+
self::assertIsArray($curves);
274+
self::assertCount(1, $curves);
275+
self::assertCurveMatches([10.0, 0.0, 10.0, 0.0, 10.0, 0.0], $curves[0], 1.0E-12);
276+
}
277+
278+
public function testGenerateArcCurvesKeepsAlphaAtZeroWhenAngleStepHitsThreshold(): void
279+
{
280+
$converter = new SvgArcConverter();
281+
$deltaAngle = 1.0E-10;
282+
$params = self::createCurveGenerationParams(0.0, 0.0, 9.0, 4.0, 1.0, 0.0, 0.0, $deltaAngle);
283+
284+
$curves = self::invokePrivateMethod(
285+
$converter,
286+
'generateArcCurves',
287+
[$params, 0.0, 0.0, 0.0, $deltaAngle],
288+
);
289+
290+
self::assertCount(1, $curves);
291+
self::assertEqualsWithDelta(9.0, $curves[0][0], 1.0E-12);
292+
self::assertEqualsWithDelta(0.0, $curves[0][1], 1.0E-12);
293+
}
294+
295+
public function testGenerateArcCurvesUsesSquaredTanHalfStepTermInAlpha(): void
296+
{
297+
$converter = new SvgArcConverter();
298+
$deltaAngle = M_PI / 3.0;
299+
$radiusX = 12.0;
300+
$radiusY = 8.0;
301+
$params = self::createCurveGenerationParams(0.0, 0.0, $radiusX, $radiusY, 1.0, 0.0, 0.0, $deltaAngle);
302+
303+
$curves = self::invokePrivateMethod(
304+
$converter,
305+
'generateArcCurves',
306+
[$params, 0.0, 0.0, 0.0, $deltaAngle],
307+
);
308+
309+
self::assertCount(1, $curves);
310+
311+
$tanHalfAngleStep = tan($deltaAngle / 2.0);
312+
$alpha = sin($deltaAngle) * (sqrt(4.0 + 3.0 * $tanHalfAngleStep * $tanHalfAngleStep) - 1.0) / 3.0;
313+
314+
$expectedControlX1 = $radiusX;
315+
$expectedControlY1 = $alpha * $radiusY;
316+
317+
self::assertEqualsWithDelta($expectedControlX1, $curves[0][0], 1.0E-12);
318+
self::assertEqualsWithDelta($expectedControlY1, $curves[0][1], 1.0E-12);
319+
}
320+
63321
public function testArcToBezierCurvesReturnsEmptyArrayWhenStartAndEndPointsMatch(): void
64322
{
65323
$converter = new SvgArcConverter();

0 commit comments

Comments
 (0)