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