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("€", "\u20AC", $Title), 462 ENT_QUOTES, 463 "ISO-8859-15" 464 ); 465 if ($HTMLEncode) { 466 $Message = str_replace( 467 ">", 468 ">", 469 str_replace( 470 "<", 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