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