77
88namespace LibreSign \XObjectTemplate \Tests \Unit \Pdf \Svg ;
99
10+ use LibreSign \XObjectTemplate \Pdf \Svg \ArcParams ;
1011use LibreSign \XObjectTemplate \Pdf \Svg \SvgArcConverter ;
1112use PHPUnit \Framework \Attributes \DataProvider ;
1213use PHPUnit \Framework \TestCase ;
1314
1415final class SvgArcConverterTest extends TestCase
1516{
17+ /**
18+ * @param array<int, float> $expected
19+ * @param array<int, float> $actual
20+ */
21+ private static function assertCurveMatches (array $ expected , array $ actual , float $ delta = 0.0001 ): void
22+ {
23+ self ::assertCount (count ($ expected ), $ actual );
24+
25+ foreach ($ expected as $ index => $ expectedValue ) {
26+ self ::assertEqualsWithDelta (
27+ $ expectedValue ,
28+ $ actual [$ index ],
29+ $ delta ,
30+ sprintf ('Curve index %d differs. ' , $ index ),
31+ );
32+ }
33+ }
34+
35+ /**
36+ * @param list<mixed> $arguments
37+ */
38+ private static function invokePrivateMethod (SvgArcConverter $ converter , string $ methodName , array $ arguments ): mixed
39+ {
40+ $ reflection = new \ReflectionClass ($ converter );
41+ $ method = $ reflection ->getMethod ($ methodName );
42+ $ method ->setAccessible (true );
43+
44+ return $ method ->invokeArgs ($ converter , $ arguments );
45+ }
46+
1647 public function testArcToBezierCurvesReturnsEmptyArrayWhenStartAndEndPointsMatch (): void
1748 {
1849 $ converter = new SvgArcConverter ();
1950
2051 self ::assertSame ([], $ converter ->arcToBezierCurves (10.0 , 10.0 , 5.0 , 6.0 , 30.0 , 0 , 1 , 10.0 , 10.0 ));
2152 }
2253
54+ public function testArcToBezierCurvesReturnsEmptyArrayWhenBothAxisDeltasStayBelowTolerance (): void
55+ {
56+ $ converter = new SvgArcConverter ();
57+
58+ self ::assertSame (
59+ [],
60+ $ converter ->arcToBezierCurves (10.0 , 10.0 , 5.0 , 6.0 , 30.0 , 0 , 1 , 10.0 + 5.0e-11 , 10.0 - 5.0e-11 ),
61+ );
62+ }
63+
64+ public function testArcToBezierCurvesDoesNotTreatSingleAxisDeltaAsSamePoint (): void
65+ {
66+ $ converter = new SvgArcConverter ();
67+
68+ $ curves = $ converter ->arcToBezierCurves (10.0 , 10.0 , 5.0 , 6.0 , 30.0 , 0 , 1 , 10.0 , 14.0 );
69+
70+ self ::assertNotSame ([], $ curves );
71+ }
72+
73+ public function testArcToBezierCurvesDoesNotTreatExactToleranceDeltaAsSamePoint (): void
74+ {
75+ $ converter = new SvgArcConverter ();
76+
77+ $ curves = $ converter ->arcToBezierCurves (
78+ 10.0 ,
79+ 10.0 ,
80+ 5.0 ,
81+ 6.0 ,
82+ 30.0 ,
83+ 0 ,
84+ 1 ,
85+ 10.0 + 1.0e-10 ,
86+ 10.0 ,
87+ );
88+
89+ self ::assertNotSame ([], $ curves );
90+ }
91+
2392 public function testArcToBezierCurvesFallsBackToDegenerateLineWhenAnyRadiusIsZero (): void
2493 {
2594 $ converter = new SvgArcConverter ();
@@ -30,6 +99,251 @@ public function testArcToBezierCurvesFallsBackToDegenerateLineWhenAnyRadiusIsZer
3099 );
31100 }
32101
102+ public function testArcToBezierCurvesFallsBackToDegenerateLineWhenRadiusYIsTiny (): void
103+ {
104+ $ converter = new SvgArcConverter ();
105+
106+ self ::assertSame (
107+ [[20.0 , 30.0 , 20.0 , 30.0 , 20.0 , 30.0 ]],
108+ $ converter ->arcToBezierCurves (0.0 , 0.0 , 5.0 , 1.0e-12 , 0.0 , 0 , 1 , 20.0 , 30.0 ),
109+ );
110+ }
111+
112+ public function testArcToBezierCurvesDoesNotDegenerateWhenRadiiAreJustAboveTolerance (): void
113+ {
114+ $ converter = new SvgArcConverter ();
115+
116+ $ curves = $ converter ->arcToBezierCurves (0.0 , 0.0 , 1.1e-10 , 2.2e-10 , 0.0 , 0 , 1 , 20.0 , 30.0 );
117+
118+ self ::assertNotSame ([], $ curves );
119+ self ::assertNotSame ([[20.0 , 30.0 , 20.0 , 30.0 , 20.0 , 30.0 ]], $ curves );
120+ }
121+
122+ public function testArcToBezierCurvesDoesNotDegenerateAtExactRadiusTolerance (): void
123+ {
124+ $ converter = new SvgArcConverter ();
125+
126+ $ curves = $ converter ->arcToBezierCurves (
127+ 0.0 ,
128+ 0.0 ,
129+ 1.0e-10 ,
130+ 2.0e-10 ,
131+ 0.0 ,
132+ 0 ,
133+ 1 ,
134+ 20.0 ,
135+ 30.0 ,
136+ );
137+
138+ self ::assertNotSame ([], $ curves );
139+ self ::assertNotSame ([[20.0 , 30.0 , 20.0 , 30.0 , 20.0 , 30.0 ]], $ curves );
140+ }
141+
142+ public function testArcToBezierCurvesUsesSweepAndLargeArcFlagsToChooseDifferentSolutions (): void
143+ {
144+ $ converter = new SvgArcConverter ();
145+
146+ $ smallSweep = $ converter ->arcToBezierCurves (10.0 , 0.0 , 10.0 , 10.0 , 0.0 , 0 , 1 , 0.0 , 10.0 );
147+ $ smallReverseSweep = $ converter ->arcToBezierCurves (10.0 , 0.0 , 10.0 , 10.0 , 0.0 , 0 , 0 , 0.0 , 10.0 );
148+ $ largeSweep = $ converter ->arcToBezierCurves (10.0 , 0.0 , 10.0 , 10.0 , 0.0 , 1 , 1 , 0.0 , 10.0 );
149+
150+ self ::assertNotSame ([], $ smallSweep );
151+ self ::assertNotSame ([], $ smallReverseSweep );
152+ self ::assertNotSame ([], $ largeSweep );
153+ self ::assertNotEquals ($ smallSweep , $ smallReverseSweep );
154+ self ::assertNotEquals ($ smallSweep , $ largeSweep );
155+ }
156+
157+ public function testArcToBezierCurvesReturnsFiniteControlPointsForNormalizedRotatedArc (): void
158+ {
159+ $ converter = new SvgArcConverter ();
160+
161+ $ curves = $ converter ->arcToBezierCurves (0.0 , 0.0 , 4.0 , 3.0 , 35.0 , 1 , 0 , 25.0 , 8.0 );
162+
163+ self ::assertNotSame ([], $ curves );
164+
165+ foreach ($ curves as $ curve ) {
166+ foreach ($ curve as $ value ) {
167+ self ::assertTrue (is_finite ($ value ));
168+ }
169+ }
170+ }
171+
172+ public function testArcToBezierCurvesMatchesExpectedHalfEllipseControlPoints (): void
173+ {
174+ $ converter = new SvgArcConverter ();
175+
176+ $ curves = $ converter ->arcToBezierCurves (0.0 , 5.0 , 10.0 , 5.0 , 0.0 , 0 , 1 , 20.0 , 5.0 );
177+
178+ self ::assertCount (2 , $ curves );
179+ self ::assertCurveMatches (
180+ [
181+ -6.7182135842927015E-16 ,
182+ 2.257081148225684 ,
183+ 4.514162296451365 ,
184+ 5.038660188219526E-16 ,
185+ 9.999999999999998 ,
186+ 0.0 ,
187+ ],
188+ $ curves [0 ],
189+ );
190+ self ::assertCurveMatches (
191+ [
192+ 15.485837703548633 ,
193+ -5.038660188219526E-16 ,
194+ 20.0 ,
195+ 2.2570811482256823 ,
196+ 20.0 ,
197+ 4.999999999999999 ,
198+ ],
199+ $ curves [1 ],
200+ );
201+ }
202+
203+ public function testArcToBezierCurvesMatchesExpectedNormalizedArcControlPoints (): void
204+ {
205+ $ converter = new SvgArcConverter ();
206+
207+ $ curves = $ converter ->arcToBezierCurves (0.0 , 0.0 , 5.0 , 5.0 , 0.0 , 0 , 1 , 30.0 , 0.0 );
208+
209+ self ::assertCount (2 , $ curves );
210+ self ::assertCurveMatches (
211+ [
212+ -1.0077320376439053E-15 ,
213+ -8.22875655532295 ,
214+ 6.7712434446770455 ,
215+ -14.999999999999998 ,
216+ 14.999999999999996 ,
217+ -15.0 ,
218+ ],
219+ $ curves [0 ],
220+ );
221+ self ::assertCurveMatches (
222+ [
223+ 23.228756555322946 ,
224+ -15.000000000000002 ,
225+ 29.999999999999996 ,
226+ -8.228756555322954 ,
227+ 30.0 ,
228+ -3.67394039744206E-15 ,
229+ ],
230+ $ curves [1 ],
231+ );
232+ }
233+
234+ public function testArcToBezierCurvesMatchesExpectedQuarterArcSolutionsForFlagVariants (): void
235+ {
236+ $ converter = new SvgArcConverter ();
237+
238+ $ largeSweep = $ converter ->arcToBezierCurves (10.0 , 0.0 , 10.0 , 10.0 , 0.0 , 1 , 1 , 0.0 , 10.0 );
239+ $ smallSweep = $ converter ->arcToBezierCurves (10.0 , 0.0 , 10.0 , 10.0 , 0.0 , 0 , 1 , 0.0 , 10.0 );
240+
241+ self ::assertCount (2 , $ largeSweep );
242+ self ::assertCount (2 , $ smallSweep );
243+ self ::assertCurveMatches (
244+ [
245+ 20.95014085253355 ,
246+ 6.808005228802601 ,
247+ 20.95014085253355 ,
248+ 13.191994771197399 ,
249+ 17.071067811865476 ,
250+ 17.071067811865476 ,
251+ ],
252+ $ largeSweep [0 ],
253+ );
254+ self ::assertCurveMatches (
255+ [
256+ 10.95014085253355 ,
257+ -3.191994771197398 ,
258+ 10.95014085253355 ,
259+ 3.191994771197398 ,
260+ 7.0710678118654755 ,
261+ 7.071067811865475 ,
262+ ],
263+ $ smallSweep [0 ],
264+ );
265+ }
266+
267+ public function testCalculatePrimeCoordinatesMatchesExpectedRotatedValues (): void
268+ {
269+ $ converter = new SvgArcConverter ();
270+ $ params = new ArcParams (
271+ 0.0 ,
272+ 0.0 ,
273+ 60.0 ,
274+ 0.0 ,
275+ 40.0 ,
276+ 20.0 ,
277+ cos (deg2rad (45.0 )),
278+ sin (deg2rad (45.0 )),
279+ 1 ,
280+ 1 ,
281+ );
282+
283+ $ primeCoordinates = self ::invokePrivateMethod ($ converter , 'calculatePrimeCoordinates ' , [$ params ]);
284+
285+ self ::assertSame ([-21.213203435596427 , 21.213203435596423 ], $ primeCoordinates );
286+ }
287+
288+ public function testGenerateArcCurvesUsesSingleSegmentBelowNinetyDegrees (): void
289+ {
290+ $ converter = new SvgArcConverter ();
291+
292+ $ curves = self ::invokePrivateMethod (
293+ $ converter ,
294+ 'generateArcCurves ' ,
295+ [0.0 , 0.0 , 10.0 , 10.0 , 1.0 , 0.0 , 0.0 , M_PI / 4.0 ],
296+ );
297+
298+ self ::assertCount (1 , $ curves );
299+ self ::assertCurveMatches (
300+ [
301+ 10.0 ,
302+ 2.6511477349130246 ,
303+ 8.94571235314983 ,
304+ 5.1964232705811195 ,
305+ 7.0710678118654755 ,
306+ 7.071067811865475 ,
307+ ],
308+ $ curves [0 ],
309+ );
310+ }
311+
312+ public function testGenerateArcCurvesSplitsLargerAnglesIntoExpectedSegments (): void
313+ {
314+ $ converter = new SvgArcConverter ();
315+
316+ $ curves = self ::invokePrivateMethod (
317+ $ converter ,
318+ 'generateArcCurves ' ,
319+ [0.0 , 0.0 , 10.0 , 10.0 , 1.0 , 0.0 , 0.0 , 3.0 * M_PI / 4.0 ],
320+ );
321+
322+ self ::assertCount (2 , $ curves );
323+ self ::assertCurveMatches (
324+ [
325+ 10.0 ,
326+ 4.036465386317128 ,
327+ 7.556042077759557 ,
328+ 7.6941068964541515 ,
329+ 3.8268343236508984 ,
330+ 9.238795325112868 ,
331+ ],
332+ $ curves [0 ],
333+ );
334+ self ::assertCurveMatches (
335+ [
336+ 0.09762656954223958 ,
337+ 10.783483753771584 ,
338+ -4.216855765175856 ,
339+ 9.925279858555093 ,
340+ -7.071067811865475 ,
341+ 7.0710678118654755 ,
342+ ],
343+ $ curves [1 ],
344+ );
345+ }
346+
33347 #[DataProvider('provideArcScenarios ' )]
34348 public function testArcToBezierCurvesGeneratesExpectedCurveShape (
35349 float $ fromX ,
@@ -69,7 +383,18 @@ public function testArcToBezierCurvesGeneratesExpectedCurveShape(
69383 }
70384
71385 /**
72- * @return iterable<string, array{fromX: float, fromY: float, radiusX: float, radiusY: float, rotation: float, largeArc: int, sweep: int, toX: float, toY: float, expectedSegmentCount: int}>
386+ * @return iterable<string, array{
387+ * fromX: float,
388+ * fromY: float,
389+ * radiusX: float,
390+ * radiusY: float,
391+ * rotation: float,
392+ * largeArc: int,
393+ * sweep: int,
394+ * toX: float,
395+ * toY: float,
396+ * expectedSegmentCount: int,
397+ * }>
73398 */
74399 public static function provideArcScenarios (): iterable
75400 {
0 commit comments