20 use League\ColorExtractor\Color;
21 use League\ColorExtractor\ColorExtractor;
22 use League\ColorExtractor\Palette;
30 ERR_FILE_NOT_FOUND = 1;
36 ERR_FREETYPE_NOT_ENABLED = 3;
39 ERR_GD_NOT_ENABLED = 4;
42 ERR_INVALID_COLOR = 5;
45 ERR_INVALID_DATA_URI = 6;
48 ERR_INVALID_IMAGE = 7;
51 ERR_LIB_NOT_LOADED = 8;
54 ERR_UNSUPPORTED_FORMAT = 9;
57 ERR_WEBP_NOT_ENABLED = 10;
63 ERR_INVALID_FLAG = 12;
71 protected null|array|
false $exif;
85 public function __construct(
string $image =
'', array $flags = [])
88 if (extension_loaded(
'gd')) {
90 ini_set(
'gd.jpeg_ignore_warning',
'1');
92 throw new Exception(
'Required extension GD is not loaded.', self::ERR_GD_NOT_ENABLED);
101 foreach ($flags as $flag =>
$value) {
102 $this->setFlag($flag,
$value);
106 if (preg_match(
'/^data:(.*?);/', $image)) {
107 $this->fromDataUri($image);
109 $this->fromFile($image);
119 $type_check = (gettype($this->image) ==
'object' && $this->image::class ==
'GdImage');
121 if (is_resource($this->image) && $type_check) {
122 imagedestroy($this->image);
141 if (! in_array($flag, array_keys($this->flags))) {
142 throw new Exception(
'Invalid flag.', self::ERR_INVALID_FLAG);
146 $this->flags[$flag] =
$value;
156 return in_array($flag, array_keys($this->flags)) ? $this->flags[$flag] :
null;
174 preg_match(
'/^data:(.*?);/', $uri, $matches);
175 if (! count($matches)) {
176 throw new Exception(
'Invalid data URI.', self::ERR_INVALID_DATA_URI);
180 $this->mimeType = $matches[1];
181 if (! preg_match(
'/^image\/(gif|jpeg|png)$/', $this->mimeType)) {
183 'Unsupported format: '.$this->mimeType,
184 self::ERR_UNSUPPORTED_FORMAT
189 $uri = base64_decode(strval(preg_replace(
'/^data:(.*?);base64,/',
'', $uri)));
190 $this->image = imagecreatefromstring($uri);
191 if (! $this->image) {
192 throw new Exception(
'Invalid image data.', self::ERR_INVALID_IMAGE);
209 $sslVerify = $this->getFlag(
'sslVerify');
212 'verify_peer' => $sslVerify,
213 'verify_peer_name' => $sslVerify,
218 $file = @file_get_contents($file,
false, stream_context_create($opts));
219 if ($file ===
false) {
220 throw new Exception(
"File not found: $file", self::ERR_FILE_NOT_FOUND);
224 $this->image = imagecreatefromstring($file);
227 $info = @getimagesizefromstring($file);
228 if ($info ===
false) {
229 throw new Exception(
"Invalid image file: $file", self::ERR_INVALID_IMAGE);
231 $this->mimeType = $info[
'mime'];
233 if (! $this->image) {
234 throw new Exception(
'Unsupported format: '.$this->mimeType, self::ERR_UNSUPPORTED_FORMAT);
237 switch($this->mimeType) {
241 $width = imagesx($this->image);
242 $height = imagesx($this->image);
244 $gif = imagecreatetruecolor((
int) $width, (
int) $height);
245 $alpha = imagecolorallocatealpha($gif, 0, 0, 0, 127);
246 imagecolortransparent($gif, $alpha ?:
null);
247 imagefill($gif, 0, 0, $alpha);
249 imagecopy($this->image, $gif, 0, 0, 0, 0, $width, $height);
254 if (function_exists(
'exif_read_data')) {
255 $this->exif = @exif_read_data(
'data://image/jpeg;base64,'.base64_encode($file));
261 imagepalettetotruecolor($this->image);
276 public function fromNew(
int $width,
int $height,
string|array $color =
'transparent'): static
278 $this->image = imagecreatetruecolor($width, $height);
281 $this->mimeType =
'image/png';
302 return $this->fromFile(
'data://;base64,'.base64_encode($string));
321 $mimeType = $mimeType ?: $this->mimeType;
332 $quality = intval(
$options[
'quality']);
336 if ($quality ===
null) {
339 $quality = (int) round(self::keepWithin((
int) $quality, 0, 100));
344 $alpha = boolval(
$options[
'alpha']);
350 $interlace = boolval(
$options[
'interlace']);
362 imagesavealpha($this->image, $alpha);
363 imagegif($this->image, $file);
366 imageinterlace($this->image, $interlace);
367 imagejpeg($this->image, $file, $quality);
373 $filters = intval(
$options[
'filters']);
380 $compression = intval(
$options[
'compression']);
382 if ($compression !== -1) {
383 $compression = (int) round(self::keepWithin($compression, 0, 10));
385 imagesavealpha($this->image, $alpha);
386 imagepng($this->image, $file, $compression, $filters);
390 if (! function_exists(
'imagewebp')) {
392 'WEBP support is not enabled in your version of PHP.',
393 self::ERR_WEBP_NOT_ENABLED
397 imagesavealpha($this->image, $alpha);
398 imagewebp($this->image, $file, $quality);
401 case 'image/x-ms-bmp':
402 case 'image/x-windows-bmp':
404 if (! function_exists(
'imagebmp')) {
406 'BMP support is not available in your version of PHP.',
407 self::ERR_UNSUPPORTED_FORMAT
413 $compression = is_int(
$options[
'compression']) ?
416 imageinterlace($this->image, $interlace);
417 imagebmp($this->image, $file, $compression);
421 if (! function_exists(
'imageavif')) {
423 'AVIF support is not available in your version of PHP.',
424 self::ERR_UNSUPPORTED_FORMAT
431 $speed = self::keepWithin($speed, 0, 10);
434 imagesavealpha($this->image, $alpha);
435 imageavif($this->image, $file, $quality, $speed);
438 throw new Exception(
'Unsupported format: '.$mimeType, self::ERR_UNSUPPORTED_FORMAT);
442 $data = ob_get_contents();
447 'mimeType' => $mimeType,
462 $image = $this->generate($mimeType,
$options);
464 return 'data:'.$image[
'mimeType'].
';base64,'.base64_encode($image[
'data']);
479 $image = $this->generate($mimeType,
$options);
482 header(
'Cache-Control: must-revalidate, post-check=0, pre-check=0');
483 header(
'Content-Description: File Transfer');
484 header(
'Content-Length: '.strlen($image[
'data']));
485 header(
'Content-Transfer-Encoding: Binary');
486 header(
'Content-Type: application/octet-stream');
487 header(
"Content-Disposition: attachment; filename=\"$filename\"");
504 public function toFile(
string $file,
string $mimeType =
null, array|
int $options = 100): static
506 $image = $this->generate($mimeType,
$options);
509 if (! file_put_contents($file, $image[
'data'])) {
510 throw new Exception(
"Failed to write image to file: $file", self::ERR_WRITE);
527 $image = $this->generate($mimeType,
$options);
530 header(
'Content-Type: '.$image[
'mimeType']);
546 return $this->generate($mimeType,
$options)[
'data'];
559 protected static function keepWithin(
int|
float $value,
int|
float $min,
int|
float $max): int|float
578 return $this->getWidth() / $this->getHeight();
588 return $this->exif ??
null;
596 return (
int) imagesy($this->image);
604 return $this->mimeType;
614 $width = $this->getWidth();
615 $height = $this->getHeight();
617 if ($width > $height) {
620 if ($width < $height) {
634 return imageresolution($this->image);
642 return (
int) imagesx($this->image);
662 protected static function imageCopyMergeAlpha($dstIm, $srcIm,
int $dstX,
int $dstY,
int $srcX,
int $srcY,
int $srcW,
int $srcH,
int $pct): bool
667 imagealphablending($srcIm,
false);
668 imagefilter($srcIm, IMG_FILTER_COLORIZE, 0, 0, 0, round(127 * ((100 - $pct) / 100)));
671 imagecopy($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH);
686 $exif = $this->getExif();
688 if (! $exif || ! isset($exif[
'Orientation'])) {
692 switch($exif[
'Orientation']) {
705 $this->flip(
'y')->rotate(90);
711 $this->flip(
'x')->rotate(90);
728 public function bestFit(
int $maxWidth,
int $maxHeight): static
731 if ($this->getWidth() <= $maxWidth && $this->getHeight() <= $maxHeight) {
736 if ($this->getOrientation() ===
'portrait') {
737 $height = $maxHeight;
738 $width = (int) round($maxHeight * $this->getAspectRatio());
741 $height = (int) round($maxWidth / $this->getAspectRatio());
745 if ($width > $maxWidth) {
747 $height = (int) round($width / $this->getAspectRatio());
751 if ($height > $maxHeight) {
752 $height = $maxHeight;
753 $width = (int) round($height * $this->getAspectRatio());
756 return $this->resize($width, $height);
768 public function crop(
int|
float $x1,
int|
float $y1,
int|
float $x2,
int|
float $y2): static
771 $x1 = self::keepWithin($x1, 0, $this->getWidth());
772 $x2 = self::keepWithin($x2, 0, $this->getWidth());
773 $y1 = self::keepWithin($y1, 0, $this->getHeight());
774 $y2 = self::keepWithin($y2, 0, $this->getHeight());
777 $dstW = abs($x2 - $x1);
778 $dstH = abs($y2 - $y1);
779 $newImage = imagecreatetruecolor((
int) $dstW, (
int) $dstH);
780 $transparentColor = imagecolorallocatealpha($newImage, 0, 0, 0, 127);
781 imagecolortransparent($newImage, $transparentColor ?:
null);
782 imagefill($newImage, 0, 0, $transparentColor);
790 (
int) round(min($x1, $x2)),
791 (
int) round(min($y1, $y2)),
799 $this->image = $newImage;
813 public function duotone(
string|array $lightColor,
string|array $darkColor): static
815 $lightColor = self::normalizeColor($lightColor);
816 $darkColor = self::normalizeColor($darkColor);
819 $redAvg = $lightColor[
'red'] - $darkColor[
'red'];
820 $greenAvg = $lightColor[
'green'] - $darkColor[
'green'];
821 $blueAvg = $lightColor[
'blue'] - $darkColor[
'blue'];
825 for (
$i = 0;
$i <= 255;
$i++) {
827 $pixels[
'red'][
$i] = $darkColor[
'red'] + $grayAvg * $redAvg;
828 $pixels[
'green'][
$i] = $darkColor[
'green'] + $grayAvg * $greenAvg;
829 $pixels[
'blue'][
$i] = $darkColor[
'blue'] + $grayAvg * $blueAvg;
833 for ($x = 0; $x < $this->getWidth(); $x++) {
834 for ($y = 0; $y < $this->getHeight(); $y++) {
835 $rgb = $this->getColorAt($x, $y);
836 $gray = min(255, round(0.299 * $rgb[
'red'] + 0.114 * $rgb[
'blue'] + 0.587 * $rgb[
'green']));
838 'red' => $pixels[
'red'][$gray],
839 'green' => $pixels[
'green'][$gray],
840 'blue' => $pixels[
'blue'][$gray],
860 return $this->resize($width);
869 public function flip(
string $direction): static
872 'x' => imageflip($this->image, IMG_FLIP_HORIZONTAL),
873 'y' => imageflip($this->image, IMG_FLIP_VERTICAL),
874 'both' => imageflip($this->image, IMG_FLIP_BOTH),
888 public function maxColors(
int $max,
bool $dither =
true): static
890 imagetruecolortopalette($this->image, $dither, max(1, $max));
908 public function overlay(
string|
SimpleImage $overlay,
string $anchor =
'center',
float|
int $opacity = 1,
int $xOffset = 0,
int $yOffset = 0,
bool $calculateOffsetFromEdge =
false): static
916 $opacity = (int) round(self::keepWithin($opacity, 0, 1) * 100);
919 $spaceX = $this->getWidth() - $overlay->
getWidth();
920 $spaceY = $this->getHeight() - $overlay->
getHeight();
923 $x = (int) round(($spaceX / 2) + ($calculateOffsetFromEdge ? 0 : $xOffset));
924 $y = (int) round(($spaceY / 2) + ($calculateOffsetFromEdge ? 0 : $yOffset));
927 if (str_contains($anchor,
'top')) {
929 } elseif (str_contains($anchor,
'bottom')) {
930 $y = $spaceY + ($calculateOffsetFromEdge ? -$yOffset : $yOffset);
934 if (str_contains($anchor,
'left')) {
936 } elseif (str_contains($anchor,
'right')) {
937 $x = $spaceX + ($calculateOffsetFromEdge ? -$xOffset : $xOffset);
941 self::imageCopyMergeAlpha(
961 public function resize(
int $width =
null,
int $height =
null): static
964 if (! $width && ! $height) {
969 if ($width && ! $height) {
970 $height = (int) round($width / $this->getAspectRatio());
974 if (! $width && $height) {
975 $width = (int) round($height * $this->getAspectRatio());
979 if ($this->getWidth() === $width && $this->getHeight() === $height) {
986 $newImage = imagecreatetruecolor($width, $height);
987 $transparentColor = imagecolorallocatealpha($newImage, 0, 0, 0, 127);
988 imagecolortransparent($newImage, $transparentColor);
989 imagefill($newImage, 0, 0, $transparentColor);
1001 $this->image = $newImage;
1013 public function resolution(
int $res_x,
int $res_y =
null): static
1015 if (is_null($res_y)) {
1016 imageresolution($this->image, $res_x);
1018 imageresolution($this->image, $res_x, $res_y);
1033 public function rotate(
int $angle,
string|array $backgroundColor =
'transparent'): static
1036 $backgroundColor = $this->allocateColor($backgroundColor);
1038 $this->image = imagerotate(
1040 -(self::keepWithin($angle, -360, 360)),
1043 imagecolortransparent($this->image, imagecolorallocatealpha($this->image, 0, 0, 0, 127));
1073 public function text(
string $text, array
$options, array &$boundary =
null): static
1076 if (! function_exists(
'imagettftext')) {
1077 throw new Exception(
1078 'Freetype support is not enabled in your version of PHP.',
1079 self::ERR_FREETYPE_NOT_ENABLED
1088 'anchor' =>
'center',
1092 'calculateOffsetFromEdge' =>
false,
1093 'baselineAlign' =>
true,
1098 $size = (
$options[
'size'] / 96) * 72;
1099 $color = $this->allocateColor(
$options[
'color']);
1103 $calculateOffsetFromEdge =
$options[
'calculateOffsetFromEdge'];
1104 $baselineAlign =
$options[
'baselineAlign'];
1120 $boxText = imagettfbbox($size, $angle, $fontFile, $text);
1122 throw new Exception(
"Unable to load font file: $fontFile", self::ERR_FONT_FILE);
1125 $boxWidth = abs($boxText[4] - $boxText[0]);
1126 $boxHeight = abs($boxText[5] - $boxText[1]);
1130 if ($calculateOffsetFromEdge) {
1131 if (str_contains($anchor,
'bottom')) {
1134 if (str_contains($anchor,
'right')) {
1141 if ($baselineAlign) {
1143 $boxFull = imagettfbbox($size, $angle, $fontFile,
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890');
1145 if (str_contains($anchor,
'bottom')) {
1146 $yOffset -= $boxFull[1];
1147 } elseif (str_contains($anchor,
'top')) {
1148 $yOffset += abs($boxFull[5]) - $boxHeight;
1150 $boxFullHeight = abs($boxFull[1]) + abs($boxFull[5]);
1151 $yOffset += ($boxFullHeight / 2) - ($boxHeight / 2) - abs($boxFull[1]);
1156 $yOffset -= $boxText[1];
1161 $xOffset -= $boxText[0];
1167 $y = $yOffset + $boxHeight;
1170 $x = $this->getWidth() - $boxWidth + $xOffset;
1171 $y = $yOffset + $boxHeight;
1174 $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset;
1175 $y = $yOffset + $boxHeight;
1179 $y = $this->getHeight() + $yOffset;
1181 case 'bottom right':
1182 $x = $this->getWidth() - $boxWidth + $xOffset;
1183 $y = $this->getHeight() + $yOffset;
1186 $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset;
1187 $y = $this->getHeight() + $yOffset;
1191 $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset;
1194 $x = $this->getWidth() - $boxWidth + $xOffset;
1195 $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset;
1198 $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset;
1199 $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset;
1202 $x = (int) round($x);
1203 $y = (int) round($y);
1207 'x1' => $x + $boxText[0],
1208 'y1' => $y + $boxText[1] - $boxHeight,
1209 'x2' => $x + $boxWidth + $boxText[0],
1210 'y2' => $y + $boxText[1],
1211 'width' => $boxWidth,
1212 'height' => $boxHeight,
1216 if (is_array(
$options[
'shadow'])) {
1223 $this->allocateColor(
$options[
'shadow'][
'color']),
1230 imagettftext($this->image, $size, $angle, $x, $y, $color, $fontFile, $text);
1263 $maxWidth = $this->getWidth();
1269 'anchor' =>
'center',
1273 'calculateOffsetFromEdge' =>
false,
1274 'width' => $maxWidth,
1282 $fontSize = $fontSizePx =
$options[
'size'];
1283 $fontSize = ($fontSize / 96) * 72;
1289 $calculateOffsetFromEdge =
$options[
'calculateOffsetFromEdge'];
1290 $maxWidth = intval(
$options[
'width']);
1292 $leading = self::keepWithin($leading, ($fontSizePx * -1), $leading);
1296 if ($align ==
'right') {
1297 $align =
'top right';
1298 } elseif ($align ==
'center') {
1300 } elseif ($align ==
'justify') {
1303 $align =
'top left';
1306 [$lines, $isLastLine, $lastLineHeight] = self::textSeparateLines($text, $fontFile, $fontSize, $maxWidth);
1308 $maxHeight = (int) round(((is_countable($lines) ? count($lines) : 0) - 1) * ($fontSizePx * 1.2 + $leading) + $lastLineHeight);
1311 $imageText->fromNew($maxWidth, $maxHeight);
1314 if ($align !=
'justify') {
1315 foreach ($lines as $key => $line) {
1316 if ($align ==
'top') {
1317 $line = trim($line);
1319 $imageText->text($line, [
'fontFile' => $fontFile,
'size' => $fontSizePx,
'color' => $color,
'anchor' => $align,
'xOffset' => 0,
'yOffset' => $key * ($fontSizePx * 1.2 + $leading),
'shadow' => $shadow,
'calculateOffsetFromEdge' =>
true]);
1324 foreach ($lines as $keyLine => $line) {
1327 if (preg_match(
"/^\s+/", $line, $match)) {
1329 $spaces = strlen($match[0]);
1330 $line = ltrim($line);
1334 $words = preg_split(
"/\s+/", $line);
1336 $words[0] = str_repeat(
' ', $spaces).$words[0];
1340 foreach ($words as $key => $word) {
1341 $wordBox = imagettfbbox($fontSize, 0, $fontFile, $word);
1342 $wordWidth = abs($wordBox[4] - $wordBox[0]);
1343 $wordsSize[$key] = $wordWidth;
1345 $wordsSizeTotal = array_sum($wordsSize);
1348 $countWords = count($words);
1350 if ($countWords > 1) {
1351 $wordSpacing = ($maxWidth - $wordsSizeTotal) / ($countWords - 1);
1352 $wordSpacing = round($wordSpacing, 3);
1355 $xOffsetJustify = 0;
1356 foreach ($words as $key => $word) {
1357 if ($isLastLine[$keyLine]) {
1358 if ($key < (count($words) - 1)) {
1363 $imageText->text($word, [
'fontFile' => $fontFile,
'size' => $fontSizePx,
'color' => $color,
'anchor' =>
'top left',
'xOffset' => $xOffsetJustify,
'yOffset' => $keyLine * ($fontSizePx * 1.2 + $leading),
'shadow' => $shadow,
'calculateOffsetFromEdge' =>
true]
1366 $xOffsetJustify += $wordsSize[$key] + $wordSpacing;
1371 $this->overlay($imageText, $anchor, $opacity, $xOffset, $yOffset, $calculateOffsetFromEdge);
1379 private function textSeparateLines(
string $text,
string $fontFile,
int $fontSize,
int $maxWidth): array
1382 $words = self::textSeparateWords($text);
1383 $countWords = count($words) - 1;
1387 for (
$i = 0;
$i < $countWords;
$i++) {
1389 $isLastLine[$lineKey] =
false;
1390 if ($word === PHP_EOL) {
1391 $isLastLine[$lineKey] =
true;
1393 $lines[$lineKey] =
'';
1397 $lineBox = imagettfbbox($fontSize, 0, $fontFile, $lines[$lineKey].$word);
1398 if (abs($lineBox[4] - $lineBox[0]) < $maxWidth) {
1399 $lines[$lineKey] .= $word.
' ';
1402 $lines[$lineKey] = $word.
' ';
1405 $isLastLine[$lineKey] =
true;
1407 $lines = array_map(
'rtrim', $lines);
1409 $boxFull = imagettfbbox($fontSize, 0, $fontFile,
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890');
1410 $lineBox = imagettfbbox($fontSize, 0, $fontFile, $lines[$lineKey]);
1412 $lastLineHeight = abs($lineBox[1]) + abs($boxFull[5]);
1414 return [$lines, $isLastLine, $lastLineHeight];
1423 $text = strval(preg_replace(
'/(\r\n|\n|\r)/', PHP_EOL, $text));
1424 $text = explode(PHP_EOL, $text);
1426 foreach ($text as $line) {
1427 $newText = array_merge($newText, explode(
' ', $line), [PHP_EOL]);
1446 $currentRatio = $this->getHeight() / $this->getWidth();
1447 $targetRatio = $height / $width;
1450 if ($targetRatio > $currentRatio) {
1451 $this->resize(
null, $height);
1453 $this->resize($width);
1458 $x1 = floor(($this->getWidth() / 2) - ($width / 2));
1464 $x1 = floor(($this->getWidth() / 2) - ($width / 2));
1466 $y1 = $this->getHeight() - $height;
1467 $y2 = $this->getHeight();
1472 $y1 = floor(($this->getHeight() / 2) - ($height / 2));
1473 $y2 = $height + $y1;
1476 $x1 = $this->getWidth() - $width;
1477 $x2 = $this->getWidth();
1478 $y1 = floor(($this->getHeight() / 2) - ($height / 2));
1479 $y2 = $height + $y1;
1488 $x1 = $this->getWidth() - $width;
1489 $x2 = $this->getWidth();
1496 $y1 = $this->getHeight() - $height;
1497 $y2 = $this->getHeight();
1499 case 'bottom right':
1500 $x1 = $this->getWidth() - $width;
1501 $x2 = $this->getWidth();
1502 $y1 = $this->getHeight() - $height;
1503 $y2 = $this->getHeight();
1506 $x1 = floor(($this->getWidth() / 2) - ($width / 2));
1508 $y1 = floor(($this->getHeight() / 2) - ($height / 2));
1509 $y2 = $height + $y1;
1514 return $this->crop($x1, $y1, $x2, $y2);
1536 public function arc(
int $x,
int $y,
int $width,
int $height,
int $start,
int $end,
string|array $color,
int|
string $thickness = 1): static
1539 $tempColor = $this->allocateColor($color);
1540 imagesetthickness($this->image, 1);
1543 if ($thickness ===
'filled') {
1544 imagefilledarc($this->image, $x, $y, $width, $height, $start, $end, $tempColor, IMG_ARC_PIE);
1545 } elseif ($thickness === 1) {
1546 imagearc($this->image, $x, $y, $width, $height, $start, $end, $tempColor);
1550 $tempImage->fromNew($this->getWidth(), $this->getHeight());
1553 $tempColor = $tempImage->allocateColor($color);
1554 imagefilledarc($tempImage->image, $x, $y, $width + $thickness, $height + $thickness, $start, $end, $tempColor, IMG_ARC_PIE);
1557 $tempColor = (self::normalizeColor($color)[
'red'] == 255) ?
'blue' :
'red';
1558 $tempColor = $tempImage->allocateColor($tempColor);
1559 imagefilledarc($tempImage->image, $x, $y, $width - $thickness, $height - $thickness, $start, $end, $tempColor, IMG_ARC_PIE);
1562 $tempImage->excludeInsideColor($x, $y, $color);
1565 $this->overlay($tempImage);
1580 public function border(
string|array $color,
int $thickness = 1): static
1584 $x2 = $this->getWidth();
1585 $y2 = $this->getHeight() - 1;
1587 $color = $this->allocateColor($color);
1588 imagesetthickness($this->image, $thickness * 2);
1589 imagerectangle($this->image, $x1, $y1, $x2, $y2, $color);
1604 public function dot(
int $x,
int $y,
string|array $color): static
1606 $color = $this->allocateColor($color);
1607 imagesetpixel($this->image, $x, $y, $color);
1625 public function ellipse(
int $x,
int $y,
int $width,
int $height,
string|array $color,
int|array $thickness = 1): static
1628 $tempColor = $this->allocateColor($color);
1629 imagesetthickness($this->image, 1);
1632 if ($thickness ==
'filled') {
1633 imagefilledellipse($this->image, $x, $y, $width, $height, $tempColor);
1634 } elseif ($thickness === 1) {
1635 imageellipse($this->image, $x, $y, $width, $height, $tempColor);
1639 $tempImage->fromNew($this->getWidth(), $this->getHeight());
1642 $tempColor = $tempImage->allocateColor($color);
1643 imagefilledellipse($tempImage->image, $x, $y, $width + $thickness, $height + $thickness, $tempColor);
1646 $tempColor = (self::normalizeColor($color)[
'red'] == 255) ?
'blue' :
'red';
1647 $tempColor = $tempImage->allocateColor($tempColor);
1648 imagefilledellipse($tempImage->image, $x, $y, $width - $thickness, $height - $thickness, $tempColor);
1651 $tempImage->excludeInsideColor($x, $y, $color);
1654 $this->overlay($tempImage);
1668 public function fill(
string|array $color): static
1671 $this->rectangle(0, 0, $this->getWidth(), $this->getHeight(),
'white',
'filled');
1674 $color = $this->allocateColor($color);
1675 imagefill($this->image, 0, 0, $color);
1693 public function line(
int $x1,
int $y1,
int $x2,
int $y2,
string|array $color,
int $thickness = 1): static
1696 $color = $this->allocateColor($color);
1699 imagesetthickness($this->image, $thickness);
1700 imageline($this->image, $x1, $y1, $x2, $y2, $color);
1722 public function polygon(array $vertices,
string|array $color,
string|
int|array $thickness = 1): static
1725 $color = $this->allocateColor($color);
1729 foreach ($vertices as $vals) {
1730 $points[] = $vals[
'x'];
1731 $points[] = $vals[
'y'];
1735 if ($thickness ==
'filled') {
1736 imagesetthickness($this->image, 1);
1737 imagefilledpolygon($this->image, $points, count($vertices), $color);
1739 imagesetthickness($this->image, $thickness);
1740 imagepolygon($this->image, $points, count($vertices), $color);
1759 public function rectangle(
int $x1,
int $y1,
int $x2,
int $y2,
string|array $color,
string|
int|array $thickness = 1): static
1762 $color = $this->allocateColor($color);
1765 if ($thickness ==
'filled') {
1766 imagesetthickness($this->image, 1);
1767 imagefilledrectangle($this->image, $x1, $y1, $x2, $y2, $color);
1769 imagesetthickness($this->image, $thickness);
1770 imagerectangle($this->image, $x1, $y1, $x2, $y2, $color);
1790 public function roundedRectangle(
int $x1,
int $y1,
int $x2,
int $y2,
int $radius,
string|array $color,
string|
int|array $thickness = 1): static
1792 if ($thickness ==
'filled') {
1794 $this->rectangle($x1 + $radius + 1, $y1, $x2 - $radius - 1, $y2, $color,
'filled');
1795 $this->rectangle($x1, $y1 + $radius + 1, $x1 + $radius, $y2 - $radius - 1, $color,
'filled');
1796 $this->rectangle($x2 - $radius, $y1 + $radius + 1, $x2, $y2 - $radius - 1, $color,
'filled');
1799 $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color,
'filled');
1800 $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color,
'filled');
1801 $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color,
'filled');
1802 $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color,
'filled');
1804 $offset = $thickness / 2;
1809 $radius = self::keepWithin($radius, 0, min(($x2 - $x1) / 2, ($y2 - $y1) / 2) - 1);
1810 $radius = (int) floor($radius);
1811 $thickness = self::keepWithin($thickness, 1, min(($x2 - $x1) / 2, ($y2 - $y1) / 2));
1815 $tempImage->fromNew($this->getWidth(), $this->getHeight());
1818 $tempImage->roundedRectangle($x1, $y1, $x2, $y2, $radius, $color,
'filled');
1821 $tempColor = (self::normalizeColor($color)[
'red'] == 255) ?
'blue' :
'red';
1822 $radius = $radius - $thickness;
1823 $radius = self::keepWithin($radius, 0, $radius);
1824 $tempImage->roundedRectangle(
1835 $tempImage->excludeInsideColor(($x2 + $x1) / 2, ($y2 + $y1) / 2, $color);
1838 $this->overlay($tempImage);
1856 $borderColor = $this->allocateColor($borderColor);
1857 $transparent = $this->allocateColor(
'transparent');
1858 imagefilltoborder($this->image, $x, $y, $borderColor, $transparent);
1874 public function blur(
string $type =
'selective',
int $passes = 1): static
1876 $filter =
$type ===
'gaussian' ? IMG_FILTER_GAUSSIAN_BLUR : IMG_FILTER_SELECTIVE_BLUR;
1878 for (
$i = 0;
$i < $passes;
$i++) {
1879 imagefilter($this->image, $filter);
1893 $percentage = self::keepWithin(255 * $percentage / 100, 0, 255);
1895 imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $percentage);
1910 $color = self::normalizeColor($color);
1914 IMG_FILTER_COLORIZE,
1918 127 - ($color[
'alpha'] * 127)
1932 imagefilter($this->image, IMG_FILTER_CONTRAST, self::keepWithin($percentage, -100, 100));
1943 public function darken(
int $percentage): static
1945 $percentage = self::keepWithin(255 * $percentage / 100, 0, 255);
1947 imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -$percentage);
1959 imagefilter($this->image, IMG_FILTER_GRAYSCALE);
1971 imagefilter($this->image, IMG_FILTER_EDGEDETECT);
1983 imagefilter($this->image, IMG_FILTER_EMBOSS);
1995 imagefilter($this->image, IMG_FILTER_NEGATE);
2012 $newImage->fromNew($this->getWidth(), $this->getHeight());
2015 self::imageCopyMergeAlpha(
2022 (
int) round(self::keepWithin($opacity, 0, 1) * 100)
2036 imagefilter($this->image, IMG_FILTER_PIXELATE, $size,
true);
2048 imagefilter($this->image, IMG_FILTER_GRAYSCALE);
2049 imagefilter($this->image, IMG_FILTER_COLORIZE, 70, 35, 0);
2063 $amount = max(1, min(100, $amount)) / 100;
2067 [-1, 8 / $amount, -1],
2070 $divisor = array_sum(array_map(
'array_sum', $sharpen));
2072 imageconvolution($this->image, $sharpen, $divisor, 0);
2084 imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL);
2102 $color = self::normalizeColor($color);
2105 $index = imagecolorexactalpha(
2110 (
int) (127 - ($color[
'alpha'] * 127))
2118 return imagecolorallocatealpha(
2123 127 - ($color[
'alpha'] * 127)
2139 public static function adjustColor(
string|array $color,
int $red,
int $green,
int $blue,
int $alpha): array
2142 $color = self::normalizeColor($color);
2145 return self::normalizeColor([
2146 'red' => $color[
'red'] + $red,
2147 'green' => $color[
'green'] + $green,
2148 'blue' => $color[
'blue'] + $blue,
2149 'alpha' => $color[
'alpha'] + $alpha,
2162 public static function darkenColor(
string|array $color,
int $amount): array
2164 return self::adjustColor($color, -$amount, -$amount, -$amount, 0);
2182 public function extractColors(
int $count = 5,
string|array $backgroundColor =
null): array
2185 if (! class_exists(
'\\'.ColorExtractor::class)) {
2186 throw new Exception(
2187 'Required library \League\ColorExtractor is missing.',
2188 self::ERR_LIB_NOT_LOADED
2193 if ($backgroundColor) {
2194 $backgroundColor = self::normalizeColor($backgroundColor);
2195 $backgroundColor = Color::fromRgbToInt([
2196 'r' => $backgroundColor[
'red'],
2197 'g' => $backgroundColor[
'green'],
2198 'b' => $backgroundColor[
'blue'],
2203 $palette = Palette::fromGD($this->image, $backgroundColor);
2204 $extractor =
new ColorExtractor($palette);
2205 $colors = $extractor->extract($count);
2208 foreach ($colors as $key =>
$value) {
2209 $colors[$key] = self::normalizeColor(Color::fromIntToHex(
$value));
2225 if ($x < 0 || $x > $this->getWidth() || $y < 0 || $y > $this->getHeight()) {
2230 $color = imagecolorat($this->image, $x, $y);
2231 $rgba = imagecolorsforindex($this->image, $color);
2232 $rgba[
'alpha'] = 127 - ($color >> 24) & 0xFF;
2246 public static function lightenColor(
string|array $color,
int $amount): array
2248 return self::adjustColor($color, $amount, $amount, $amount, 0);
2267 'aliceblue' =>
'#f0f8ff',
'antiquewhite' =>
'#faebd7',
'aqua' =>
'#00ffff',
2268 'aquamarine' =>
'#7fffd4',
'azure' =>
'#f0ffff',
'beige' =>
'#f5f5dc',
'bisque' =>
'#ffe4c4',
2269 'black' =>
'#000000',
'blanchedalmond' =>
'#ffebcd',
'blue' =>
'#0000ff',
2270 'blueviolet' =>
'#8a2be2',
'brown' =>
'#a52a2a',
'burlywood' =>
'#deb887',
2271 'cadetblue' =>
'#5f9ea0',
'chartreuse' =>
'#7fff00',
'chocolate' =>
'#d2691e',
2272 'coral' =>
'#ff7f50',
'cornflowerblue' =>
'#6495ed',
'cornsilk' =>
'#fff8dc',
2273 'crimson' =>
'#dc143c',
'cyan' =>
'#00ffff',
'darkblue' =>
'#00008b',
'darkcyan' =>
'#008b8b',
2274 'darkgoldenrod' =>
'#b8860b',
'darkgray' =>
'#a9a9a9',
'darkgrey' =>
'#a9a9a9',
2275 'darkgreen' =>
'#006400',
'darkkhaki' =>
'#bdb76b',
'darkmagenta' =>
'#8b008b',
2276 'darkolivegreen' =>
'#556b2f',
'darkorange' =>
'#ff8c00',
'darkorchid' =>
'#9932cc',
2277 'darkred' =>
'#8b0000',
'darksalmon' =>
'#e9967a',
'darkseagreen' =>
'#8fbc8f',
2278 'darkslateblue' =>
'#483d8b',
'darkslategray' =>
'#2f4f4f',
'darkslategrey' =>
'#2f4f4f',
2279 'darkturquoise' =>
'#00ced1',
'darkviolet' =>
'#9400d3',
'deeppink' =>
'#ff1493',
2280 'deepskyblue' =>
'#00bfff',
'dimgray' =>
'#696969',
'dimgrey' =>
'#696969',
2281 'dodgerblue' =>
'#1e90ff',
'firebrick' =>
'#b22222',
'floralwhite' =>
'#fffaf0',
2282 'forestgreen' =>
'#228b22',
'fuchsia' =>
'#ff00ff',
'gainsboro' =>
'#dcdcdc',
2283 'ghostwhite' =>
'#f8f8ff',
'gold' =>
'#ffd700',
'goldenrod' =>
'#daa520',
'gray' =>
'#808080',
2284 'grey' =>
'#808080',
'green' =>
'#008000',
'greenyellow' =>
'#adff2f',
2285 'honeydew' =>
'#f0fff0',
'hotpink' =>
'#ff69b4',
'indianred ' =>
'#cd5c5c',
2286 'indigo ' =>
'#4b0082',
'ivory' =>
'#fffff0',
'khaki' =>
'#f0e68c',
'lavender' =>
'#e6e6fa',
2287 'lavenderblush' =>
'#fff0f5',
'lawngreen' =>
'#7cfc00',
'lemonchiffon' =>
'#fffacd',
2288 'lightblue' =>
'#add8e6',
'lightcoral' =>
'#f08080',
'lightcyan' =>
'#e0ffff',
2289 'lightgoldenrodyellow' =>
'#fafad2',
'lightgray' =>
'#d3d3d3',
'lightgrey' =>
'#d3d3d3',
2290 'lightgreen' =>
'#90ee90',
'lightpink' =>
'#ffb6c1',
'lightsalmon' =>
'#ffa07a',
2291 'lightseagreen' =>
'#20b2aa',
'lightskyblue' =>
'#87cefa',
'lightslategray' =>
'#778899',
2292 'lightslategrey' =>
'#778899',
'lightsteelblue' =>
'#b0c4de',
'lightyellow' =>
'#ffffe0',
2293 'lime' =>
'#00ff00',
'limegreen' =>
'#32cd32',
'linen' =>
'#faf0e6',
'magenta' =>
'#ff00ff',
2294 'maroon' =>
'#800000',
'mediumaquamarine' =>
'#66cdaa',
'mediumblue' =>
'#0000cd',
2295 'mediumorchid' =>
'#ba55d3',
'mediumpurple' =>
'#9370db',
'mediumseagreen' =>
'#3cb371',
2296 'mediumslateblue' =>
'#7b68ee',
'mediumspringgreen' =>
'#00fa9a',
2297 'mediumturquoise' =>
'#48d1cc',
'mediumvioletred' =>
'#c71585',
'midnightblue' =>
'#191970',
2298 'mintcream' =>
'#f5fffa',
'mistyrose' =>
'#ffe4e1',
'moccasin' =>
'#ffe4b5',
2299 'navajowhite' =>
'#ffdead',
'navy' =>
'#000080',
'oldlace' =>
'#fdf5e6',
'olive' =>
'#808000',
2300 'olivedrab' =>
'#6b8e23',
'orange' =>
'#ffa500',
'orangered' =>
'#ff4500',
2301 'orchid' =>
'#da70d6',
'palegoldenrod' =>
'#eee8aa',
'palegreen' =>
'#98fb98',
2302 'paleturquoise' =>
'#afeeee',
'palevioletred' =>
'#db7093',
'papayawhip' =>
'#ffefd5',
2303 'peachpuff' =>
'#ffdab9',
'peru' =>
'#cd853f',
'pink' =>
'#ffc0cb',
'plum' =>
'#dda0dd',
2304 'powderblue' =>
'#b0e0e6',
'purple' =>
'#800080',
'rebeccapurple' =>
'#663399',
2305 'red' =>
'#ff0000',
'rosybrown' =>
'#bc8f8f',
'royalblue' =>
'#4169e1',
2306 'saddlebrown' =>
'#8b4513',
'salmon' =>
'#fa8072',
'sandybrown' =>
'#f4a460',
2307 'seagreen' =>
'#2e8b57',
'seashell' =>
'#fff5ee',
'sienna' =>
'#a0522d',
2308 'silver' =>
'#c0c0c0',
'skyblue' =>
'#87ceeb',
'slateblue' =>
'#6a5acd',
2309 'slategray' =>
'#708090',
'slategrey' =>
'#708090',
'snow' =>
'#fffafa',
2310 'springgreen' =>
'#00ff7f',
'steelblue' =>
'#4682b4',
'tan' =>
'#d2b48c',
'teal' =>
'#008080',
2311 'thistle' =>
'#d8bfd8',
'tomato' =>
'#ff6347',
'turquoise' =>
'#40e0d0',
2312 'violet' =>
'#ee82ee',
'wheat' =>
'#f5deb3',
'white' =>
'#ffffff',
'whitesmoke' =>
'#f5f5f5',
2313 'yellow' =>
'#ffff00',
'yellowgreen' =>
'#9acd32',
2317 if (is_string($color) && strstr($color,
'|')) {
2318 $color = explode(
'|', $color);
2319 $alpha = (float) $color[1];
2320 $color = trim($color[0]);
2326 if (is_string($color) && array_key_exists(strtolower($color), $cssColors)) {
2327 $color = $cssColors[strtolower($color)];
2331 if ($color ===
'transparent') {
2332 $color = [
'red' => 0,
'green' => 0,
'blue' => 0,
'alpha' => 0];
2336 if (is_string($color)) {
2338 $hex = strval(preg_replace(
'/^#/',
'', $color));
2341 if (strlen($hex) === 3) {
2342 [$red, $green, $blue] = [
2347 } elseif (strlen($hex) === 6) {
2348 [$red, $green, $blue] = [
2354 throw new Exception(
"Invalid color value: $color", self::ERR_INVALID_COLOR);
2359 'red' => hexdec($red),
2360 'green' => hexdec($green),
2361 'blue' => hexdec($blue),
2367 if (is_array($color)) {
2369 $color[
'red'] ??= 0;
2370 $color[
'green'] ??= 0;
2371 $color[
'blue'] ??= 0;
2374 $color[
'alpha'] ??= 1;
2377 'red' => (int) self::keepWithin((
int) $color[
'red'], 0, 255),
2378 'green' => (
int) self::keepWithin((
int) $color[
'green'], 0, 255),
2379 'blue' => (int) self::keepWithin((
int) $color[
'blue'], 0, 255),
2380 'alpha' => self::keepWithin($color[
'alpha'], 0, 1),
2384 throw new Exception(
"Invalid color value: $color", self::ERR_INVALID_COLOR);
textSeparateLines(string $text, string $fontFile, int $fontSize, int $maxWidth)
roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, string|array $color, string|int|array $thickness=1)
static keepWithin(int|float $value, int|float $min, int|float $max)
text(string $text, array $options, array &$boundary=null)
static imageCopyMergeAlpha($dstIm, $srcIm, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
toScreen(string $mimeType=null, array|int $options=100)
contrast(int $percentage)
resolution(int $res_x, int $res_y=null)
toString(string $mimeType=null, array|int $options=100)
generate(string $mimeType=null, array|int $options=[])
setFlag(string $flag, bool $value)
toFile(string $file, string $mimeType=null, array|int $options=100)
getColorAt(int $x, int $y)
ellipse(int $x, int $y, int $width, int $height, string|array $color, int|array $thickness=1)
toDownload(string $filename, string $mimeType=null, array|int $options=100)
blur(string $type='selective', int $passes=1)
excludeInsideColor(int $x, int $y, string|array $borderColor)
overlay(string|SimpleImage $overlay, string $anchor='center', float|int $opacity=1, int $xOffset=0, int $yOffset=0, bool $calculateOffsetFromEdge=false)
line(int $x1, int $y1, int $x2, int $y2, string|array $color, int $thickness=1)
bestFit(int $maxWidth, int $maxHeight)
crop(int|float $x1, int|float $y1, int|float $x2, int|float $y2)
static darkenColor(string|array $color, int $amount)
brighten(int $percentage)
static adjustColor(string|array $color, int $red, int $green, int $blue, int $alpha)
__construct(string $image='', array $flags=[])
textBox(string $text, array $options)
rectangle(int $x1, int $y1, int $x2, int $y2, string|array $color, string|int|array $thickness=1)
rotate(int $angle, string|array $backgroundColor='transparent')
toDataUri(string $mimeType=null, array|int $options=100)
resize(int $width=null, int $height=null)
textSeparateWords(string $text)
border(string|array $color, int $thickness=1)
thumbnail(int $width, int $height, string $anchor='center')
static lightenColor(string|array $color, int $amount)
fromNew(int $width, int $height, string|array $color='transparent')
static normalizeColor(string|array $color)
duotone(string|array $lightColor, string|array $darkColor)
maxColors(int $max, bool $dither=true)
dot(int $x, int $y, string|array $color)
arc(int $x, int $y, int $width, int $height, int $start, int $end, string|array $color, int|string $thickness=1)
extractColors(int $count=5, string|array $backgroundColor=null)
fromString(string $string)
colorize(string|array $color)
polygon(array $vertices, string|array $color, string|int|array $thickness=1)
fill(string|array $color)
allocateColor(string|array $color)
This class serves methods to create backup from files.