From e72b0ff6df6e596c41eb3f71f31a85f13c904082 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Mon, 9 Oct 2023 16:30:03 +0200 Subject: [PATCH] Refactor color management --- src/Colors/Cmyk/Channels/Cyan.php | 45 +++++++++ src/Colors/Cmyk/Channels/Key.php | 8 ++ src/Colors/Cmyk/Channels/Magenta.php | 8 ++ src/Colors/Cmyk/Channels/Yellow.php | 8 ++ src/Colors/Cmyk/Color.php | 97 +++++++++++++++++++ src/Colors/Cmyk/Colorspace.php | 33 +++++++ src/Colors/Rgb/Channels/Blue.php | 8 ++ src/Colors/Rgb/Channels/Green.php | 8 ++ src/Colors/Rgb/Channels/Red.php | 45 +++++++++ src/Colors/Rgb/Color.php | 115 +++++++++++++++++++++++ src/Colors/Rgb/Colorspace.php | 27 ++++++ src/Exceptions/ColorException.php | 8 ++ src/Interfaces/ColorChannelInterface.php | 12 +++ src/Interfaces/ColorInterface.php | 20 ++-- src/Interfaces/ColorspaceInterface.php | 8 ++ tests/Colors/Rgb/ColorTest.php | 106 +++++++++++++++++++++ 16 files changed, 548 insertions(+), 8 deletions(-) create mode 100644 src/Colors/Cmyk/Channels/Cyan.php create mode 100644 src/Colors/Cmyk/Channels/Key.php create mode 100644 src/Colors/Cmyk/Channels/Magenta.php create mode 100644 src/Colors/Cmyk/Channels/Yellow.php create mode 100644 src/Colors/Cmyk/Color.php create mode 100644 src/Colors/Cmyk/Colorspace.php create mode 100644 src/Colors/Rgb/Channels/Blue.php create mode 100644 src/Colors/Rgb/Channels/Green.php create mode 100644 src/Colors/Rgb/Channels/Red.php create mode 100644 src/Colors/Rgb/Color.php create mode 100644 src/Colors/Rgb/Colorspace.php create mode 100644 src/Exceptions/ColorException.php create mode 100644 src/Interfaces/ColorChannelInterface.php create mode 100644 src/Interfaces/ColorspaceInterface.php create mode 100644 tests/Colors/Rgb/ColorTest.php diff --git a/src/Colors/Cmyk/Channels/Cyan.php b/src/Colors/Cmyk/Channels/Cyan.php new file mode 100644 index 00000000..cb40daac --- /dev/null +++ b/src/Colors/Cmyk/Channels/Cyan.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/src/Colors/Cmyk/Channels/Key.php b/src/Colors/Cmyk/Channels/Key.php new file mode 100644 index 00000000..2893d2af --- /dev/null +++ b/src/Colors/Cmyk/Channels/Key.php @@ -0,0 +1,8 @@ +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() + ); + } +} diff --git a/src/Colors/Cmyk/Colorspace.php b/src/Colors/Cmyk/Colorspace.php new file mode 100644 index 00000000..968bd023 --- /dev/null +++ b/src/Colors/Cmyk/Colorspace.php @@ -0,0 +1,33 @@ + $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); + } +} diff --git a/src/Colors/Rgb/Channels/Blue.php b/src/Colors/Rgb/Channels/Blue.php new file mode 100644 index 00000000..b83be821 --- /dev/null +++ b/src/Colors/Rgb/Channels/Blue.php @@ -0,0 +1,8 @@ +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; + } +} diff --git a/src/Colors/Rgb/Color.php b/src/Colors/Rgb/Color.php new file mode 100644 index 00000000..18be0fc2 --- /dev/null +++ b/src/Colors/Rgb/Color.php @@ -0,0 +1,115 @@ +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() + ); + } +} diff --git a/src/Colors/Rgb/Colorspace.php b/src/Colors/Rgb/Colorspace.php new file mode 100644 index 00000000..fd28b1e9 --- /dev/null +++ b/src/Colors/Rgb/Colorspace.php @@ -0,0 +1,27 @@ + $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())), + ); + } +} diff --git a/src/Exceptions/ColorException.php b/src/Exceptions/ColorException.php new file mode 100644 index 00000000..4eda9322 --- /dev/null +++ b/src/Exceptions/ColorException.php @@ -0,0 +1,8 @@ +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()); + } +}