xref: /plugin/statdisplay/vendor/szymach/c-pchart/src/Image.php (revision 03cdebb71906645bc452782ad23f052261c2cb18) !
1<?php
2
3namespace CpChart;
4
5use RuntimeException;
6
7use const IMAGE_MAP_DELIMITER;
8use const IMAGE_MAP_STORAGE_FILE;
9use const IMAGE_MAP_STORAGE_SESSION;
10use const TEXT_ALIGN_BOTTOMLEFT;
11use const TEXT_ALIGN_BOTTOMMIDDLE;
12use const TEXT_ALIGN_BOTTOMRIGHT;
13use const TEXT_ALIGN_MIDDLELEFT;
14use const TEXT_ALIGN_MIDDLEMIDDLE;
15use const TEXT_ALIGN_MIDDLERIGHT;
16use const TEXT_ALIGN_TOPLEFT;
17use const TEXT_ALIGN_TOPMIDDLE;
18use const TEXT_ALIGN_TOPRIGHT;
19use const VOID;
20
21class Image extends Draw
22{
23    /**
24     * @var string
25     */
26    public $ImageMapFileName;
27
28    /**
29     * @var string
30     */
31    public $ImageMapStorageFolder;
32
33    /**
34     * @param int<1, max> $XSize
35     * @param int<1, max> $YSize
36     * @param Data|null $DataSet
37     * @param bool $TransparentBackground
38     */
39    public function __construct(
40        $XSize,
41        $YSize,
42        ?Data $DataSet = null,
43        $TransparentBackground = false
44    ) {
45        parent::__construct();
46
47        $this->TransparentBackground = $TransparentBackground;
48
49        $this->DataSet = null !== $DataSet ? $DataSet : new Data();
50        $this->XSize = $XSize;
51        $this->YSize = $YSize;
52        $this->Picture = imagecreatetruecolor($XSize, $YSize);
53
54        if ($this->TransparentBackground) {
55            imagealphablending($this->Picture, false);
56            /** @var int<0, max> $fillColor */
57            $fillColor = imagecolorallocatealpha($this->Picture, 255, 255, 255, 127);
58            imagefilledrectangle(
59                $this->Picture,
60                0,
61                0,
62                $XSize,
63                $YSize,
64                $fillColor
65            );
66            imagealphablending($this->Picture, true);
67            imagesavealpha($this->Picture, true);
68        } else {
69            $C_White = $this->AllocateColor($this->Picture, 255, 255, 255);
70            imagefilledrectangle($this->Picture, 0, 0, $XSize, $YSize, $C_White);
71        }
72    }
73
74    public function __toString()
75    {
76        if ($this->TransparentBackground) {
77            imagealphablending($this->Picture, false);
78            imagesavealpha($this->Picture, true);
79        }
80
81        ob_start();
82        imagepng($this->Picture);
83
84        $return = ob_get_clean();
85        if ($return === false) {
86            throw new RuntimeException('Unable to cast the image to string');
87        }
88
89        return $return;
90    }
91
92    /**
93     * Enable / disable and set shadow properties
94     *
95     * @param bool $Enabled
96     * @param array{ X?: int, Y?: int, R?: int, G?: int, B?: int, Alpha?: int } $Format
97     * @return void
98     */
99    public function setShadow($Enabled = true, array $Format = [])
100    {
101        $this->Shadow = (bool) $Enabled;
102        $this->ShadowX = $Format["X"] ?? 2;
103        $this->ShadowY = $Format["Y"] ?? 2;
104        $this->ShadowR = $Format["R"] ?? 0;
105        $this->ShadowG = $Format["G"] ?? 0;
106        $this->ShadowB = $Format["B"] ?? 0;
107        $this->Shadowa = $Format["Alpha"] ?? 10;
108    }
109
110    /**
111     * Set the graph area position
112     * @param int $X1
113     * @param int $Y1
114     * @param int $X2
115     * @param int $Y2
116     * @return int|null
117     */
118    public function setGraphArea($X1, $Y1, $X2, $Y2)
119    {
120        if ($X2 < $X1 || $X1 == $X2 || $Y2 < $Y1 || $Y1 == $Y2) {
121            return -1;
122        }
123
124        $this->GraphAreaX1 = $X1;
125        $this->DataSet->Data["GraphArea"]["X1"] = $X1;
126        $this->GraphAreaY1 = $Y1;
127        $this->DataSet->Data["GraphArea"]["Y1"] = $Y1;
128        $this->GraphAreaX2 = $X2;
129        $this->DataSet->Data["GraphArea"]["X2"] = $X2;
130        $this->GraphAreaY2 = $Y2;
131        $this->DataSet->Data["GraphArea"]["Y2"] = $Y2;
132
133        return null;
134    }
135
136    /**
137     * Return the width of the picture
138     * @return int
139     */
140    public function getWidth()
141    {
142        return $this->XSize;
143    }
144
145    /**
146     * Return the heigth of the picture
147     * @return int
148     */
149    public function getHeight()
150    {
151        return $this->YSize;
152    }
153
154    /**
155     * Render the picture to a file
156     * @param string $FileName
157     * @return void
158     */
159    public function render($FileName)
160    {
161        if ($this->TransparentBackground) {
162            imagealphablending($this->Picture, false);
163            imagesavealpha($this->Picture, true);
164        }
165        imagepng($this->Picture, $FileName);
166    }
167
168    /**
169     * @return non-empty-string
170     */
171    public function toDataURI()
172    {
173        return 'data:image/png;base64,' . base64_encode($this->__toString());
174    }
175
176    /**
177     * @FIXME better way to do this?
178     *
179     * Render the picture to a web browser stream
180     * @param bool $BrowserExpire
181     * @return void
182     */
183    public function stroke($BrowserExpire = false)
184    {
185        if ($this->TransparentBackground) {
186            imagealphablending($this->Picture, false);
187            imagesavealpha($this->Picture, true);
188        }
189
190        if ($BrowserExpire) {
191            header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
192            header("Cache-Control: no-cache");
193            header("Pragma: no-cache");
194        }
195
196        header('Content-type: image/png');
197        imagepng($this->Picture);
198    }
199
200    /**
201     * Automatic output method based on the calling interface
202     * @param string $FileName
203     * @return void
204     */
205    public function autoOutput($FileName = "output.png")
206    {
207        if (php_sapi_name() == "cli") {
208            $this->Render($FileName);
209        } else {
210            $this->Stroke();
211        }
212    }
213
214    /**
215     * Return the length between two points
216     * @param int $X1
217     * @param int $Y1
218     * @param int $X2
219     * @param int $Y2
220     * @return float
221     */
222    public function getLength($X1, $Y1, $X2, $Y2)
223    {
224        return sqrt(
225            pow(max($X1, $X2) - min($X1, $X2), 2) + pow(max($Y1, $Y2) - min($Y1, $Y2), 2)
226        );
227    }
228
229    /**
230     * Return the orientation of a line
231     * @param int $X1
232     * @param int $Y1
233     * @param int $X2
234     * @param int $Y2
235     * @return float|int
236     */
237    public function getAngle($X1, $Y1, $X2, $Y2)
238    {
239        $Opposite = $Y2 - $Y1;
240        $Adjacent = $X2 - $X1;
241        $Angle = rad2deg(atan2($Opposite, $Adjacent));
242
243        return ($Angle > 0) ? $Angle : (360 - abs($Angle));
244    }
245
246    /**
247     * Return the surrounding box of text area
248     * @param int $X
249     * @param int $Y
250     * @param string $FontName
251     * @param int $FontSize
252     * @param int $Angle
253     * @param string $Text
254     * @return array<int<0, max>, array{ X: float, Y: float }>
255     */
256    public function getTextBox($X, $Y, $FontName, $FontSize, $Angle, $Text)
257    {
258        $coords = imagettfbbox($FontSize, 0, $this->loadFont($FontName, 'fonts'), $Text);
259        if ($coords === false) {
260            throw new RuntimeException('Unable to create text box coordinates');
261        }
262
263        $a = deg2rad($Angle);
264        $ca = cos($a);
265        $sa = sin($a);
266
267        $initalPositions = [];
268        for ($i = 0; $i < 7; $i += 2) {
269            /** @var int<0, 3> $index */
270            $index = $i / 2;
271            $initalPositions[$index] = [
272                "X" => $X + round($coords[$i] * $ca + $coords[$i + 1] * $sa),
273                "Y" => $Y + round($coords[$i + 1] * $ca - $coords[$i] * $sa),
274            ];
275        }
276
277        // Split the assignments here to make PHPStan happy. In theory modifying
278        // the array you are pulling values from could break the logic, even though
279        // chances of that were zero, it still complained.
280
281        $realPositions = $initalPositions;
282
283        $realPositions[TEXT_ALIGN_BOTTOMLEFT] = [
284            "X" => $initalPositions[0]["X"],
285            "Y" => $initalPositions[0]["Y"],
286        ];
287
288        $realPositions[TEXT_ALIGN_BOTTOMRIGHT] = [
289            "X" => $initalPositions[1]["X"],
290            "Y" => $initalPositions[1]["Y"],
291        ];
292
293        $realPositions[TEXT_ALIGN_TOPLEFT] = [
294            "X" => $initalPositions[3]["X"],
295            "Y" => $initalPositions[3]["Y"],
296        ];
297
298        $realPositions[TEXT_ALIGN_TOPRIGHT] = [
299            "X" => $initalPositions[2]["X"],
300            "Y" => $initalPositions[2]["Y"],
301        ];
302
303        $realPositions[TEXT_ALIGN_BOTTOMMIDDLE] = [
304            "X" => ($initalPositions[1]["X"] - $initalPositions[0]["X"]) / 2 + $initalPositions[0]["X"],
305            "Y" => ($initalPositions[0]["Y"] - $initalPositions[1]["Y"]) / 2 + $initalPositions[1]["Y"],
306        ];
307
308        $realPositions[TEXT_ALIGN_TOPMIDDLE] = [
309            "X" => ($initalPositions[2]["X"] - $initalPositions[3]["X"]) / 2 + $initalPositions[3]["X"],
310            "Y" => ($initalPositions[3]["Y"] - $initalPositions[2]["Y"]) / 2 + $initalPositions[2]["Y"],
311        ];
312
313        $realPositions[TEXT_ALIGN_MIDDLELEFT] = [
314            "X" => ($initalPositions[0]["X"] - $initalPositions[3]["X"]) / 2 + $initalPositions[3]["X"],
315            "Y" => ($initalPositions[0]["Y"] - $initalPositions[3]["Y"]) / 2 + $initalPositions[3]["Y"]
316        ];
317
318        $realPositions[TEXT_ALIGN_MIDDLERIGHT] = [
319            "X" => ($initalPositions[1]["X"] - $initalPositions[2]["X"]) / 2 + $initalPositions[2]["X"],
320            "Y" => ($initalPositions[1]["Y"] - $initalPositions[2]["Y"]) / 2 + $initalPositions[2]["Y"],
321        ];
322
323        $realPositions[TEXT_ALIGN_MIDDLEMIDDLE] = [
324            "X" => ($initalPositions[1]["X"] - $initalPositions[3]["X"]) / 2 + $initalPositions[3]["X"],
325            "Y" => ($initalPositions[0]["Y"] - $initalPositions[2]["Y"]) / 2 + $initalPositions[2]["Y"],
326        ];
327
328
329        return $realPositions;
330    }
331
332    /**
333     * @param array{
334     *  R?: int,
335     *  G?: int,
336     *  B?: int,
337     *  Alpha?: int,
338     *  FontName?: string,
339     *  FontSize?: int
340     * } $Format
341     * @return void
342     */
343    public function setFontProperties($Format = [])
344    {
345        $R = $Format["R"] ?? -1;
346        $G = $Format["G"] ?? -1;
347        $B = $Format["B"] ?? -1;
348        $Alpha = $Format["Alpha"] ?? 100;
349        $FontName = $Format["FontName"] ?? null;
350        $FontSize = $Format["FontSize"] ?? null;
351
352        if ($R != -1) {
353            $this->FontColorR = $R;
354        }
355        if ($G != -1) {
356            $this->FontColorG = $G;
357        }
358        if ($B != -1) {
359            $this->FontColorB = $B;
360        }
361        if ($Alpha != null) {
362            $this->FontColorA = $Alpha;
363        }
364
365        if ($FontName != null) {
366            $this->FontName = $this->loadFont($FontName, 'fonts');
367        }
368        if ($FontSize != null) {
369            $this->FontSize = $FontSize;
370        }
371    }
372
373    /**
374     * Returns the 1st decimal values (used to correct AA bugs)
375     * @param string $Value
376     * @return string|int
377     */
378    public function getFirstDecimal($Value)
379    {
380        $Values = preg_split("/\./", $Value);
381        return isset($Values[1]) ? substr($Values[1], 0, 1) : 0;
382    }
383
384    /**
385     * Attach a dataset to your pChart Object
386     * @return void
387     */
388    public function setDataSet(Data $DataSet)
389    {
390        $this->DataSet = $DataSet;
391    }
392
393    /**
394     * Print attached dataset contents to STDOUT
395     * @return void
396     */
397    public function printDataSet()
398    {
399        print_r($this->DataSet);
400    }
401
402    /**
403     * Initialise the image map methods
404     * @param string $Name
405     * @param int $StorageMode
406     * @param string $UniqueID
407     * @param string $StorageFolder
408     * @return void
409     */
410    public function initialiseImageMap(
411        $Name = "pChart",
412        $StorageMode = IMAGE_MAP_STORAGE_SESSION,
413        $UniqueID = "imageMap",
414        $StorageFolder = "tmp"
415    ) {
416        $this->ImageMapIndex = $Name;
417        $this->ImageMapStorageMode = $StorageMode;
418
419        if ($StorageMode == IMAGE_MAP_STORAGE_SESSION) {
420            // @FIXME looks like something potentially problematic
421            if (!isset($_SESSION)) {
422                session_start();
423            }
424            $_SESSION[$this->ImageMapIndex] = null;
425        } elseif ($StorageMode == IMAGE_MAP_STORAGE_FILE) {
426            $this->ImageMapFileName = $UniqueID;
427            $this->ImageMapStorageFolder = $StorageFolder;
428
429            $path = sprintf("%s/%s.map", $StorageFolder, $UniqueID);
430            if (file_exists($path)) {
431                unlink($path);
432            }
433        }
434    }
435
436    /**
437     * Add a zone to the image map
438     *
439     * @param string $Type
440     * @param string $Plots
441     * @param string|null $Color
442     * @param string $Title
443     * @param string $Message
444     * @param bool $HTMLEncode
445     * @return void
446     */
447    public function addToImageMap(
448        $Type,
449        $Plots,
450        $Color = null,
451        $Title = null,
452        $Message = null,
453        $HTMLEncode = false
454    ) {
455        if ($this->ImageMapStorageMode == null) {
456            $this->initialiseImageMap();
457        }
458
459        /* Encode the characters in the imagemap in HTML standards */
460        $Title = htmlentities(
461            str_replace("&#8364;", "\u20AC", $Title),
462            ENT_QUOTES,
463            "ISO-8859-15"
464        );
465        if ($HTMLEncode) {
466            $Message = str_replace(
467                "&gt;",
468                ">",
469                str_replace(
470                    "&lt;",
471                    "<",
472                    htmlentities($Message, ENT_QUOTES, "ISO-8859-15")
473                )
474            );
475        }
476
477        if ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_SESSION) {
478            if (!isset($_SESSION)) {
479                $this->initialiseImageMap();
480            }
481            $_SESSION[$this->ImageMapIndex][] = [$Type, $Plots, $Color, $Title, $Message];
482        } elseif ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_FILE) {
483            $Handle = $this->openImageStorageFileHandle('a');
484            if (is_resource($Handle) === false) {
485                throw new RuntimeException('Unable to write to image storage file');
486            }
487
488            fwrite(
489                $Handle,
490                sprintf(
491                    "%s%s%s%s%s%s%s%s%s\r\n",
492                    $Type,
493                    IMAGE_MAP_DELIMITER,
494                    $Plots,
495                    IMAGE_MAP_DELIMITER,
496                    $Color,
497                    IMAGE_MAP_DELIMITER,
498                    $Title,
499                    IMAGE_MAP_DELIMITER,
500                    $Message
501                )
502            );
503            fclose($Handle);
504        }
505    }
506
507    /**
508     * Remove VOID values from an imagemap custom values array
509     * @param string $SerieName
510     * @param array<int|float|numeric-string, int|float|numeric-string> $Values
511     * @return list<float|int|numeric-string>|int
512     */
513    public function removeVOIDFromArray($SerieName, array $Values)
514    {
515        if (!isset($this->DataSet->Data["Series"][$SerieName])) {
516            return -1;
517        }
518
519        $Result = [];
520        foreach ($this->DataSet->Data["Series"][$SerieName]["Data"] as $Key => $Value) {
521            if ($Value != VOID && isset($Values[$Key])) {
522                $Result[] = $Values[$Key];
523            }
524        }
525
526        return $Result;
527    }
528
529    /**
530     * Replace the title of one image map serie
531     * @param string $OldTitle
532     * @param array<int|float|numeric-string, int|float|numeric-string>|string $NewTitle
533     * @return null|int
534     */
535    public function replaceImageMapTitle($OldTitle, $NewTitle)
536    {
537        if ($this->ImageMapStorageMode == null) {
538            return -1;
539        }
540
541        if (is_array($NewTitle)) {
542            $NewTitle = $this->removeVOIDFromArray($OldTitle, $NewTitle);
543        }
544
545        if ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_SESSION) {
546            if (!isset($_SESSION)) {
547                return -1;
548            }
549            if (is_array($NewTitle)) {
550                $ID = 0;
551                foreach ($_SESSION[$this->ImageMapIndex] as $Key => $Settings) {
552                    if ($Settings[3] == $OldTitle && isset($NewTitle[$ID])) {
553                        $_SESSION[$this->ImageMapIndex][$Key][3] = $NewTitle[$ID];
554                        $ID++;
555                    }
556                }
557            } else {
558                foreach ($_SESSION[$this->ImageMapIndex] as $Key => $Settings) {
559                    if ($Settings[3] == $OldTitle) {
560                        $_SESSION[$this->ImageMapIndex][$Key][3] = $NewTitle;
561                    }
562                }
563            }
564        } elseif ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_FILE) {
565            $TempArray = [];
566            $Handle = $this->openImageStorageFileHandle();
567            if ($Handle) {
568                while (($Buffer = fgets($Handle, 4096)) !== false) {
569                    $Fields = preg_split(
570                        sprintf("/%s/", IMAGE_MAP_DELIMITER),
571                        str_replace([chr(10), chr(13)], "", $Buffer)
572                    );
573
574                    if ($Fields === false) {
575                        throw new RuntimeException('Unable to parse image storage file buffer');
576                    }
577
578                    $TempArray[] = [$Fields[0], $Fields[1], $Fields[2], $Fields[3], $Fields[4]];
579                }
580                fclose($Handle);
581
582                if (is_array($NewTitle)) {
583                    $ID = 0;
584                    foreach ($TempArray as $Key => $Settings) {
585                        if ($Settings[3] == $OldTitle && isset($NewTitle[$ID])) {
586                            $TempArray[$Key][3] = $NewTitle[$ID];
587                            $ID++;
588                        }
589                    }
590                } else {
591                    foreach ($TempArray as $Key => $Settings) {
592                        if ($Settings[3] == $OldTitle) {
593                            $TempArray[$Key][3] = $NewTitle;
594                        }
595                    }
596                }
597
598                $Handle = $this->openImageStorageFileHandle("w");
599                foreach ($TempArray as $Key => $Settings) {
600                    fwrite(
601                        $Handle,
602                        sprintf(
603                            "%s%s%s%s%s%s%s%s%s\r\n",
604                            $Settings[0],
605                            IMAGE_MAP_DELIMITER,
606                            $Settings[1],
607                            IMAGE_MAP_DELIMITER,
608                            $Settings[2],
609                            IMAGE_MAP_DELIMITER,
610                            $Settings[3],
611                            IMAGE_MAP_DELIMITER,
612                            $Settings[4]
613                        )
614                    );
615                }
616                fclose($Handle);
617            }
618        }
619
620        return null;
621    }
622
623    /**
624     * Replace the values of the image map contents
625     * @param string $Title
626     * @param list<int|float|numeric-string> $Values
627     * @return null|int
628     */
629    public function replaceImageMapValues($Title, array $Values)
630    {
631        if ($this->ImageMapStorageMode == null) {
632            return -1;
633        }
634
635        $Values = $this->removeVOIDFromArray($Title, $Values);
636        $ID = 0;
637        if ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_SESSION) {
638            if (!isset($_SESSION)) {
639                return -1;
640            }
641            foreach ($_SESSION[$this->ImageMapIndex] as $Key => $Settings) {
642                if ($Settings[3] == $Title) {
643                    if (isset($Values[$ID])) {
644                        $_SESSION[$this->ImageMapIndex][$Key][4] = $Values[$ID];
645                    }
646                    $ID++;
647                }
648            }
649        } elseif ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_FILE) {
650            $TempArray = [];
651            $Handle = $this->openImageStorageFileHandle();
652            if ($Handle) {
653                while (($Buffer = fgets($Handle, 4096)) !== false) {
654                    $Fields = preg_split(
655                        "/" . IMAGE_MAP_DELIMITER . "/",
656                        str_replace([chr(10), chr(13)], "", $Buffer)
657                    );
658
659                    if ($Fields === false) {
660                        throw new RuntimeException('Unable to parse image storage file buffer');
661                    }
662
663                    $TempArray[] = [$Fields[0], $Fields[1], $Fields[2], $Fields[3], $Fields[4]];
664                }
665                fclose($Handle);
666
667                foreach ($TempArray as $Key => $Settings) {
668                    if ($Settings[3] == $Title) {
669                        if (isset($Values[$ID])) {
670                            $TempArray[$Key][4] = $Values[$ID];
671                        }
672                        $ID++;
673                    }
674                }
675
676                $Handle = $this->openImageStorageFileHandle("w");
677                foreach ($TempArray as $Key => $Settings) {
678                    fwrite(
679                        $Handle,
680                        sprintf(
681                            "%s%s%s%s%s%s%s%s%s\r\n",
682                            $Settings[0],
683                            IMAGE_MAP_DELIMITER,
684                            $Settings[1],
685                            IMAGE_MAP_DELIMITER,
686                            $Settings[2],
687                            IMAGE_MAP_DELIMITER,
688                            $Settings[3],
689                            IMAGE_MAP_DELIMITER,
690                            $Settings[4]
691                        )
692                    );
693                }
694                fclose($Handle);
695            }
696        }
697
698        return null;
699    }
700
701    /**
702     * Dump the image map
703     * @param string $Name
704     * @param int $StorageMode
705     * @param string $UniqueID
706     * @param string $StorageFolder
707     * @return void
708     */
709    public function dumpImageMap(
710        $Name = "pChart",
711        $StorageMode = IMAGE_MAP_STORAGE_SESSION,
712        $UniqueID = "imageMap",
713        $StorageFolder = "tmp"
714    ) {
715        $this->ImageMapIndex = $Name;
716        $this->ImageMapStorageMode = $StorageMode;
717
718        if ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_SESSION) {
719            if (!isset($_SESSION)) {
720                session_start();
721            }
722            if ($_SESSION[$Name] != null) {
723                foreach ($_SESSION[$Name] as $Key => $Params) {
724                    echo $Params[0] . IMAGE_MAP_DELIMITER . $Params[1]
725                    . IMAGE_MAP_DELIMITER . $Params[2] . IMAGE_MAP_DELIMITER
726                    . $Params[3] . IMAGE_MAP_DELIMITER . $Params[4] . "\r\n";
727                }
728            }
729        } elseif ($this->ImageMapStorageMode == IMAGE_MAP_STORAGE_FILE) {
730            $storageFileMapFilePath = "$StorageFolder/$UniqueID.map";
731            if (file_exists($storageFileMapFilePath)) {
732                $Handle = @fopen($storageFileMapFilePath, "r");
733                if ($Handle !== false) {
734                    while (($Buffer = fgets($Handle, 4096)) !== false) {
735                        echo $Buffer;
736                    }
737                    fclose($Handle);
738                }
739
740                if ($this->ImageMapAutoDelete) {
741                    unlink($storageFileMapFilePath);
742                }
743            }
744        }
745    }
746
747    /**
748     * Return the HTML converted color from the RGB composite values
749     * @param int $R
750     * @param int $G
751     * @param int $B
752     * @return string
753     */
754    public function toHTMLColor($R, $G, $B)
755    {
756        $R = intval($R);
757        $G = intval($G);
758        $B = intval($B);
759        $R = dechex($R < 0 ? 0 : ($R > 255 ? 255 : $R));
760        $G = dechex($G < 0 ? 0 : ($G > 255 ? 255 : $G));
761        $B = dechex($B < 0 ? 0 : ($B > 255 ? 255 : $B));
762        $Color = "#" . (strlen($R) < 2 ? '0' : '') . $R;
763        $Color .= (strlen($G) < 2 ? '0' : '') . $G;
764        $Color .= (strlen($B) < 2 ? '0' : '') . $B;
765
766        return $Color;
767    }
768
769    /**
770     * Reverse an array of points
771     * @param array<int, int|float|numeric-string> $Plots
772     * @return list<int|float|numeric-string>
773     */
774    public function reversePlots(array $Plots)
775    {
776        $Result = [];
777        for ($i = count($Plots) - 2; $i >= 0; $i = $i - 2) {
778            $Result[] = $Plots[$i];
779            $Result[] = $Plots[$i + 1];
780        }
781
782        return $Result;
783    }
784
785    /**
786     * Mirror Effect
787     *
788     * @param int $X
789     * @param int $Y
790     * @param int $Width
791     * @param int $Height
792     * @param array{ StartAlpha?: int, EndAlpha?: int } $Format
793     * @return void
794     */
795    public function drawAreaMirror($X, $Y, $Width, $Height, array $Format = [])
796    {
797        $StartAlpha = $Format["StartAlpha"] ?? 80;
798        $EndAlpha = isset($Format["EndAlpha"]) ? $Format["EndAlpha"] : 0;
799
800        $AlphaStep = ($StartAlpha - $EndAlpha) / $Height;
801
802        $Picture = imagecreatetruecolor($this->XSize, $this->YSize);
803        imagecopy($Picture, $this->Picture, 0, 0, 0, 0, $this->XSize, $this->YSize);
804
805        for ($i = 1; $i <= $Height; $i++) {
806            if ($Y + ($i - 1) < $this->YSize && $Y - $i > 0) {
807                imagecopymerge(
808                    $Picture,
809                    $this->Picture,
810                    $X,
811                    $Y + ($i - 1),
812                    $X,
813                    $Y - $i,
814                    $Width,
815                    1,
816                    $StartAlpha - $AlphaStep * $i
817                );
818            }
819        }
820
821        imagecopy($this->Picture, $Picture, 0, 0, 0, 0, $this->XSize, $this->YSize);
822    }
823
824    /**
825     * @param string $mode
826     */
827    private function openImageStorageFileHandle($mode = "r"): mixed
828    {
829        return @fopen(
830            "$this->ImageMapStorageFolder/$this->ImageMapFileName.map",
831            $mode
832        );
833    }
834}
835