1
0
mirror of https://github.com/Intervention/image.git synced 2025-09-02 10:23:29 +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;
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

View File

@@ -9,6 +9,27 @@ use Intervention\Image\Interfaces\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}
*

View File

@@ -10,13 +10,19 @@ class Red implements ColorChannelInterface
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
{
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
{
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\Rectangle;
use Intervention\Image\Interfaces\CollectionInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
@@ -25,6 +26,7 @@ abstract class AbstractImage implements ImageInterface
use CanHandleInput;
use CanRunCallback;
protected ColorspaceInterface $colorspace;
protected Collection $exif;
public function eachFrame(callable $callback): ImageInterface

View File

@@ -3,9 +3,12 @@
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Collection;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\Abstract\AbstractImage;
use Intervention\Image\Drivers\Gd\Traits\CanHandleColors;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use IteratorAggregate;
@@ -79,4 +82,27 @@ class Image extends AbstractImage implements ImageInterface, IteratorAggregate
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->setImageCompressionQuality($this->quality);
if ($imagick->getImageColorspace() != Imagick::COLORSPACE_SRGB) {
$imagick->transformImageColorspace(Imagick::COLORSPACE_SRGB);
}
return new EncodedImage($imagick->getImagesBlob(), 'image/jpeg');
}
}

View File

@@ -4,9 +4,13 @@ namespace Intervention\Image\Drivers\Imagick;
use Imagick;
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\Imagick\Modifiers\ColorspaceModifier;
use Intervention\Image\Drivers\Imagick\Traits\CanHandleColors;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Iterator;
@@ -19,7 +23,14 @@ class Image extends AbstractImage implements ImageInterface, Iterator
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
@@ -128,10 +139,29 @@ class Image extends AbstractImage implements ImageInterface, Iterator
{
if ($frame = $this->getFrame($frame_key)) {
return $this->colorFromPixel(
$frame->getCore()->getImagePixelColor($x, $y)
$frame->getCore()->getImagePixelColor($x, $y),
$this->colorspace
);
}
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;
use Imagick;
use ImagickPixel;
use Intervention\Image\Colors\Cmyk\Colorspace as CmykColorspace;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Traits\CanHandleInput;
use Intervention\Image\Interfaces\ColorspaceInterface;
trait CanHandleColors
{
use CanHandleInput;
/**
* Transforms ImagickPixel to own color object
*
* @param ImagickPixel $pixel
* @param ImagickPixel $pixel
* @param ColorspaceInterface $colorspace
* @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
{
/**
* 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
*

View File

@@ -11,4 +11,12 @@ interface ColorspaceInterface
* @return 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 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
*

View File

@@ -19,6 +19,18 @@ class ChannelTest extends TestCase
{
$channel = new Channel(0);
$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

View File

@@ -18,6 +18,18 @@ class ChannelTest extends TestCase
{
$channel = new Channel(0);
$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

View File

@@ -8,6 +8,9 @@ use Intervention\Image\Drivers\Gd\Image;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Tests\TestCase;
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
@@ -119,4 +122,33 @@ class ImageTest extends TestCase
$this->assertEquals([0, 255, 0, 255], $colors->get(1)->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;
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\Image;
use Intervention\Image\Tests\TestCase;
@@ -13,6 +15,7 @@ class BinaryImageDecoderTest extends TestCase
$decoder = new BinaryImageDecoder();
$image = $decoder->decode(file_get_contents($this->getTestImagePath('tile.png')));
$this->assertInstanceOf(Image::class, $image);
$this->assertInstanceOf(RgbColorspace::class, $image->getColorspace());
$this->assertEquals(16, $image->getWidth());
$this->assertEquals(16, $image->getHeight());
$this->assertCount(1, $image);
@@ -48,4 +51,12 @@ class BinaryImageDecoderTest extends TestCase
$this->assertCount(1, $image);
$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 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\Image;
use Intervention\Image\Geometry\Rectangle;
@@ -87,4 +89,36 @@ class ImageTest extends TestCase
{
$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