1
0
mirror of https://github.com/Intervention/image.git synced 2025-08-26 15:24:37 +02:00

Refactor color management

This commit is contained in:
Oliver Vogel
2023-10-09 16:30:03 +02:00
parent d71448e6bf
commit e72b0ff6df
16 changed files with 548 additions and 8 deletions

View 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;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Channels;
class Key extends Cyan
{
//
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Channels;
class Magenta extends Cyan
{
//
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Channels;
class Yellow extends Cyan
{
//
}

97
src/Colors/Cmyk/Color.php Normal file
View 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()
);
}
}

View 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);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Rgb\Channels;
class Blue extends Red
{
//
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Rgb\Channels;
class Green extends Red
{
//
}

View 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
View 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()
);
}
}

View 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())),
);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Exceptions;
class ColorException extends \RuntimeException
{
//
}

View 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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Interfaces;
interface ColorspaceInterface
{
public function transformColor(ColorInterface $color): ColorInterface;
}

View 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());
}
}