1
0
mirror of https://github.com/Intervention/image.git synced 2025-09-03 02:42:45 +02:00

Add colorspace transformation

This commit is contained in:
Oliver Vogel
2023-10-22 12:10:16 +02:00
parent f16b56103a
commit 24c8071200
19 changed files with 343 additions and 18 deletions

View File

@@ -9,9 +9,20 @@ class Cyan implements ColorChannelInterface
{ {
protected int $value; protected int $value;
public function __construct(int $value) /**
* {@inheritdoc}
*
* @see ColorChannelInterface::__construct()
*/
public function __construct(int $value = null, float $normalized = null)
{ {
$this->value = $this->validate($value); $this->value = $this->validate(
match (true) {
is_null($value) && is_numeric($normalized) => intval(round($normalized * $this->max())),
is_numeric($value) && is_null($normalized) => $value,
default => throw new ColorException('Color channels must either have a value or a normalized value')
}
);
} }
public function value(): int public function value(): int

View File

@@ -9,6 +9,27 @@ use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface class Colorspace implements ColorspaceInterface
{ {
public static $channels = [
Channels\Cyan::class,
Channels\Magenta::class,
Channels\Yellow::class,
Channels\Key::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::createColor()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
$values = array_map(function ($classname, $value_normalized) {
return (new $classname(normalized: $value_normalized))->value();
}, self::$channels, $normalized);
return new Color(...$values);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* *

View File

@@ -10,13 +10,19 @@ class Red implements ColorChannelInterface
protected int $value; protected int $value;
/** /**
* Create and validate new instance * {@inheritdoc}
* *
* @param int $value * @see ColorChannelInterface::__construct()
*/ */
public function __construct(int $value) public function __construct(int $value = null, float $normalized = null)
{ {
$this->value = $this->validate($value); $this->value = $this->validate(
match (true) {
is_null($value) && is_numeric($normalized) => intval(round($normalized * $this->max())),
is_numeric($value) && is_null($normalized) => $value,
default => throw new ColorException('Color channels must either have a value or a normalized value')
}
);
} }
/** /**

View File

@@ -8,6 +8,27 @@ use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface class Colorspace implements ColorspaceInterface
{ {
public static $channels = [
Channels\Red::class,
Channels\Green::class,
Channels\Blue::class,
Channels\Alpha::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::createColor()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
$values = array_map(function ($classname, $value_normalized) {
return (new $classname(normalized: $value_normalized))->value();
}, self::$channels, $normalized);
return new Color(...$values);
}
public function convertColor(ColorInterface $color): ColorInterface public function convertColor(ColorInterface $color): ColorInterface
{ {
return match (get_class($color)) { return match (get_class($color)) {

View File

@@ -11,6 +11,7 @@ use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Polygon; use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\CollectionInterface; use Intervention\Image\Interfaces\CollectionInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\EncoderInterface; use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface; use Intervention\Image\Interfaces\ModifierInterface;
@@ -25,6 +26,7 @@ abstract class AbstractImage implements ImageInterface
use CanHandleInput; use CanHandleInput;
use CanRunCallback; use CanRunCallback;
protected ColorspaceInterface $colorspace;
protected Collection $exif; protected Collection $exif;
public function eachFrame(callable $callback): ImageInterface public function eachFrame(callable $callback): ImageInterface

View File

@@ -3,9 +3,12 @@
namespace Intervention\Image\Drivers\Gd; namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Collection; use Intervention\Image\Collection;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\Abstract\AbstractImage; use Intervention\Image\Drivers\Abstract\AbstractImage;
use Intervention\Image\Drivers\Gd\Traits\CanHandleColors; use Intervention\Image\Drivers\Gd\Traits\CanHandleColors;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
use IteratorAggregate; use IteratorAggregate;
@@ -79,4 +82,27 @@ class Image extends AbstractImage implements ImageInterface, IteratorAggregate
return null; return null;
} }
public function getColorspace(): ColorspaceInterface
{
return new RgbColorspace();
}
/**
* {@inheritdoc}
*
* @see ImageInterface::setColorspace()
*/
public function setColorspace(string|ColorspaceInterface $target): ImageInterface
{
if (is_string($target) && !in_array($target, ['rgb', RgbColorspace::class])) {
throw new NotSupportedException('Only RGB colorspace is supported with GD driver.');
}
if (is_object($target) && !is_a($target, RgbColorspace::class)) {
throw new NotSupportedException('Only RGB colorspace is supported with GD driver.');
}
return $this;
}
} }

View File

@@ -30,10 +30,6 @@ class JpegEncoder extends AbstractEncoder implements EncoderInterface
$imagick->setCompressionQuality($this->quality); $imagick->setCompressionQuality($this->quality);
$imagick->setImageCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality);
if ($imagick->getImageColorspace() != Imagick::COLORSPACE_SRGB) {
$imagick->transformImageColorspace(Imagick::COLORSPACE_SRGB);
}
return new EncodedImage($imagick->getImagesBlob(), 'image/jpeg'); return new EncodedImage($imagick->getImagesBlob(), 'image/jpeg');
} }
} }

View File

@@ -4,9 +4,13 @@ namespace Intervention\Image\Drivers\Imagick;
use Imagick; use Imagick;
use ImagickException; use ImagickException;
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\Abstract\AbstractImage; use Intervention\Image\Drivers\Abstract\AbstractImage;
use Intervention\Image\Drivers\Imagick\Modifiers\ColorspaceModifier;
use Intervention\Image\Drivers\Imagick\Traits\CanHandleColors; use Intervention\Image\Drivers\Imagick\Traits\CanHandleColors;
use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
use Iterator; use Iterator;
@@ -19,7 +23,14 @@ class Image extends AbstractImage implements ImageInterface, Iterator
public function __construct(protected Imagick $imagick) public function __construct(protected Imagick $imagick)
{ {
// $this->colorspace = match ($imagick->getImageColorspace()) {
Imagick::COLORSPACE_RGB, Imagick::COLORSPACE_SRGB => new RgbColorspace(),
Imagick::COLORSPACE_CMYK => new CmykColorspace(),
default => function () use ($imagick) {
$imagick->transformImageColorspace(Imagick::COLORSPACE_SRGB);
return new RgbColorspace();
}
};
} }
public function getImagick(): Imagick public function getImagick(): Imagick
@@ -128,10 +139,29 @@ class Image extends AbstractImage implements ImageInterface, Iterator
{ {
if ($frame = $this->getFrame($frame_key)) { if ($frame = $this->getFrame($frame_key)) {
return $this->colorFromPixel( return $this->colorFromPixel(
$frame->getCore()->getImagePixelColor($x, $y) $frame->getCore()->getImagePixelColor($x, $y),
$this->colorspace
); );
} }
return null; return null;
} }
public function getColorspace(): ColorspaceInterface
{
return match ($this->imagick->getImageColorspace()) {
Imagick::COLORSPACE_CMYK => new CmykColorspace(),
default => new RgbColorspace(),
};
}
/**
* {@inheritdoc}
*
* @see ImageInterface::setColorspace()
*/
public function setColorspace(string|ColorspaceInterface $colorspace): ImageInterface
{
return $this->modify(new ColorspaceModifier($colorspace));
}
} }

View File

@@ -0,0 +1,66 @@
<?php
namespace Intervention\Image\Drivers\Imagick\Modifiers;
use Imagick;
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\Imagick\Image;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
use Intervention\Image\Traits\CanCheckType;
class ColorspaceModifier implements ModifierInterface
{
use CanCheckType;
protected static $mapping = [
RgbColorspace::class => Imagick::COLORSPACE_SRGB,
CmykColorspace::class => Imagick::COLORSPACE_CMYK,
];
public function __construct(protected string|ColorspaceInterface $target)
{
//
}
public function apply(ImageInterface $image): ImageInterface
{
$colorspace = $this->targetColorspace();
$imagick = $this->failIfNotClass($image, Image::class)->getImagick();
$imagick->transformImageColorspace(
$this->getImagickColorspace($colorspace)
);
return $image;
}
private function getImagickColorspace(ColorspaceInterface $colorspace): int
{
if (!array_key_exists(get_class($colorspace), self::$mapping)) {
throw new NotSupportedException('Given colorspace is not supported.');
}
return self::$mapping[get_class($colorspace)];
}
private function targetColorspace(): ColorspaceInterface
{
if (is_object($this->target)) {
return $this->target;
}
if (in_array($this->target, ['rgb', 'RGB', RgbColorspace::class])) {
return new RgbColorspace();
}
if (in_array($this->target, ['cmyk', 'CMYK', CmykColorspace::class])) {
return new CmykColorspace();
}
throw new NotSupportedException('Given colorspace is not supported.');
}
}

View File

@@ -2,23 +2,37 @@
namespace Intervention\Image\Drivers\Imagick\Traits; namespace Intervention\Image\Drivers\Imagick\Traits;
use Imagick;
use ImagickPixel; use ImagickPixel;
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Traits\CanHandleInput; use Intervention\Image\Interfaces\ColorspaceInterface;
trait CanHandleColors trait CanHandleColors
{ {
use CanHandleInput;
/** /**
* Transforms ImagickPixel to own color object * Transforms ImagickPixel to own color object
* *
* @param ImagickPixel $pixel * @param ImagickPixel $pixel
* @param ColorspaceInterface $colorspace
* @return ColorInterface * @return ColorInterface
*/ */
public function colorFromPixel(ImagickPixel $pixel): ColorInterface public function colorFromPixel(ImagickPixel $pixel, ColorspaceInterface $colorspace): ColorInterface
{ {
return $this->handleInput($pixel->getColorAsString()); return match (get_class($colorspace)) {
CmykColorspace::class => $colorspace->colorFromNormalized([
$pixel->getColorValue(Imagick::COLOR_CYAN),
$pixel->getColorValue(Imagick::COLOR_MAGENTA),
$pixel->getColorValue(Imagick::COLOR_YELLOW),
$pixel->getColorValue(Imagick::COLOR_BLACK),
]),
default => $colorspace->colorFromNormalized([
$pixel->getColorValue(Imagick::COLOR_RED),
$pixel->getColorValue(Imagick::COLOR_GREEN),
$pixel->getColorValue(Imagick::COLOR_BLUE),
$pixel->getColorValue(Imagick::COLOR_ALPHA),
]),
};
} }
/** /**

View File

@@ -4,6 +4,14 @@ namespace Intervention\Image\Interfaces;
interface ColorChannelInterface interface ColorChannelInterface
{ {
/**
* Create new instance by either value or normalized value
*
* @param int|null $value
* @param float|null $normalized
*/
public function __construct(int $value = null, float $normalized = null);
/** /**
* Return color channels integer value * Return color channels integer value
* *

View File

@@ -11,4 +11,12 @@ interface ColorspaceInterface
* @return ColorInterface * @return ColorInterface
*/ */
public function convertColor(ColorInterface $color): ColorInterface; public function convertColor(ColorInterface $color): ColorInterface;
/**
* Create new color in colorspace from given normalized channel values
*
* @param array $normalized
* @return ColorInterface
*/
public function colorFromNormalized(array $normalized): ColorInterface;
} }

View File

@@ -140,6 +140,21 @@ interface ImageInterface extends Traversable, Countable
public function pickColor(int $x, int $y, int $frame_key = 0): ?ColorInterface; public function pickColor(int $x, int $y, int $frame_key = 0): ?ColorInterface;
public function pickColors(int $x, int $y): CollectionInterface; public function pickColors(int $x, int $y): CollectionInterface;
/**
* Get the colorspace of the image
*
* @return ColorspaceInterface
*/
public function getColorspace(): ColorspaceInterface;
/**
* Transform image to given colorspace
*
* @param string|ColorspaceInterface $target
* @return ImageInterface
*/
public function setColorspace(string|ColorspaceInterface $target): ImageInterface;
/** /**
* Draw text on image * Draw text on image
* *

View File

@@ -19,6 +19,18 @@ class ChannelTest extends TestCase
{ {
$channel = new Channel(0); $channel = new Channel(0);
$this->assertInstanceOf(Channel::class, $channel); $this->assertInstanceOf(Channel::class, $channel);
$channel = new Channel(value: 0);
$this->assertInstanceOf(Channel::class, $channel);
$channel = new Channel(normalized: 0);
$this->assertInstanceOf(Channel::class, $channel);
$this->expectException(ColorException::class);
$channel = new Channel();
$this->expectException(ColorException::class);
$channel = new Channel(normalized: 2);
} }
public function testValue(): void public function testValue(): void

View File

@@ -18,6 +18,18 @@ class ChannelTest extends TestCase
{ {
$channel = new Channel(0); $channel = new Channel(0);
$this->assertInstanceOf(Channel::class, $channel); $this->assertInstanceOf(Channel::class, $channel);
$channel = new Channel(value: 0);
$this->assertInstanceOf(Channel::class, $channel);
$channel = new Channel(normalized: 0);
$this->assertInstanceOf(Channel::class, $channel);
$this->expectException(ColorException::class);
$channel = new Channel();
$this->expectException(ColorException::class);
$channel = new Channel(normalized: 2);
} }
public function testValue(): void public function testValue(): void

View File

@@ -8,6 +8,9 @@ use Intervention\Image\Drivers\Gd\Image;
use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Tests\TestCase; use Intervention\Image\Tests\TestCase;
use Intervention\Image\Colors\Rgb\Color; use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
use Intervention\Image\Exceptions\NotSupportedException;
/** /**
* @requires extension gd * @requires extension gd
@@ -119,4 +122,33 @@ class ImageTest extends TestCase
$this->assertEquals([0, 255, 0, 255], $colors->get(1)->toArray()); $this->assertEquals([0, 255, 0, 255], $colors->get(1)->toArray());
$this->assertEquals([0, 0, 255, 255], $colors->get(2)->toArray()); $this->assertEquals([0, 0, 255, 255], $colors->get(2)->toArray());
} }
public function testGetColorspace(): void
{
$this->assertInstanceOf(RgbColorspace::class, $this->image->getColorspace());
}
public function testSetColorspace(): void
{
$result = $this->image->setColorspace('rgb');
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(RgbColorspace::class, $result->getColorspace());
$result = $this->image->setColorspace(RgbColorspace::class);
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(RgbColorspace::class, $result->getColorspace());
$result = $this->image->setColorspace(new RgbColorspace());
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(RgbColorspace::class, $result->getColorspace());
$this->expectException(NotSupportedException::class);
$this->image->setColorspace('cmyk');
$this->expectException(NotSupportedException::class);
$this->image->setColorspace(CmykColorspace::class);
$this->expectException(NotSupportedException::class);
$this->image->setColorspace(new CmykColorspace());
}
} }

View File

@@ -2,6 +2,8 @@
namespace Intervention\Image\Tests\Drivers\Imagick\Decoders; namespace Intervention\Image\Tests\Drivers\Imagick\Decoders;
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\Imagick\Decoders\BinaryImageDecoder; use Intervention\Image\Drivers\Imagick\Decoders\BinaryImageDecoder;
use Intervention\Image\Drivers\Imagick\Image; use Intervention\Image\Drivers\Imagick\Image;
use Intervention\Image\Tests\TestCase; use Intervention\Image\Tests\TestCase;
@@ -13,6 +15,7 @@ class BinaryImageDecoderTest extends TestCase
$decoder = new BinaryImageDecoder(); $decoder = new BinaryImageDecoder();
$image = $decoder->decode(file_get_contents($this->getTestImagePath('tile.png'))); $image = $decoder->decode(file_get_contents($this->getTestImagePath('tile.png')));
$this->assertInstanceOf(Image::class, $image); $this->assertInstanceOf(Image::class, $image);
$this->assertInstanceOf(RgbColorspace::class, $image->getColorspace());
$this->assertEquals(16, $image->getWidth()); $this->assertEquals(16, $image->getWidth());
$this->assertEquals(16, $image->getHeight()); $this->assertEquals(16, $image->getHeight());
$this->assertCount(1, $image); $this->assertCount(1, $image);
@@ -48,4 +51,12 @@ class BinaryImageDecoderTest extends TestCase
$this->assertCount(1, $image); $this->assertCount(1, $image);
$this->assertEquals('Oliver Vogel', $image->getExif('IFD0.Artist')); $this->assertEquals('Oliver Vogel', $image->getExif('IFD0.Artist'));
} }
public function testDecodeCmykImage(): void
{
$decoder = new BinaryImageDecoder();
$image = $decoder->decode(file_get_contents($this->getTestImagePath('cmyk.jpg')));
$this->assertInstanceOf(Image::class, $image);
$this->assertInstanceOf(CmykColorspace::class, $image->getColorspace());
}
} }

View File

@@ -4,6 +4,8 @@ namespace Intervention\Image\Tests\Drivers\Imagick;
use Imagick; use Imagick;
use ImagickPixel; use ImagickPixel;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
use Intervention\Image\Drivers\Imagick\Frame; use Intervention\Image\Drivers\Imagick\Frame;
use Intervention\Image\Drivers\Imagick\Image; use Intervention\Image\Drivers\Imagick\Image;
use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Geometry\Rectangle;
@@ -87,4 +89,36 @@ class ImageTest extends TestCase
{ {
$this->assertInstanceOf(Rectangle::class, $this->image->getSize()); $this->assertInstanceOf(Rectangle::class, $this->image->getSize());
} }
public function testGetColorspace(): void
{
$this->assertInstanceOf(RgbColorspace::class, $this->image->getColorspace());
}
public function testSetColorspace(): void
{
$result = $this->image->setColorspace('rgb');
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(RgbColorspace::class, $result->getColorspace());
$result = $this->image->setColorspace(RgbColorspace::class);
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(RgbColorspace::class, $result->getColorspace());
$result = $this->image->setColorspace(new RgbColorspace());
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(RgbColorspace::class, $result->getColorspace());
$result = $this->image->setColorspace('cmyk');
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(CmykColorspace::class, $result->getColorspace());
$result = $this->image->setColorspace(CmykColorspace::class);
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(CmykColorspace::class, $result->getColorspace());
$result = $this->image->setColorspace(new CmykColorspace());
$this->assertInstanceOf(Image::class, $result);
$this->assertInstanceOf(CmykColorspace::class, $result->getColorspace());
}
} }

BIN
tests/images/cmyk.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB