Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
312 / 312
100.00% covered (success)
100.00%
26 / 26
CRAP
100.00% covered (success)
100.00%
1 / 1
Path
100.00% covered (success)
100.00%
312 / 312
100.00% covered (success)
100.00%
26 / 26
71
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFillColor
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 setStrokeColor
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 setStroke
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 setStyle
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 openLayer
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 closeLayer
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getStreams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 drawLine
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 drawRectangle
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 drawRoundedRectangle
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
3
 drawSquare
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 drawRoundedSquare
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 drawPolygon
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
6
 drawEllipse
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
2
 drawCircle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 drawArc
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 drawChord
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 drawPie
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 drawOpenCubicBezierCurve
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 drawClosedCubicBezierCurve
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 drawOpenQuadraticBezierCurve
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 drawClosedQuadraticBezierCurve
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 calculateDegrees
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
10
 calculateArc
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Document\Page;
16
17use Pop\Color\Color;
18use OutOfRangeException;
19
20/**
21 * Pdf page path class
22 *
23 * @category   Pop
24 * @package    Pop\Pdf
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    6.0.0
29 */
30class Path
31{
32
33    /**
34     * Style constants
35     */
36    const STROKE                     = 'S';
37    const STROKE_CLOSE               = 's';
38    const FILL                       = 'F';
39    const FILL_EVEN_ODD              = 'f*';
40    const FILL_STROKE                = 'B';
41    const FILL_STROKE_EVEN_ODD       = 'B*';
42    const FILL_STROKE_CLOSE          = 'b';
43    const FILL_STROKE_CLOSE_EVEN_ODD = 'b*';
44    const CLIPPING                   = 'W';
45    const CLIPPING_FILL              = 'W F';
46    const CLIPPING_NO_STYLE          = 'W n';
47    const CLIPPING_EVEN_ODD          = 'W*';
48    const CLIPPING_EVEN_ODD_FILL     = 'W* F';
49    const CLIPPING_EVEN_ODD_NO_STYLE = 'W* n';
50    const NO_STYLE                   = 'n';
51
52    /**
53     * Allowed styles
54     * @var array
55     */
56    protected array $allowedStyles = [
57        'S', 's', 'F', 'f*', 'B', 'B*', 'b', 'b*',
58        'W', 'W F', 'W*', 'W* F', 'W* n', 'n'
59    ];
60
61    /**
62     * Path streams array
63     * @var ?array
64     */
65    protected ?array $streams = null;
66
67    /**
68     * Path style
69     * @var string
70     */
71    protected string $style = 'S';
72
73    /**
74     * Constructor
75     *
76     * Instantiate a PDF path object
77     *
78     * @param string $style
79     */
80    public function __construct(string $style = Path::STROKE)
81    {
82        $this->setStyle($style);
83    }
84
85    /**
86     * Set the path fill color
87     *
88     * @param  Color\ColorInterface $color
89     * @return Path
90     */
91    public function setFillColor(Color\ColorInterface $color): Path
92    {
93        $stream = null;
94        if ($color instanceof Color\Rgb) {
95            $stream .= "\n" . $color->render(Color\Rgb::PERCENT) . " rg\n";
96        } else if ($color instanceof Color\Cmyk) {
97            $stream .= "\n" . $color->render(Color\Cmyk::PERCENT) . " k\n";
98        } else if ($color instanceof Color\Grayscale) {
99            $stream .= "\n" . $color->render(Color\Grayscale::PERCENT) . " g\n";
100        }
101
102        if ($stream !== null) {
103            $this->streams[] = [
104                'stream' => $stream
105            ];
106        }
107
108        return $this;
109    }
110
111    /**
112     * Set the path stroke color
113     *
114     * @param  Color\ColorInterface $color
115     * @return Path
116     */
117    public function setStrokeColor(Color\ColorInterface $color): Path
118    {
119        $stream = null;
120        if ($color instanceof Color\Rgb) {
121            $stream .= "\n" . $color->render(Color\Rgb::PERCENT) . " RG\n";
122        } else if ($color instanceof Color\Cmyk) {
123            $stream .= "\n" . $color->render(Color\Rgb::PERCENT) . " K\n";
124        } else if ($color instanceof Color\Grayscale) {
125            $stream .= "\n" . $color->render(Color\Rgb::PERCENT) . " G\n";
126        }
127
128        if ($stream !== null) {
129            $this->streams[] = [
130                'stream' => $stream
131            ];
132        }
133
134        return $this;
135    }
136
137    /**
138     * Set the stroke properties
139     *
140     * @param  int|float      $width
141     * @param  int|float|null $dashLength
142     * @param  int|float|null $dashGap
143     * @return Path
144     */
145    public function setStroke(int|float $width, int|float|null $dashLength = null, int|float|null $dashGap = null): Path
146    {
147        $stream = "\n" . $width . " w\n";
148        if ($width != 0) {
149            $stream .= (($dashLength !== null) && ($dashGap !== null)) ?
150                "[" . $dashLength . " " . $dashGap . "] 0 d\n" : "[] 0 d\n";
151        }
152
153        $this->streams[] = [
154            'stream' => $stream
155        ];
156
157        return $this;
158    }
159
160    /**
161     * Set the style
162     *
163     * @param  string $style
164     * @return Path
165     */
166    public function setStyle(string $style): Path
167    {
168        if (in_array($style, $this->allowedStyles)) {
169            $this->style = $style;
170        }
171        return $this;
172    }
173
174    /**
175     * Open a graphics state layer
176     *
177     * @return Path
178     */
179    public function openLayer(): Path
180    {
181        $this->streams[] = [
182            'stream' => "\nq\n"
183        ];
184        return $this;
185    }
186
187    /**
188     * Close a graphics state layer
189     *
190     * @return Path
191     */
192    public function closeLayer(): Path
193    {
194        $this->streams[] = [
195            'stream' => "\nQ\n"
196        ];
197        return $this;
198    }
199
200    /**
201     * Get the streams
202     *
203     * @return ?array
204     */
205    public function getStreams(): ?array
206    {
207        return $this->streams;
208    }
209
210    /**
211     * Get the current style
212     *
213     * @return string
214     */
215    public function getStyle(): string
216    {
217        return $this->style;
218    }
219
220    /**
221     * Draw a line
222     *
223     * @param  int|float $x1
224     * @param  int|float $y1
225     * @param  int|float $x2
226     * @param  int|float $y2
227     * @return Path
228     */
229    public function drawLine(int|float $x1, int|float $y1, int|float $x2, int|float $y2): Path
230    {
231        $this->streams[] = [
232            'points' => [
233                ['x1' => $x1, 'y1' => $y1],
234                ['x2' => $x2, 'y2' => $y2]
235            ],
236            'stream' => "\n[{x1}] [{y1}] m\n[{x2}] [{y2}] l\nS\n"
237        ];
238
239        return $this;
240    }
241
242    /**
243     * Draw a rectangle
244     *
245     * @param  int|float      $x
246     * @param  int|float      $y
247     * @param  int|float      $w
248     * @param  int|float|null $h
249     * @return Path
250     */
251    public function drawRectangle(int|float $x, int|float $y, int|float $w, int|float|null $h = null): Path
252    {
253        if ($h === null) {
254            $h = $w;
255        }
256
257        $this->streams[] = [
258            'points' => [
259                ['x' => $x, 'y' => $y]
260            ],
261            'stream' => "\n[{x}] [{y}] {$w} {$h} re\n" . $this->style . "\n"
262        ];
263
264        return $this;
265    }
266
267    /**
268     * Draw a rounded rectangle
269     *
270     * @param  int|float $x
271     * @param  int|float $y
272     * @param  int|float $w
273     * @param  int|float|null $h
274     * @param  int|float $rx
275     * @param  int|float|null $ry
276     * @return Path
277     */
278    public function drawRoundedRectangle(int|float $x, int|float $y, int|float $w, int|float|null $h = null, int|float $rx = 10, int|float|null $ry = null): Path
279    {
280        if ($h === null) {
281            $h = $w;
282        }
283
284        if ($ry === null) {
285            $ry = $rx;
286        }
287
288        $rectangle = null;
289
290        $bez1X = $x;
291        $bez1Y = $y;
292        $bez2X = $x + $w;
293        $bez2Y = $y;
294        $bez3X = $x + $w;
295        $bez3Y = $y + $h;
296        $bez4X = $x;
297        $bez4Y = $y + $h;
298
299        $points = [
300            ['x1'  => $x,            'y1'  => $y + $ry],
301            ['x2'  => $x + $rx,      'y2'  => $y],
302            ['x3'  => $x + $w - $rx, 'y3'  => $y],
303            ['x4'  => $x + $w,       'y4'  => $y + $ry],
304            ['x5'  => $x + $w,       'y5'  => $y + $h - $ry],
305            ['x6'  => $x + $w - $rx, 'y6'  => $y + $h],
306            ['x7'  => $x + $rx,      'y7'  => $y + $h],
307            ['x8'  => $x,            'y8'  => $y + $h - $ry],
308            ['x9'  => $bez1X,        'y9'  => $bez1Y],
309            ['x10' => $bez2X,        'y10' => $bez2Y],
310            ['x11' => $bez3X,        'y11' => $bez3Y],
311            ['x12' => $bez4X,        'y12' => $bez4Y]
312        ];
313
314        $rectangle .= "[{x8}] [{y8}] m\n";
315        $rectangle .= "[{x1}] [{y1}] l\n";
316        $rectangle .= "[{x9}] [{y9}] [{x9}] [{y9}] [{x2}] [{y2}] c\n";
317        $rectangle .= "[{x3}] [{y3}] l\n";
318        $rectangle .= "[{x10}] [{y10}] [{x10}] [{y10}] [{x4}] [{y4}] c\n";
319        $rectangle .= "[{x5}] [{y5}] l\n";
320        $rectangle .= "[{x11}] [{y11}] [{x11}] [{y11}] [{x6}] [{y6}] c\n";
321        $rectangle .= "[{x7}] [{y7}] l\n";
322        $rectangle .= "[{x12}] [{y12}] [{x12}] [{y12}] [{x8}] [{y8}] c\n";
323
324        $rectangle .= "h\n";
325
326        $this->streams[] = [
327            'points' => $points,
328            'stream' => "\n{$rectangle}\n" . $this->style . "\n"
329        ];
330
331        return $this;
332    }
333
334    /**
335     * Draw a square
336     *
337     * @param  int|float $x
338     * @param  int|float $y
339     * @param  int|float $w
340     * @return Path
341     */
342    public function drawSquare(int|float $x, int|float $y, int|float $w): Path
343    {
344        return $this->drawRectangle($x, $y, $w, $w);
345    }
346
347    /**
348     * Draw a rounded square
349     *
350     * @param  int|float $x
351     * @param  int|float $y
352     * @param  int|float $w
353     * @param  int|float $rx
354     * @param  int|float|null $ry
355     * @return Path
356     */
357    public function drawRoundedSquare(int|float $x, int|float $y, int|float $w, int|float $rx = 10, int|float|null $ry = null): Path
358    {
359        return $this->drawRoundedRectangle($x, $y, $w, $w, $rx, $ry);
360    }
361
362    /**
363     * Draw a polygon
364     *
365     * @param  array $points
366     * @throws Exception
367     * @return Path
368     */
369    public function drawPolygon(array $points): Path
370    {
371        $i = 1;
372        $polygon = null;
373
374        $stream = [
375            'points' => [],
376            'stream' => null
377        ];
378
379        foreach ($points as $coord) {
380            if (!isset($coord['x']) || !isset($coord['y'])) {
381                throw new Exception("Error: The array of points must contain arrays with an 'x' and 'y' values.");
382            }
383            $stream['points'][] = [
384                'x' . $i => $coord['x'],
385                'y' . $i => $coord['y']
386            ];
387
388            if ($i == 1) {
389                $stream['stream'] .= "[{x" . $i . "}] [{y" . $i . "}] m\n";
390            } else if ($i <= count($points)) {
391                $stream['stream'] .= "[{x" . $i . "}] [{y" . $i . "}] l\n";
392            }
393            $i++;
394        }
395
396        $stream['stream'] .= "h\n";
397        $this->streams[] = $stream;
398
399        return $this;
400    }
401
402    /**
403     * Draw an ellipse
404     *
405     * @param  int|float $x
406     * @param  int|float $y
407     * @param  int|float $w
408     * @param  int|float|null $h
409     * @return Path
410     */
411    public function drawEllipse(int|float $x, int|float $y, int|float $w, int|float|null $h = null): Path
412    {
413        if ($h === null) {
414            $h = $w;
415        }
416
417        $x1 = $x + $w;
418        $y1 = $y;
419        $x2 = $x;
420        $y2 = $y - $h;
421        $x3 = $x - $w;
422        $y3 = $y;
423        $x4 = $x;
424        $y4 = $y + $h;
425
426        // Calculate coordinate number one's 2 bezier points.
427        $coor1Bez1X = $x1;
428        $coor1Bez1Y = (round(0.55 * ($y2 - $y1))) + $y1;
429        $coor1Bez2X = $x1;
430        $coor1Bez2Y = (round(0.45 * ($y1 - $y4))) + $y4;
431
432        // Calculate coordinate number two's 2 bezier points.
433        $coor2Bez1X = (round(0.45 * ($x2 - $x1))) + $x1;
434        $coor2Bez1Y = $y2;
435        $coor2Bez2X = (round(0.55 * ($x3 - $x2))) + $x2;
436        $coor2Bez2Y = $y2;
437
438        // Calculate coordinate number three's 2 bezier points.
439        $coor3Bez1X = $x3;
440        $coor3Bez1Y = (round(0.55 * ($y2 - $y3))) + $y3;
441        $coor3Bez2X = $x3;
442        $coor3Bez2Y = (round(0.45 * ($y3 - $y4))) + $y4;
443
444        // Calculate coordinate number four's 2 bezier points.
445        $coor4Bez1X = (round(0.55 * ($x3 - $x4))) + $x4;
446        $coor4Bez1Y = $y4;
447        $coor4Bez2X = (round(0.45 * ($x4 - $x1))) + $x1;
448        $coor4Bez2Y = $y4;
449
450        $this->streams[] = [
451            'points' => [
452                ['x1'  => $x1,         'y1'  => $y1],
453                ['x2'  => $x2,         'y2'  => $y2],
454                ['x3'  => $x3,         'y3'  => $y3],
455                ['x4'  => $x4,         'y4'  => $y4],
456                ['x5'  => $coor1Bez1X, 'y5'  => $coor1Bez1Y],
457                ['x6'  => $coor1Bez2X, 'y6'  => $coor1Bez2Y],
458                ['x7'  => $coor2Bez1X, 'y7'  => $coor2Bez1Y],
459                ['x8'  => $coor2Bez2X, 'y8'  => $coor2Bez2Y],
460                ['x9'  => $coor3Bez1X, 'y9'  => $coor3Bez1Y],
461                ['x10' => $coor3Bez2X, 'y10' => $coor3Bez2Y],
462                ['x11' => $coor4Bez1X, 'y11' => $coor4Bez1Y],
463                ['x12' => $coor4Bez2X, 'y12' => $coor4Bez2Y]
464            ],
465            'stream' => "\n[{x1}] [{y1}] m\n[{x5}] [{y5}] [{x7}] [{y7}] [{x2}] [{y2}] c\n" .
466                "[{x8}] [{y8}] [{x9}] [{y9}] [{x3}] [{y3}] c\n[{x10}] [{y10}] " .
467                "[{x11}] [{y11}] [{x4}] [{y4}] c\n[{x12}] [{y12}] [{x6}] [{y6}] " .
468                "[{x1}] [{y1}] c\n" . $this->style . "\n"
469        ];
470
471        return $this;
472    }
473
474    /**
475     * Draw a circle
476     *
477     * @param  int|float $x
478     * @param  int|float $y
479     * @param  int|float $w
480     * @return Path
481     */
482    public function drawCircle(int|float $x, int|float $y, int|float $w): Path
483    {
484        return $this->drawEllipse($x, $y, $w, $w);
485    }
486
487    /**
488     * Draw an arc
489     *
490     * @param  int|float $x
491     * @param  int|float $y
492     * @param  int|float $start
493     * @param  int|float $end
494     * @param  int|float $w
495     * @param  int|float|null $h
496     * @return Path
497     */
498    public function drawArc(int|float $x, int|float $y, int|float $start, int|float $end, int|float $w, int|float|null $h = null): Path
499    {
500        if ($h === null) {
501            $h = $w;
502        }
503        $this->calculateArc($x, $y, $this->calculateDegrees($start, $end), $w, $h);
504
505        return $this;
506    }
507
508    /**
509     * Draw a chord
510     *
511     * @param  int|float $x
512     * @param  int|float $y
513     * @param  int|float $start
514     * @param  int|float $end
515     * @param  int|float $w
516     * @param  int|float|null $h
517     * @return Path
518     */
519    public function drawChord(int|float $x, int|float $y, int|float $start, int|float $end, int|float $w, int|float|null $h = null): Path
520    {
521        if ($h === null) {
522            $h = $w;
523        }
524        $this->calculateArc($x, $y, $this->calculateDegrees($start, $end), $w, $h, true);
525
526        return $this;
527    }
528
529    /**
530     * Draw a pie slice
531     *
532     * @param  int|float $x
533     * @param  int|float $y
534     * @param  int|float $start
535     * @param  int|float $end
536     * @param  int|float $w
537     * @param  int|float|null $h
538     * @return Path
539     */
540    public function drawPie(int|float $x, int|float $y, int|float $start, int|float $end, int|float $w, int|float|null $h = null): Path
541    {
542        if ($h === null) {
543            $h = $w;
544        }
545        $this->calculateArc($x, $y, $this->calculateDegrees($start, $end), $w, $h, true, true);
546
547        return $this;
548    }
549
550    /**
551     * Draw an open cubic bezier curve
552     *
553     * @param  int|float $x1
554     * @param  int|float $y1
555     * @param  int|float $x2
556     * @param  int|float $y2
557     * @param  int|float $bezierX1
558     * @param  int|float $bezierY1
559     * @param  int|float $bezierX2
560     * @param  int|float $bezierY2
561     * @return Path
562     */
563    public function drawOpenCubicBezierCurve(
564        int|float $x1, int|float $y1, int|float $x2, int|float $y2, int|float $bezierX1, int|float $bezierY1, int|float $bezierX2, int|float $bezierY2
565    ): Path
566    {
567        $this->streams[] = [
568            'points' => [
569                ['x1' => $x1, 'y1' => $y1],
570                ['x2' => $bezierX1, 'y2' => $bezierY1],
571                ['x3' => $bezierX2, 'y3' => $bezierY2],
572                ['x4' => $x2, 'y4' => $y2]
573            ],
574            'stream' => "\n[{x1}] [{y1}] m\n[{x2}] [{y2}] [{x3}] [{y3}] [{x4}] [{y4}] c\n" . $this->style . "\n"
575        ];
576
577        return $this;
578    }
579
580    /**
581     * Draw a closed cubic bezier curve
582     *
583     * @param  int|float $x1
584     * @param  int|float $y1
585     * @param  int|float $x2
586     * @param  int|float $y2
587     * @param  int|float $bezierX1
588     * @param  int|float $bezierY1
589     * @param  int|float $bezierX2
590     * @param  int|float $bezierY2
591     * @return Path
592     */
593    public function drawClosedCubicBezierCurve(
594        int|float $x1, int|float $y1, int|float $x2, int|float $y2, int|float $bezierX1, int|float $bezierY1, int|float $bezierX2, int|float $bezierY2
595    ): Path
596    {
597        $this->streams[] = [
598            'points' => [
599                ['x1' => $x1,       'y1' => $y1],
600                ['x2' => $bezierX1, 'y2' => $bezierY1],
601                ['x3' => $bezierX2, 'y3' => $bezierY2],
602                ['x4' => $x2,       'y4' => $y2]
603            ],
604            'stream' => "\n[{x1}] [{y1}] m\n[{x2}] [{y2}] [{x3}] [{y3}] [{x4}] [{y4}] c\nh\n" . $this->style . "\n"
605        ];
606
607        return $this;
608    }
609
610    /**
611     * Draw an open quadratic bezier curve, single control point
612     *
613     * @param  int|float $x1
614     * @param  int|float $y1
615     * @param  int|float $x2
616     * @param  int|float $y2
617     * @param  int|float $bezierX
618     * @param  int|float $bezierY
619     * @param  bool $first
620     * @return Path
621     */
622    public function drawOpenQuadraticBezierCurve(int|float $x1, int|float $y1, int|float $x2, int|float $y2, int|float $bezierX, int|float $bezierY, bool $first = true): Path
623    {
624        $this->streams[] = [
625            'points' => [
626                ['x1' => $x1,      'y1' => $y1],
627                ['x2' => $bezierX, 'y2' => $bezierY],
628                ['x3' => $x2,      'y3' => $y2]
629            ],
630            'stream' => "\n[{x1}] [{y1}] m\n[{x2}] [{y2}] [{x3}] [{y3}] " . (($first) ? "y" : "v") . "\n" . $this->style . "\n"
631        ];
632
633        return $this;
634    }
635
636    /**
637     * Draw an open quadratic bezier curve, single control point
638     *
639     * @param  int|float $x1
640     * @param  int|float $y1
641     * @param  int|float $x2
642     * @param  int|float $y2
643     * @param  int|float $bezierX
644     * @param  int|float $bezierY
645     * @param  bool $first
646     * @return Path
647     */
648    public function drawClosedQuadraticBezierCurve(int|float $x1, int|float $y1, int|float $x2, int|float $y2, int|float $bezierX, int|float $bezierY, bool $first = true): Path
649    {
650        $this->streams[] = [
651            'points' => [
652                ['x1' => $x1,      'y1' => $y1],
653                ['x2' => $bezierX, 'y2' => $bezierY],
654                ['x3' => $x2,      'y3' => $y2]
655            ],
656            'stream' => "\n[{x1}] [{y1}] m\n[{x2}] [{y2}] [{x3}] [{y3}] " . (($first) ? "y" : "v") . "\nh\n" . $this->style . "\n"
657        ];
658
659        return $this;
660    }
661
662    /**
663     * Calculate degrees
664     *
665     * @param  int|float $start
666     * @param  int|float $end
667     * @throws OutOfRangeException
668     * @return array
669     */
670    protected function calculateDegrees(int|float $start, int|float $end): array
671    {
672        if (($start < 0) || ($end > 360)) {
673            throw new OutOfRangeException('The start and end angles must be between 0 and 360.');
674        }
675        if ($start >= $end) {
676            throw new OutOfRangeException('The start angle must be less than the end angle.');
677        }
678
679        if (($end - $start) > 90) {
680            $degrees = [];
681            if ($start < 90) {
682                $degrees[] = [$start, 90];
683                $current = 90;
684            } else {
685                $current = $start;
686            }
687            while (($current + 90) < $end) {
688                $next = ($current + 90) - ($current % 90);
689                $degrees[] = [$current, $next];
690                $current = $next;
691            }
692            $degrees[] = [$current, $end];
693        } else if (($start < 180) && ($start > 90) && ($end > 180)) {
694            // This branch only runs when (end - start) <= 90 (the guard
695            // above already claimed the > 90 case), so combined with
696            // start < 180, end < start + 90 < 270 always - the same
697            // continuation loop as above can never iterate here since it
698            // requires end > 270.
699            $degrees[] = [$start, 180];
700            $degrees[] = [180, $end];
701        } else {
702            $degrees[] = [$start, $end];
703        }
704
705        return $degrees;
706    }
707
708    /**
709     * Calculate arc
710     *
711     * @param  int|float  $x
712     * @param  int|float  $y
713     * @param  array $degrees
714     * @param  int|float  $w
715     * @param  int|float|null  $h
716     * @param  bool  $closed
717     * @param  bool  $pie
718     * @return void
719     */
720    protected function calculateArc(int|float $x, int|float $y, array $degrees, int|float $w, int|float|null $h = null, bool $closed = false, bool $pie = false): void
721    {
722        foreach ($degrees as $key => $value) {
723            $start  = $value[0];
724            $end    = $value[1];
725            $startX = round($x + ($w * cos(deg2rad($start))));
726            $startY = round($y + ($h * sin(deg2rad($start))));
727            $endX   = round($x + ($w * cos(deg2rad($end))));
728            $endY   = round($y + ($h * sin(deg2rad($end))));
729            $n1     = acos(($startX - $x) / $w);
730            $n2     = acos(($endX - $x) / $w);
731            $t      = $n2 - $n1;
732            $a      = sin($t) * ((sqrt(4 + (3 * pow(tan($t / 2), 2))) - 1) / 3);
733
734            $e1x = 0 - ($w * sin($n1));
735            $e1y = $h * cos($n1);
736
737            $e2x = 0 - ($w * sin($n2));
738            $e2y = $h * cos($n2);
739
740            $q1X = round($startX + ($a * $e1x));
741            $q2X = round($endX - ($a * $e2x));
742
743            if ($end > 180) {
744                $q1Y = round($startY + ((0 - $a) * $e1y));
745                $q2Y = round($endY - ((0 - $a) * $e2y));
746            } else {
747                $q1Y = round($startY + ($a * $e1y));
748                $q2Y = round($endY - ($a * $e2y));
749            }
750
751            if ($key == 0) {
752                $points = [
753                    ['x1' => $startX, 'y1' => $startY],
754                    ['x2' => $q1X,    'y2' => $q1Y],
755                    ['x3' => $q2X,    'y3' => $q2Y],
756                    ['x4' => $endX,   'y4' => $endY]
757                ];
758                $stream = "\n[{x1}] [{y1}] m\n[{x2}] [{y2}] [{x3}] [{y3}] [{x4}] [{y4}] c\n";
759                if (count($degrees) == 1) {
760                    if ($pie) {
761                        $points[] = ['x5' => $x,   'y5' => $y];
762                        $stream .= "\n[{x5}] [{y5}] l\n" . (($closed) ? "h" : null) . "\n" . $this->style . "\n";
763                    } else {
764                        $stream .= (($closed) ? "h" : null) . "\n" . $this->style . "\n";
765                    }
766                }
767
768                $this->streams[] = [
769                    'points' => $points,
770                    'stream' => $stream
771                ];
772            } else if ($key == (count($degrees) - 1)) {
773                if ($pie) {
774                    $this->streams[] = [
775                        'points' => [
776                            ['x2' => $q1X,  'y2' => $q1Y],
777                            ['x3' => $q2X,  'y3' => $q2Y],
778                            ['x4' => $endX, 'y4' => $endY],
779                            ['x5' => $x,    'y5' => $y]
780                        ],
781                        'stream' => "\n[{x2}] [{y2}] [{x3}] [{y3}] [{x4}] [{y4}] c\n[{x5}] [{y5}] l\nh\n" . $this->style . "\n"
782                    ];
783                } else {
784                    $this->streams[] = [
785                        'points' => [
786                            ['x2' => $q1X,  'y2' => $q1Y],
787                            ['x3' => $q2X,  'y3' => $q2Y],
788                            ['x4' => $endX, 'y4' => $endY]
789                        ],
790                        'stream' => "\n[{x2}] [{y2}] [{x3}] [{y3}] [{x4}] [{y4}] c\n" . (($closed) ? "h" : null) . "\n" . $this->style . "\n"
791                    ];
792                }
793            } else {
794                $this->streams[] = [
795                    'points' => [
796                        ['x2' => $q1X, 'y2' => $q1Y],
797                        ['x3' => $q2X, 'y3' => $q2Y],
798                        ['x4' => $endX, 'y4' => $endY]
799                    ],
800                    'stream' => "\n[{x2}] [{y2}] [{x3}] [{y3}] [{x4}] [{y4}] c\n"
801                ];
802            }
803        }
804    }
805
806}