mirror of
https://github.com/Intervention/image.git
synced 2025-08-26 07:14:31 +02:00
Refactor color management
This commit is contained in:
45
src/Colors/Cmyk/Channels/Cyan.php
Normal file
45
src/Colors/Cmyk/Channels/Cyan.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Cmyk\Channels;
|
||||
|
||||
use Intervention\Image\Exceptions\ColorException;
|
||||
use Intervention\Image\Interfaces\ColorChannelInterface;
|
||||
|
||||
class Cyan implements ColorChannelInterface
|
||||
{
|
||||
protected int $value;
|
||||
|
||||
public function __construct(int $value)
|
||||
{
|
||||
$this->value = $this->validate($value);
|
||||
}
|
||||
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function normalize($precision = 32): float
|
||||
{
|
||||
return round($this->value() / $this->max(), $precision);
|
||||
}
|
||||
|
||||
public function min(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function max(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function validate(mixed $value): mixed
|
||||
{
|
||||
if ($value < $this->min() || $value > $this->max()) {
|
||||
throw new ColorException('CMYK color values must be in range 0-100.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
8
src/Colors/Cmyk/Channels/Key.php
Normal file
8
src/Colors/Cmyk/Channels/Key.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Cmyk\Channels;
|
||||
|
||||
class Key extends Cyan
|
||||
{
|
||||
//
|
||||
}
|
8
src/Colors/Cmyk/Channels/Magenta.php
Normal file
8
src/Colors/Cmyk/Channels/Magenta.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Cmyk\Channels;
|
||||
|
||||
class Magenta extends Cyan
|
||||
{
|
||||
//
|
||||
}
|
8
src/Colors/Cmyk/Channels/Yellow.php
Normal file
8
src/Colors/Cmyk/Channels/Yellow.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Cmyk\Channels;
|
||||
|
||||
class Yellow extends Cyan
|
||||
{
|
||||
//
|
||||
}
|
97
src/Colors/Cmyk/Color.php
Normal file
97
src/Colors/Cmyk/Color.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Cmyk;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Cyan;
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Magenta;
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Yellow;
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Key;
|
||||
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
|
||||
class Color implements ColorInterface
|
||||
{
|
||||
protected array $channels;
|
||||
|
||||
protected Cyan $cyan;
|
||||
protected Magenta $magenta;
|
||||
protected Yellow $yellow;
|
||||
protected Key $key;
|
||||
|
||||
public function __construct(int $c, int $m, int $y, int $k)
|
||||
{
|
||||
$this->cyan = new Cyan($c);
|
||||
$this->magenta = new Magenta($m);
|
||||
$this->yellow = new Yellow($y);
|
||||
$this->key = new Key($k);
|
||||
}
|
||||
|
||||
public function channels(): array
|
||||
{
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
public function cyan(): Cyan
|
||||
{
|
||||
return $this->cyan;
|
||||
}
|
||||
|
||||
public function magenta(): Magenta
|
||||
{
|
||||
return $this->magenta;
|
||||
}
|
||||
|
||||
public function yellow(): Yellow
|
||||
{
|
||||
return $this->yellow;
|
||||
}
|
||||
|
||||
public function key(): Key
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
$this->cyan()->value(),
|
||||
$this->magenta()->value(),
|
||||
$this->yellow()->value(),
|
||||
$this->key()->value(),
|
||||
];
|
||||
}
|
||||
|
||||
public function transformTo(string|ColorspaceInterface $colorspace): ColorInterface
|
||||
{
|
||||
$colorspace = match (true) {
|
||||
is_object($colorspace) => $colorspace,
|
||||
default => new $colorspace(),
|
||||
};
|
||||
|
||||
return $colorspace->transformColor($this);
|
||||
}
|
||||
|
||||
public function toRgb(): RgbColor
|
||||
{
|
||||
return $this->transformTo(RgbColorspace::class);
|
||||
}
|
||||
|
||||
public function toCmyk(): self
|
||||
{
|
||||
return $this->transformTo(CmykColorspace::class);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'cmyk(%d, %d, %d, %d)',
|
||||
$this->cyan()->value(),
|
||||
$this->magenta()->value(),
|
||||
$this->yellow()->value(),
|
||||
$this->key()->value()
|
||||
);
|
||||
}
|
||||
}
|
33
src/Colors/Cmyk/Colorspace.php
Normal file
33
src/Colors/Cmyk/Colorspace.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Cmyk;
|
||||
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
|
||||
class Colorspace implements ColorspaceInterface
|
||||
{
|
||||
public function transformColor(ColorInterface $color): ColorInterface
|
||||
{
|
||||
return match (get_class($color)) {
|
||||
RgbColor::class => $this->convertRgbColor($color),
|
||||
default => $color,
|
||||
};
|
||||
}
|
||||
|
||||
protected function convertRgbColor(RgbColor $color): CmykColor
|
||||
{
|
||||
$c = (255 - $color->red()->value()) / 255.0 * 100;
|
||||
$m = (255 - $color->green()->value()) / 255.0 * 100;
|
||||
$y = (255 - $color->blue()->value()) / 255.0 * 100;
|
||||
$k = intval(round(min([$c, $m, $y])));
|
||||
|
||||
$c = intval(round($c - $k));
|
||||
$m = intval(round($m - $k));
|
||||
$y = intval(round($y - $k));
|
||||
|
||||
return new CmykColor($c, $m, $y, $k);
|
||||
}
|
||||
}
|
8
src/Colors/Rgb/Channels/Blue.php
Normal file
8
src/Colors/Rgb/Channels/Blue.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Rgb\Channels;
|
||||
|
||||
class Blue extends Red
|
||||
{
|
||||
//
|
||||
}
|
8
src/Colors/Rgb/Channels/Green.php
Normal file
8
src/Colors/Rgb/Channels/Green.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Rgb\Channels;
|
||||
|
||||
class Green extends Red
|
||||
{
|
||||
//
|
||||
}
|
45
src/Colors/Rgb/Channels/Red.php
Normal file
45
src/Colors/Rgb/Channels/Red.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Rgb\Channels;
|
||||
|
||||
use Intervention\Image\Exceptions\ColorException;
|
||||
use Intervention\Image\Interfaces\ColorChannelInterface;
|
||||
|
||||
class Red implements ColorChannelInterface
|
||||
{
|
||||
protected int $value;
|
||||
|
||||
public function __construct(int $value)
|
||||
{
|
||||
$this->value = $this->validate($value);
|
||||
}
|
||||
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function normalize($precision = 32): float
|
||||
{
|
||||
return round($this->value() / $this->max(), $precision);
|
||||
}
|
||||
|
||||
public function min(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function max(): int
|
||||
{
|
||||
return 255;
|
||||
}
|
||||
|
||||
public function validate(mixed $value): mixed
|
||||
{
|
||||
if ($value < $this->min() || $value > $this->max()) {
|
||||
throw new ColorException('RGB color values must be in range 0-255.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
115
src/Colors/Rgb/Color.php
Normal file
115
src/Colors/Rgb/Color.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Rgb;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Blue;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Green;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Red;
|
||||
use Intervention\Image\Interfaces\ColorChannelInterface;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
|
||||
class Color implements ColorInterface
|
||||
{
|
||||
protected array $channels;
|
||||
|
||||
public function __construct(int $r, int $g, int $b)
|
||||
{
|
||||
$this->channels = [
|
||||
new Red($r),
|
||||
new Green($g),
|
||||
new Blue($b),
|
||||
];
|
||||
}
|
||||
|
||||
public function channels(): array
|
||||
{
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
public function channel(string $classname): ColorChannelInterface
|
||||
{
|
||||
$channels = array_filter($this->channels(), function (ColorChannelInterface $channel) use ($classname) {
|
||||
return is_a($channel, $classname);
|
||||
});
|
||||
|
||||
return reset($channels);
|
||||
}
|
||||
|
||||
public function red(): Red
|
||||
{
|
||||
return $this->channel(Red::class);
|
||||
}
|
||||
|
||||
public function green(): Green
|
||||
{
|
||||
return $this->channel(Green::class);
|
||||
}
|
||||
|
||||
public function blue(): Blue
|
||||
{
|
||||
return $this->channel(Blue::class);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_map(function (ColorChannelInterface $channel) {
|
||||
return $channel->value();
|
||||
}, $this->channels());
|
||||
}
|
||||
|
||||
public function normalize(): array
|
||||
{
|
||||
return array_map(function (ColorChannelInterface $channel) {
|
||||
return $channel->normalize();
|
||||
}, $this->channels());
|
||||
}
|
||||
|
||||
public function toHex(string $prefix = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%02x%02x%02x',
|
||||
$prefix,
|
||||
$this->red()->value(),
|
||||
$this->green()->value(),
|
||||
$this->blue()->value()
|
||||
);
|
||||
}
|
||||
|
||||
public function toRgb(): Color
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toInt(): int
|
||||
{
|
||||
return $this->red()->value() * 256 * 256 + $this->green()->value() * 256 + $this->blue()->value();
|
||||
}
|
||||
|
||||
public function transformTo(string|ColorspaceInterface $colorspace): ColorInterface
|
||||
{
|
||||
$colorspace = match (true) {
|
||||
is_object($colorspace) => $colorspace,
|
||||
default => new $colorspace(),
|
||||
};
|
||||
|
||||
return $colorspace->transformColor($this);
|
||||
}
|
||||
|
||||
public function toCmyk(): CmykColor
|
||||
{
|
||||
return $this->transformTo(CmykColorspace::class);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'rgb(%d, %d, %d)',
|
||||
$this->red()->value(),
|
||||
$this->green()->value(),
|
||||
$this->blue()->value()
|
||||
);
|
||||
}
|
||||
}
|
27
src/Colors/Rgb/Colorspace.php
Normal file
27
src/Colors/Rgb/Colorspace.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Rgb;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
|
||||
class Colorspace implements ColorspaceInterface
|
||||
{
|
||||
public function transformColor(ColorInterface $color): ColorInterface
|
||||
{
|
||||
return match ($color) {
|
||||
CmykColor::class => $this->convertCmykColor($color),
|
||||
default => $color,
|
||||
};
|
||||
}
|
||||
|
||||
protected function convertCmykColor(CmykColor $color): Color
|
||||
{
|
||||
return new Color(
|
||||
(int) (255 * (1 - $color->cyan()->value()) * (1 - $color->key()->value())),
|
||||
(int) (255 * (1 - $color->magenta()->value()) * (1 - $color->key()->value())),
|
||||
(int) (255 * (1 - $color->yellow()->value()) * (1 - $color->key()->value())),
|
||||
);
|
||||
}
|
||||
}
|
8
src/Exceptions/ColorException.php
Normal file
8
src/Exceptions/ColorException.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Exceptions;
|
||||
|
||||
class ColorException extends \RuntimeException
|
||||
{
|
||||
//
|
||||
}
|
12
src/Interfaces/ColorChannelInterface.php
Normal file
12
src/Interfaces/ColorChannelInterface.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Interfaces;
|
||||
|
||||
interface ColorChannelInterface
|
||||
{
|
||||
public function value(): int;
|
||||
public function normalize(int $precision = 32): float;
|
||||
public function validate(mixed $value): mixed;
|
||||
public function min(): int;
|
||||
public function max(): int;
|
||||
}
|
@@ -2,14 +2,18 @@
|
||||
|
||||
namespace Intervention\Image\Interfaces;
|
||||
|
||||
use Intervention\Image\Colors\CmykColor;
|
||||
use Intervention\Image\Colors\RgbColor;
|
||||
|
||||
interface ColorInterface
|
||||
{
|
||||
public function red(): int;
|
||||
public function green(): int;
|
||||
public function blue(): int;
|
||||
public function alpha(): float;
|
||||
public function toArray(): array;
|
||||
public function toHex(string $prefix = ''): string;
|
||||
public function toInt(): int;
|
||||
public function isGreyscale(): bool;
|
||||
// public function channels(): array;
|
||||
// public function channel(string $classname): ColorChannelInterface;
|
||||
// public function colorspace(): ColorspaceInterface;
|
||||
// public function toRgb(): RgbColor;
|
||||
// public function toRgba(): RgbColor;
|
||||
// public function toCmyk(): CmykColor;
|
||||
// public function toArray(): array;
|
||||
// public function transformTo(string|ColorspaceInterface $colorspace): ColorInterface;
|
||||
// public function __toString(): string;
|
||||
}
|
||||
|
8
src/Interfaces/ColorspaceInterface.php
Normal file
8
src/Interfaces/ColorspaceInterface.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Interfaces;
|
||||
|
||||
interface ColorspaceInterface
|
||||
{
|
||||
public function transformColor(ColorInterface $color): ColorInterface;
|
||||
}
|
106
tests/Colors/Rgb/ColorTest.php
Normal file
106
tests/Colors/Rgb/ColorTest.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Tests\Colors\Rgb;
|
||||
|
||||
use Intervention\Image\Colors\Rgb\Channels\Red;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Green;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Blue;
|
||||
use Intervention\Image\Colors\Rgb\Color as Color;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @requires extension gd
|
||||
* @covers \Intervention\Image\Colors\Rgb\Color
|
||||
*/
|
||||
class ColorTest extends TestCase
|
||||
{
|
||||
public function testConstructor(): void
|
||||
{
|
||||
$color = new Color(0, 0, 0);
|
||||
$this->assertInstanceOf(Color::class, $color);
|
||||
}
|
||||
|
||||
public function testChannels(): void
|
||||
{
|
||||
$color = new Color(10, 20, 30);
|
||||
$this->assertIsArray($color->channels());
|
||||
$this->assertCount(3, $color->channels());
|
||||
}
|
||||
|
||||
public function testChannel(): void
|
||||
{
|
||||
$color = new Color(10, 20, 30);
|
||||
$channel = $color->channel(Red::class);
|
||||
$this->assertInstanceOf(Red::class, $channel);
|
||||
$this->assertEquals(10, $channel->value());
|
||||
}
|
||||
|
||||
public function testRedGreenBlue(): void
|
||||
{
|
||||
$color = new Color(10, 20, 30);
|
||||
$this->assertInstanceOf(Red::class, $color->red());
|
||||
$this->assertInstanceOf(Green::class, $color->green());
|
||||
$this->assertInstanceOf(Blue::class, $color->blue());
|
||||
$this->assertEquals(10, $color->red()->value());
|
||||
$this->assertEquals(20, $color->green()->value());
|
||||
$this->assertEquals(30, $color->blue()->value());
|
||||
}
|
||||
|
||||
public function testToArray(): void
|
||||
{
|
||||
$color = new Color(10, 20, 30);
|
||||
$this->assertEquals([10, 20, 30], $color->toArray());
|
||||
}
|
||||
|
||||
public function testToHex(): void
|
||||
{
|
||||
$color = new Color(181, 55, 23);
|
||||
$this->assertEquals('b53717', $color->toHex());
|
||||
$this->assertEquals('#b53717', $color->toHex('#'));
|
||||
}
|
||||
|
||||
public function testNormalize(): void
|
||||
{
|
||||
$color = new Color(255, 0, 51);
|
||||
$this->assertEquals([1.0, 0.0, 0.2], $color->normalize());
|
||||
}
|
||||
|
||||
public function testToInt(): void
|
||||
{
|
||||
$color = new Color(0, 0, 0);
|
||||
$this->assertEquals(0, $color->toInt());
|
||||
|
||||
$color = new Color(255, 255, 255);
|
||||
$this->assertEquals(16777215, $color->toInt());
|
||||
|
||||
$color = new Color(181, 55, 23);
|
||||
$this->assertEquals(11876119, $color->toInt());
|
||||
}
|
||||
|
||||
public function testToString(): void
|
||||
{
|
||||
$color = new Color(181, 55, 23);
|
||||
$this->assertEquals('rgb(181, 55, 23)', (string) $color);
|
||||
}
|
||||
|
||||
public function testToCmyk(): void
|
||||
{
|
||||
$color = new Color(0, 0, 0);
|
||||
$this->assertEquals([0, 0, 0, 100], $color->toCmyk()->toArray());
|
||||
|
||||
$color = new Color(255, 255, 255);
|
||||
$this->assertEquals([0, 0, 0, 0], $color->toCmyk()->toArray());
|
||||
|
||||
$color = new Color(255, 0, 0);
|
||||
$this->assertEquals([0, 100, 100, 0], $color->toCmyk()->toArray());
|
||||
|
||||
$color = new Color(255, 0, 255);
|
||||
$this->assertEquals([0, 100, 0, 0], $color->toCmyk()->toArray());
|
||||
|
||||
$color = new Color(255, 255, 0);
|
||||
$this->assertEquals([0, 0, 100, 0], $color->toCmyk()->toArray());
|
||||
|
||||
$color = new Color(255, 204, 204);
|
||||
$this->assertEquals([0, 20, 20, 0], $color->toCmyk()->toArray());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user