Skip to content

Commit

Permalink
added ImageColor, replaces Image::rgb()
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Oct 25, 2023
1 parent d9fd477 commit 962e3a7
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 42 deletions.
78 changes: 36 additions & 42 deletions src/Utils/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
* @method array affineMatrixGet(int $type, mixed $options = null)
* @method void alphaBlending(bool $on)
* @method void antialias(bool $on)
* @method void arc($x, $y, $w, $h, $start, $end, $color)
* @method void char(int $font, $x, $y, string $char, $color)
* @method void charUp(int $font, $x, $y, string $char, $color)
* @method void arc($x, $y, $w, $h, $start, $end, ImageColor $color)
* @method void char(int $font, $x, $y, string $char, ImageColor $color)
* @method void charUp(int $font, $x, $y, string $char, ImageColor $color)
* @method int colorAllocate($red, $green, $blue)
* @method int colorAllocateAlpha($red, $green, $blue, $alpha)
* @method int colorAt($x, $y)
Expand All @@ -52,14 +52,14 @@
* @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity)
* @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH)
* @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH)
* @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1)
* @method void ellipse($cx, $cy, $w, $h, $color)
* @method void fill($x, $y, $color)
* @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style)
* @method void filledEllipse($cx, $cy, $w, $h, $color)
* @method void filledPolygon(array $points, $numPoints, $color)
* @method void filledRectangle($x1, $y1, $x2, $y2, $color)
* @method void fillToBorder($x, $y, $border, $color)
* @method Image cropAuto(int $mode = -1, float $threshold = .5, ?ImageColor $color = null)
* @method void ellipse($cx, $cy, $w, $h, ImageColor $color)
* @method void fill($x, $y, ImageColor $color)
* @method void filledArc($cx, $cy, $w, $h, $s, $e, ImageColor $color, $style)
* @method void filledEllipse($cx, $cy, $w, $h, ImageColor $color)
* @method void filledPolygon(array $points, $numPoints, ImageColor $color)
* @method void filledRectangle($x1, $y1, $x2, $y2, ImageColor $color)
* @method void fillToBorder($x, $y, $border, ImageColor $color)
* @method void filter($filtertype)
* @method void flip(int $mode)
* @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null)
Expand All @@ -68,28 +68,28 @@
* @method int interlace($interlace = null)
* @method bool isTrueColor()
* @method void layerEffect($effect)
* @method void line($x1, $y1, $x2, $y2, $color)
* @method void openPolygon(array $points, int $num_points, int $color)
* @method void line($x1, $y1, $x2, $y2, ImageColor $color)
* @method void openPolygon(array $points, int $num_points, ImageColor $color)
* @method void paletteCopy(Image $source)
* @method void paletteToTrueColor()
* @method void polygon(array $points, $numPoints, $color)
* @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null)
* @method void polygon(array $points, $numPoints, ImageColor $color)
* @method array psText(string $text, $font, $size, ImageColor $color, ImageColor $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null)
* @method void rectangle($x1, $y1, $x2, $y2, $col)
* @method mixed resolution(int $res_x = null, int $res_y = null)
* @method Image rotate(float $angle, $backgroundColor)
* @method Image rotate(float $angle, ImageColor $backgroundColor)
* @method void saveAlpha(bool $saveflag)
* @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED)
* @method void setBrush(Image $brush)
* @method void setClip(int $x1, int $y1, int $x2, int $y2)
* @method void setInterpolation(int $method = IMG_BILINEAR_FIXED)
* @method void setPixel($x, $y, $color)
* @method void setPixel($x, $y, ImageColor $color)
* @method void setStyle(array $style)
* @method void setThickness($thickness)
* @method void setTile(Image $tile)
* @method void string($font, $x, $y, string $s, $col)
* @method void stringUp($font, $x, $y, string $s, $col)
* @method void trueColorToPalette(bool $dither, $ncolors)
* @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text)
* @method array ttfText($size, $angle, $x, $y, ImageColor $color, string $fontfile, string $text)
* @property-read positive-int $width
* @property-read positive-int $height
* @property-read \GdImage $imageResource
Expand Down Expand Up @@ -149,6 +149,7 @@ class Image

/**
* Returns RGB color (0..255) and transparency (0..127).
* @deprecated use ImageColor::rgb()
*/
public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array
{
Expand Down Expand Up @@ -224,7 +225,7 @@ private static function invokeSafe(string $func, string $arg, string $message, s
* @param positive-int $height
* @throws Nette\NotSupportedException if gd extension is not loaded
*/
public static function fromBlank(int $width, int $height, ?array $color = null): static
public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
Expand All @@ -234,16 +235,14 @@ public static function fromBlank(int $width, int $height, ?array $color = null):
throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.');
}

$image = imagecreatetruecolor($width, $height);
$image = new static(imagecreatetruecolor($width, $height));
if ($color) {
$color += ['alpha' => 0];
$color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
imagealphablending($image, false);
imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
imagealphablending($image, true);
$image->alphablending(false);

Check failure on line 240 in src/Utils/Image.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method Nette\Utils\Image::alphablending().
$image->filledrectangle(0, 0, $width - 1, $height - 1, $color);

Check failure on line 241 in src/Utils/Image.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method Nette\Utils\Image::filledrectangle().
$image->alphablending(true);

Check failure on line 242 in src/Utils/Image.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method Nette\Utils\Image::alphablending().
}

return new static($image);
return $image;
}


Expand Down Expand Up @@ -389,7 +388,7 @@ public function resize(int|string|null $width, int|string|null $height, int $mod
[$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode);

if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
$newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource();
$newImage = static::fromBlank($newWidth, $newHeight, ImageColor::rgb(0, 0, 0, 0))->getImageResource();
imagecopyresampled(
$newImage,
$this->image,
Expand Down Expand Up @@ -492,7 +491,7 @@ public function crop(int|string $left, int|string $top, int|string $width, int|s
$this->image = imagecrop($this->image, $r);
imagesavealpha($this->image, true);
} else {
$newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource();
$newImage = static::fromBlank($r['width'], $r['height'], ImageColor::rgb(0, 0, 0, 0))->getImageResource();
imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
$this->image = $newImage;
}
Expand Down Expand Up @@ -727,20 +726,8 @@ public function __call(string $name, array $args): mixed
if ($value instanceof self) {
$args[$key] = $value->getImageResource();

} elseif (is_array($value) && isset($value['red'])) { // rgb
$args[$key] = imagecolorallocatealpha(
$this->image,
$value['red'],
$value['green'],
$value['blue'],
$value['alpha'],
) ?: imagecolorresolvealpha(
$this->image,
$value['red'],
$value['green'],
$value['blue'],
$value['alpha'],
);
} elseif ($value instanceof ImageColor || (is_array($value) && isset($value['red']))) {
$args[$key] = $this->resolveColor($value);
}
}

Expand Down Expand Up @@ -780,4 +767,11 @@ public function __sleep(): array
{
throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.');
}


public function resolveColor(ImageColor|array $color): int
{
$color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color);
return imagecolorallocatealpha($this->image, ...$color) ?: imagecolorresolvealpha($this->image, ...$color);
}
}
75 changes: 75 additions & 0 deletions src/Utils/ImageColor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
* Represent RGB color (0..255) with opacity (0..1).
*/
class ImageColor
{
public static function rgb(int $red, int $green, int $blue, float $opacity = 1): self
{
return new self($red, $green, $blue, $opacity);
}


/**
* Accepts formats #RRGGBB, #RRGGBBAA, #RGB, #RGBA
*/
public static function hex(string $hex): self
{
$hex = ltrim($hex, '#');
$len = strlen($hex);
if ($len === 3 || $len === 4) {
return new self(
(int) hexdec($hex[0]) * 17,
(int) hexdec($hex[1]) * 17,
(int) hexdec($hex[2]) * 17,
(int) hexdec($hex[3] ?? 'F') * 17 / 255,
);
} elseif ($len === 6 || $len === 8) {
return new self(
(int) hexdec($hex[0] . $hex[1]),
(int) hexdec($hex[2] . $hex[3]),
(int) hexdec($hex[4] . $hex[5]),
(int) hexdec(($hex[6] ?? 'F') . ($hex[7] ?? 'F')) / 255,
);
} else {
throw new Nette\InvalidArgumentException('Invalid hex color format.');
}
}


private function __construct(
public int $red,
public int $green,
public int $blue,
public float $opacity = 1,
) {
$this->red = max(0, min(255, $red));
$this->green = max(0, min(255, $green));
$this->blue = max(0, min(255, $blue));
$this->opacity = max(0, min(1, $opacity));
}


public function toRGBA(): array
{
return [
max(0, min(255, $this->red)),
max(0, min(255, $this->green)),
max(0, min(255, $this->blue)),
max(0, min(127, (int) round(127 - $this->opacity * 127))),
];
}
}
32 changes: 32 additions & 0 deletions tests/Utils/Image.color.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* Test: Nette\Utils\ImageColor
*/

declare(strict_types=1);

use Nette\Utils\ImageColor;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


test('hex()', function () {
Assert::equal(ImageColor::rgb(17, 34, 51, 1.0), ImageColor::hex('#123'));
Assert::equal(ImageColor::rgb(17, 34, 51, 0.8 / 3), ImageColor::hex('#1234'));
Assert::equal(ImageColor::rgb(18, 52, 86), ImageColor::hex('#123456'));
Assert::equal(ImageColor::rgb(18, 52, 86, 120 / 255), ImageColor::hex('#12345678'));

Assert::exception(
fn() => ImageColor::hex('#12'),
InvalidArgumentException::class,
'Invalid hex color format.',
);
});

test('toRGBA()', function () {
Assert::same((ImageColor::rgb(0, 1, 2, 0.3))->toRGBA(), [0, 1, 2, 89]);
Assert::same((ImageColor::rgb(1000, -1, 1000, -10))->toRGBA(), [255, 0, 255, 127]);
});

0 comments on commit 962e3a7

Please sign in to comment.