mirror of
https://github.com/Intervention/image.git
synced 2025-08-29 16:50:07 +02:00
Implement HSV color space
This commit is contained in:
91
src/Colors/AbstractColorChannel.php
Normal file
91
src/Colors/AbstractColorChannel.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors;
|
||||
|
||||
use Intervention\Image\Exceptions\ColorException;
|
||||
use Intervention\Image\Interfaces\ColorChannelInterface;
|
||||
|
||||
abstract class AbstractColorChannel implements ColorChannelInterface
|
||||
{
|
||||
protected int $value;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::__construct()
|
||||
*/
|
||||
public function __construct(int $value = null, float $normalized = null)
|
||||
{
|
||||
$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')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of value()
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function toInt(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::value()
|
||||
*/
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::normalize()
|
||||
*/
|
||||
public function normalize($precision = 32): float
|
||||
{
|
||||
return round($this->value() / $this->max(), $precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::validate()
|
||||
*/
|
||||
public function validate(mixed $value): mixed
|
||||
{
|
||||
if ($value < $this->min() || $value > $this->max()) {
|
||||
throw new ColorException('Color channel value must be in range ' . $this->min() . ' to ' . $this->max());
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::toString()
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return (string) $this->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::__toString()
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
@@ -2,39 +2,10 @@
|
||||
|
||||
namespace Intervention\Image\Colors\Cmyk\Channels;
|
||||
|
||||
use Intervention\Image\Exceptions\ColorException;
|
||||
use Intervention\Image\Interfaces\ColorChannelInterface;
|
||||
use Intervention\Image\Colors\AbstractColorChannel;
|
||||
|
||||
class Cyan implements ColorChannelInterface
|
||||
class Cyan extends AbstractColorChannel
|
||||
{
|
||||
protected int $value;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::__construct()
|
||||
*/
|
||||
public function __construct(int $value = null, float $normalized = null)
|
||||
{
|
||||
$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
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function normalize($precision = 32): float
|
||||
{
|
||||
return round($this->value() / $this->max(), $precision);
|
||||
}
|
||||
|
||||
public function min(): int
|
||||
{
|
||||
return 0;
|
||||
@@ -44,23 +15,4 @@ class Cyan implements ColorChannelInterface
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return (string) $this->value();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ namespace Intervention\Image\Colors\Cmyk;
|
||||
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Colors\Hsv\Color as HsvColor;
|
||||
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
|
||||
@@ -39,6 +41,7 @@ class Colorspace implements ColorspaceInterface
|
||||
{
|
||||
return match (get_class($color)) {
|
||||
RgbColor::class => $this->convertRgbColor($color),
|
||||
HsvColor::class => $this->convertRgbColor($color->convertTo(RgbColorspace::class)),
|
||||
default => $color,
|
||||
};
|
||||
}
|
||||
|
28
src/Colors/Hsv/Channels/Hue.php
Normal file
28
src/Colors/Hsv/Channels/Hue.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Hsv\Channels;
|
||||
|
||||
use Intervention\Image\Colors\AbstractColorChannel;
|
||||
|
||||
class Hue extends AbstractColorChannel
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::min()
|
||||
*/
|
||||
public function min(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::max()
|
||||
*/
|
||||
public function max(): int
|
||||
{
|
||||
return 360;
|
||||
}
|
||||
}
|
28
src/Colors/Hsv/Channels/Saturation.php
Normal file
28
src/Colors/Hsv/Channels/Saturation.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Hsv\Channels;
|
||||
|
||||
use Intervention\Image\Colors\AbstractColorChannel;
|
||||
|
||||
class Saturation extends AbstractColorChannel
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::min()
|
||||
*/
|
||||
public function min(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::max()
|
||||
*/
|
||||
public function max(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
}
|
28
src/Colors/Hsv/Channels/Value.php
Normal file
28
src/Colors/Hsv/Channels/Value.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Hsv\Channels;
|
||||
|
||||
use Intervention\Image\Colors\AbstractColorChannel;
|
||||
|
||||
class Value extends AbstractColorChannel
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::min()
|
||||
*/
|
||||
public function min(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::max()
|
||||
*/
|
||||
public function max(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
}
|
148
src/Colors/Hsv/Color.php
Normal file
148
src/Colors/Hsv/Color.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Hsv;
|
||||
|
||||
use Intervention\Image\Colors\Hsv\Channels\Hue;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Saturation;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Value;
|
||||
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
|
||||
use Intervention\Image\Colors\Traits\CanHandleChannels;
|
||||
use Intervention\Image\Drivers\AbstractInputHandler;
|
||||
use Intervention\Image\Interfaces\ColorChannelInterface;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
|
||||
class Color implements ColorInterface
|
||||
{
|
||||
use CanHandleChannels;
|
||||
|
||||
/**
|
||||
* Color channels
|
||||
*/
|
||||
protected array $channels;
|
||||
|
||||
public function __construct(int $h, int $s, int $v)
|
||||
{
|
||||
$this->channels = [
|
||||
new Hue($h),
|
||||
new Saturation($s),
|
||||
new Value($v),
|
||||
];
|
||||
}
|
||||
|
||||
public function colorspace(): ColorspaceInterface
|
||||
{
|
||||
return new Colorspace();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorInterface::create()
|
||||
*/
|
||||
public static function create(mixed $input): ColorInterface
|
||||
{
|
||||
return (new class ([
|
||||
Decoders\StringColorDecoder::class,
|
||||
]) extends AbstractInputHandler
|
||||
{
|
||||
})->handle($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Hue channel
|
||||
*
|
||||
* @return ColorChannelInterface
|
||||
*/
|
||||
public function hue(): ColorChannelInterface
|
||||
{
|
||||
return $this->channel(Hue::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Saturation channel
|
||||
*
|
||||
* @return ColorChannelInterface
|
||||
*/
|
||||
public function saturation(): ColorChannelInterface
|
||||
{
|
||||
return $this->channel(Saturation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Value channel
|
||||
*
|
||||
* @return ColorChannelInterface
|
||||
*/
|
||||
public function value(): ColorChannelInterface
|
||||
{
|
||||
return $this->channel(Value::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorInterface::toArray()
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_map(function (ColorChannelInterface $channel) {
|
||||
return $channel->value();
|
||||
}, $this->channels());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorInterface::convertTo()
|
||||
*/
|
||||
public function convertTo(string|ColorspaceInterface $colorspace): ColorInterface
|
||||
{
|
||||
$colorspace = match (true) {
|
||||
is_object($colorspace) => $colorspace,
|
||||
default => new $colorspace(),
|
||||
};
|
||||
|
||||
return $colorspace->convertColor($this);
|
||||
}
|
||||
|
||||
public function toHex(string $prefix = ''): string
|
||||
{
|
||||
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorInterface::toString()
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'hsv(%d, %d%%, %d%%)',
|
||||
$this->hue()->value(),
|
||||
$this->saturation()->value(),
|
||||
$this->value()->value()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorInterface::isGreyscale()
|
||||
*/
|
||||
public function isGreyscale(): bool
|
||||
{
|
||||
return $this->saturation()->value() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorInterface::__toString()
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
77
src/Colors/Hsv/Colorspace.php
Normal file
77
src/Colors/Hsv/Colorspace.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Hsv;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
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 Colorspace implements ColorspaceInterface
|
||||
{
|
||||
public static $channels = [
|
||||
Channels\Hue::class,
|
||||
Channels\Saturation::class,
|
||||
Channels\Value::class
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorspaceInterface::colorFromNormalized()
|
||||
*/
|
||||
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)) {
|
||||
CmykColor::class => $this->convertRgbColor($color->convertTo(RgbColorspace::class)),
|
||||
RgbColor::class => $this->convertRgbColor($color),
|
||||
default => $color,
|
||||
};
|
||||
}
|
||||
|
||||
protected function convertRgbColor(RgbColor $color): Color
|
||||
{
|
||||
// percentage values of rgb channels
|
||||
$values = array_map(function ($channel) {
|
||||
return $channel->normalize();
|
||||
}, $color->channels());
|
||||
|
||||
// take only RGB
|
||||
$values = array_slice($values, 0, 3);
|
||||
|
||||
// calculate chroma
|
||||
$min = min(...$values);
|
||||
$max = max(...$values);
|
||||
$chroma = $max - $min;
|
||||
|
||||
// calculate value
|
||||
$v = 100 * $max;
|
||||
|
||||
// calculate saturation
|
||||
$s = 100 * ($chroma / $max);
|
||||
|
||||
// calculate hue
|
||||
list($r, $g, $b) = $values;
|
||||
$h = match (true) {
|
||||
($r == $min) => 3 - (($g - $b) / $chroma),
|
||||
($b == $min) => 1 - (($r - $g) / $chroma),
|
||||
default => 5 - (($b - $r) / $chroma),
|
||||
} * 60;
|
||||
|
||||
return new Color(
|
||||
intval(round($h)),
|
||||
intval(round($s)),
|
||||
intval(round($v))
|
||||
);
|
||||
}
|
||||
}
|
40
src/Colors/Hsv/Decoders/StringColorDecoder.php
Normal file
40
src/Colors/Hsv/Decoders/StringColorDecoder.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Colors\Hsv\Decoders;
|
||||
|
||||
use Intervention\Image\Colors\Hsv\Color;
|
||||
use Intervention\Image\Drivers\AbstractDecoder;
|
||||
use Intervention\Image\Exceptions\DecoderException;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\DecoderInterface;
|
||||
use Intervention\Image\Interfaces\ImageInterface;
|
||||
|
||||
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
|
||||
{
|
||||
/**
|
||||
* Decode hsv/hsb color strings
|
||||
*
|
||||
* @param mixed $input
|
||||
* @return ImageInterface|ColorInterface
|
||||
*/
|
||||
public function decode($input): ImageInterface|ColorInterface
|
||||
{
|
||||
if (! is_string($input)) {
|
||||
throw new DecoderException('Unable to decode input');
|
||||
}
|
||||
|
||||
$pattern = '/^hs(v|b)\((?P<h>[0-9\.]+), ?(?P<s>[0-9\.]+%?), ?(?P<v>[0-9\.]+%?)?\)$/i';
|
||||
if (preg_match($pattern, $input, $matches) != 1) {
|
||||
throw new DecoderException('Unable to decode input');
|
||||
}
|
||||
|
||||
$values = array_map(function ($value) {
|
||||
return match (strpos($value, '%')) {
|
||||
false => intval(trim($value)),
|
||||
default => intval(trim(str_replace('%', '', $value))),
|
||||
};
|
||||
}, [$matches['h'], $matches['s'], $matches['v']]);
|
||||
|
||||
return new Color(...$values);
|
||||
}
|
||||
}
|
@@ -8,9 +8,4 @@ class Alpha extends Red
|
||||
{
|
||||
return strval(round($this->normalize(), 6));
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
||||
|
@@ -2,59 +2,10 @@
|
||||
|
||||
namespace Intervention\Image\Colors\Rgb\Channels;
|
||||
|
||||
use Intervention\Image\Exceptions\ColorException;
|
||||
use Intervention\Image\Interfaces\ColorChannelInterface;
|
||||
use Intervention\Image\Colors\AbstractColorChannel;
|
||||
|
||||
class Red implements ColorChannelInterface
|
||||
class Red extends AbstractColorChannel
|
||||
{
|
||||
protected int $value;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::__construct()
|
||||
*/
|
||||
public function __construct(int $value = null, float $normalized = null)
|
||||
{
|
||||
$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')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of value()
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function toInt(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::value()
|
||||
*/
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::normalize()
|
||||
*/
|
||||
public function normalize($precision = 32): float
|
||||
{
|
||||
return round($this->value() / $this->max(), $precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
@@ -74,38 +25,4 @@ class Red implements ColorChannelInterface
|
||||
{
|
||||
return 255;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::validate()
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::toString()
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return (string) $this->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorChannelInterface::__toString()
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Intervention\Image\Colors\Rgb;
|
||||
|
||||
use Intervention\Image\Colors\Hsv\Color as HsvColor;
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
@@ -18,7 +19,7 @@ class Colorspace implements ColorspaceInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorspaceInterface::createColor()
|
||||
* @see ColorspaceInterface::colorFromNormalized()
|
||||
*/
|
||||
public function colorFromNormalized(array $normalized): ColorInterface
|
||||
{
|
||||
@@ -29,10 +30,16 @@ class Colorspace implements ColorspaceInterface
|
||||
return new Color(...$values);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see ColorspaceInterface::convertColor()
|
||||
*/
|
||||
public function convertColor(ColorInterface $color): ColorInterface
|
||||
{
|
||||
return match (get_class($color)) {
|
||||
CmykColor::class => $this->convertCmykColor($color),
|
||||
HsvColor::class => $this->convertHsvColor($color),
|
||||
default => $color,
|
||||
};
|
||||
}
|
||||
@@ -45,4 +52,30 @@ class Colorspace implements ColorspaceInterface
|
||||
(int) (255 * (1 - $color->yellow()->normalize()) * (1 - $color->key()->normalize())),
|
||||
);
|
||||
}
|
||||
|
||||
protected function convertHsvColor(HsvColor $color): Color
|
||||
{
|
||||
$chroma = $color->value()->normalize() * $color->saturation()->normalize();
|
||||
$hue = $color->hue()->normalize() * 6;
|
||||
$x = $chroma * (1 - abs(fmod($hue, 2) - 1));
|
||||
|
||||
// connect channel values
|
||||
$values = match (true) {
|
||||
$hue < 1 => [$chroma, $x, 0],
|
||||
$hue < 2 => [$x, $chroma, 0],
|
||||
$hue < 3 => [0, $chroma, $x],
|
||||
$hue < 4 => [0, $x, $chroma],
|
||||
$hue < 5 => [$x, 0, $chroma],
|
||||
default => [$chroma, 0, $x],
|
||||
};
|
||||
|
||||
// add to each value
|
||||
$values = array_map(function ($value) use ($color, $chroma) {
|
||||
return $value + ($color->value()->normalize() - $chroma);
|
||||
}, $values);
|
||||
|
||||
array_push($values, 1); // append alpha channel value
|
||||
|
||||
return $this->colorFromNormalized($values);
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ use Intervention\Image\Colors\Rgb\Decoders\StringColorDecoder as RgbStringColorD
|
||||
use Intervention\Image\Colors\Rgb\Decoders\HtmlColornameDecoder;
|
||||
use Intervention\Image\Colors\Rgb\Decoders\TransparentColorDecoder;
|
||||
use Intervention\Image\Colors\Cmyk\Decoders\StringColorDecoder as CmykStringColorDecoder;
|
||||
use Intervention\Image\Colors\Hsv\Decoders\StringColorDecoder as HsvStringColorDecoder;
|
||||
use Intervention\Image\Drivers\AbstractInputHandler;
|
||||
use Intervention\Image\Drivers\Gd\Decoders\ImageObjectDecoder;
|
||||
use Intervention\Image\Drivers\Gd\Decoders\ColorObjectDecoder;
|
||||
@@ -25,6 +26,7 @@ class InputHandler extends AbstractInputHandler
|
||||
RgbHexColorDecoder::class,
|
||||
RgbStringColorDecoder::class,
|
||||
CmykStringColorDecoder::class,
|
||||
HsvStringColorDecoder::class,
|
||||
TransparentColorDecoder::class,
|
||||
HtmlColornameDecoder::class,
|
||||
FilePointerImageDecoder::class,
|
||||
|
@@ -7,6 +7,7 @@ use Intervention\Image\Colors\Rgb\Decoders\StringColorDecoder as RgbStringColorD
|
||||
use Intervention\Image\Colors\Rgb\Decoders\HtmlColornameDecoder;
|
||||
use Intervention\Image\Colors\Rgb\Decoders\TransparentColorDecoder;
|
||||
use Intervention\Image\Colors\Cmyk\Decoders\StringColorDecoder as CmykStringColorDecoder;
|
||||
use Intervention\Image\Colors\Hsv\Decoders\StringColorDecoder as HsvStringColorDecoder;
|
||||
use Intervention\Image\Drivers\AbstractInputHandler;
|
||||
use Intervention\Image\Drivers\Imagick\Decoders\ImageObjectDecoder;
|
||||
use Intervention\Image\Drivers\Imagick\Decoders\ColorObjectDecoder;
|
||||
@@ -25,6 +26,7 @@ class InputHandler extends AbstractInputHandler
|
||||
RgbHexColorDecoder::class,
|
||||
RgbStringColorDecoder::class,
|
||||
CmykStringColorDecoder::class,
|
||||
HsvStringColorDecoder::class,
|
||||
TransparentColorDecoder::class,
|
||||
HtmlColornameDecoder::class,
|
||||
FilePointerImageDecoder::class,
|
||||
|
@@ -2,8 +2,13 @@
|
||||
|
||||
namespace Intervention\Image\Tests\Colors\Cmyk;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Cyan;
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Key;
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Magenta;
|
||||
use Intervention\Image\Colors\Cmyk\Channels\Yellow;
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Colors\Hsv\Color as HsvColor;
|
||||
use Intervention\Image\Colors\Cmyk\Colorspace;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
|
||||
@@ -13,14 +18,25 @@ use Intervention\Image\Tests\TestCase;
|
||||
*/
|
||||
class ColorspaceTest extends TestCase
|
||||
{
|
||||
public function testConvertColor(): void
|
||||
public function testConvertRgbColor(): void
|
||||
{
|
||||
$colorspace = new Colorspace();
|
||||
$this->assertInstanceOf(
|
||||
CmykColor::class,
|
||||
$colorspace->convertColor(
|
||||
new RgbColor(0, 0, 0)
|
||||
)
|
||||
);
|
||||
$result = $colorspace->convertColor(new RgbColor(0, 0, 255));
|
||||
$this->assertInstanceOf(CmykColor::class, $result);
|
||||
$this->assertEquals(100, $result->channel(Cyan::class)->value());
|
||||
$this->assertEquals(100, $result->channel(Magenta::class)->value());
|
||||
$this->assertEquals(0, $result->channel(Yellow::class)->value());
|
||||
$this->assertEquals(0, $result->channel(Key::class)->value());
|
||||
}
|
||||
|
||||
public function testConvertHsvColor(): void
|
||||
{
|
||||
$colorspace = new Colorspace();
|
||||
$result = $colorspace->convertColor(new HsvColor(240, 100, 100));
|
||||
$this->assertInstanceOf(CmykColor::class, $result);
|
||||
$this->assertEquals(100, $result->channel(Cyan::class)->value());
|
||||
$this->assertEquals(100, $result->channel(Magenta::class)->value());
|
||||
$this->assertEquals(0, $result->channel(Yellow::class)->value());
|
||||
$this->assertEquals(0, $result->channel(Key::class)->value());
|
||||
}
|
||||
}
|
||||
|
75
tests/Colors/Hsv/ChannelTest.php
Normal file
75
tests/Colors/Hsv/ChannelTest.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Tests\Colors\Hsv;
|
||||
|
||||
use Intervention\Image\Colors\Hsv\Channels\Hue;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Saturation;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Value;
|
||||
use Intervention\Image\Exceptions\ColorException;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @requires extension gd
|
||||
* @covers \Intervention\Image\Colors\Hsv\Channels\Hue
|
||||
* @covers \Intervention\Image\Colors\Hsv\Channels\Saturation
|
||||
* @covers \Intervention\Image\Colors\Hsv\Channels\Value
|
||||
*/
|
||||
class ChannelTest extends TestCase
|
||||
{
|
||||
public function testConstructor(): void
|
||||
{
|
||||
$channel = new Hue(0);
|
||||
$this->assertInstanceOf(Hue::class, $channel);
|
||||
|
||||
$channel = new Hue(value: 0);
|
||||
$this->assertInstanceOf(Hue::class, $channel);
|
||||
|
||||
$channel = new Hue(normalized: 0);
|
||||
$this->assertInstanceOf(Hue::class, $channel);
|
||||
|
||||
$this->expectException(ColorException::class);
|
||||
$channel = new Hue();
|
||||
|
||||
$this->expectException(ColorException::class);
|
||||
$channel = new Hue(normalized: 2);
|
||||
}
|
||||
|
||||
public function testValue(): void
|
||||
{
|
||||
$channel = new Hue(10);
|
||||
$this->assertEquals(10, $channel->value());
|
||||
}
|
||||
|
||||
public function testNormalize(): void
|
||||
{
|
||||
$channel = new Hue(360);
|
||||
$this->assertEquals(1, $channel->normalize());
|
||||
$channel = new Hue(180);
|
||||
$this->assertEquals(0.5, $channel->normalize());
|
||||
$channel = new Hue(0);
|
||||
$this->assertEquals(0, $channel->normalize());
|
||||
$channel = new Hue(90);
|
||||
$this->assertEquals(.25, $channel->normalize());
|
||||
}
|
||||
|
||||
public function testValidate(): void
|
||||
{
|
||||
$this->expectException(ColorException::class);
|
||||
new Hue(361);
|
||||
|
||||
$this->expectException(ColorException::class);
|
||||
new Hue(-1);
|
||||
|
||||
$this->expectException(ColorException::class);
|
||||
new Saturation(101);
|
||||
|
||||
$this->expectException(ColorException::class);
|
||||
new Saturation(-1);
|
||||
|
||||
$this->expectException(ColorException::class);
|
||||
new Value(101);
|
||||
|
||||
$this->expectException(ColorException::class);
|
||||
new Value(-1);
|
||||
}
|
||||
}
|
84
tests/Colors/Hsv/ColorTest.php
Normal file
84
tests/Colors/Hsv/ColorTest.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Tests\Colors\Hsv;
|
||||
|
||||
use Intervention\Image\Colors\Hsv\Channels\Hue;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Saturation;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Value;
|
||||
use Intervention\Image\Colors\Hsv\Color as Color;
|
||||
use Intervention\Image\Colors\Hsv\Colorspace;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \Intervention\Image\Colors\Hsv\Color
|
||||
*/
|
||||
class ColorTest extends TestCase
|
||||
{
|
||||
public function testConstructor(): void
|
||||
{
|
||||
$color = new Color(0, 0, 0);
|
||||
$this->assertInstanceOf(Color::class, $color);
|
||||
}
|
||||
|
||||
public function testColorspace(): void
|
||||
{
|
||||
$color = new Color(0, 0, 0);
|
||||
$this->assertInstanceOf(Colorspace::class, $color->colorspace());
|
||||
}
|
||||
|
||||
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(Hue::class);
|
||||
$this->assertInstanceOf(Hue::class, $channel);
|
||||
$this->assertEquals(10, $channel->value());
|
||||
}
|
||||
|
||||
public function testHueSaturationValueKey(): void
|
||||
{
|
||||
$color = new Color(10, 20, 30);
|
||||
$this->assertInstanceOf(Hue::class, $color->hue());
|
||||
$this->assertInstanceOf(Saturation::class, $color->saturation());
|
||||
$this->assertInstanceOf(Value::class, $color->value());
|
||||
$this->assertEquals(10, $color->hue()->value());
|
||||
$this->assertEquals(20, $color->saturation()->value());
|
||||
$this->assertEquals(30, $color->value()->value());
|
||||
}
|
||||
|
||||
public function testToArray(): void
|
||||
{
|
||||
$color = new Color(10, 20, 30);
|
||||
$this->assertEquals([10, 20, 30], $color->toArray());
|
||||
}
|
||||
|
||||
public function testNormalize(): void
|
||||
{
|
||||
$color = new Color(180, 50, 25);
|
||||
$this->assertEquals([.5, 0.5, 0.25], $color->normalize());
|
||||
}
|
||||
|
||||
public function testToString(): void
|
||||
{
|
||||
$color = new Color(100, 50, 20, 0);
|
||||
$this->assertEquals('hsv(100, 50%, 20%)', (string) $color);
|
||||
}
|
||||
|
||||
public function testIsGreyscale(): void
|
||||
{
|
||||
$color = new Color(0, 1, 0);
|
||||
$this->assertFalse($color->isGreyscale());
|
||||
|
||||
$color = new Color(1, 0, 0);
|
||||
$this->assertTrue($color->isGreyscale());
|
||||
|
||||
$color = new Color(0, 0, 1);
|
||||
$this->assertTrue($color->isGreyscale());
|
||||
}
|
||||
}
|
39
tests/Colors/Hsv/ColorspaceTest.php
Normal file
39
tests/Colors/Hsv/ColorspaceTest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Tests\Colors\Hsv;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Hue;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Saturation;
|
||||
use Intervention\Image\Colors\Hsv\Channels\Value;
|
||||
use Intervention\Image\Colors\Hsv\Color as HsvColor;
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Colors\Hsv\Colorspace;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @requires extension gd
|
||||
* @covers \Intervention\Image\Colors\Hsv\Colorspace
|
||||
*/
|
||||
class ColorspaceTest extends TestCase
|
||||
{
|
||||
public function testConvertRgbColor(): void
|
||||
{
|
||||
$colorspace = new Colorspace();
|
||||
$result = $colorspace->convertColor(new RgbColor(26, 26, 128));
|
||||
$this->assertInstanceOf(HsvColor::class, $result);
|
||||
$this->assertEquals(240, $result->channel(Hue::class)->value());
|
||||
$this->assertEquals(80, $result->channel(Saturation::class)->value());
|
||||
$this->assertEquals(50, $result->channel(Value::class)->value());
|
||||
}
|
||||
|
||||
public function testConvertCmykColor(): void
|
||||
{
|
||||
$colorspace = new Colorspace();
|
||||
$result = $colorspace->convertColor(new CmykColor(100, 0, 100, 0));
|
||||
$this->assertInstanceOf(HsvColor::class, $result);
|
||||
$this->assertEquals(120, $result->channel(Hue::class)->value());
|
||||
$this->assertEquals(100, $result->channel(Saturation::class)->value());
|
||||
$this->assertEquals(100, $result->channel(Value::class)->value());
|
||||
}
|
||||
}
|
56
tests/Colors/Hsv/Decoders/StringColorDecoderTest.php
Normal file
56
tests/Colors/Hsv/Decoders/StringColorDecoderTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Tests\Colors\Hsv\Decoders;
|
||||
|
||||
use Intervention\Image\Colors\Hsv\Color;
|
||||
use Intervention\Image\Colors\Hsv\Decoders\StringColorDecoder;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @requires extension gd
|
||||
* @covers \Intervention\Image\Colors\Hsv\Decoders\StringColorDecoder
|
||||
*/
|
||||
class StringColorDecoderTest extends TestCase
|
||||
{
|
||||
public function testDecodeHsv(): void
|
||||
{
|
||||
$decoder = new StringColorDecoder();
|
||||
$result = $decoder->decode('hsv(0,0,0)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([0, 0, 0], $result->toArray());
|
||||
|
||||
$result = $decoder->decode('hsv(0, 100, 100)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([0, 100, 100], $result->toArray());
|
||||
|
||||
$result = $decoder->decode('hsv(360, 100, 100)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([360, 100, 100], $result->toArray());
|
||||
|
||||
|
||||
$result = $decoder->decode('hsv(180, 100%, 100%)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([180, 100, 100], $result->toArray());
|
||||
}
|
||||
|
||||
public function testDecodeHsb(): void
|
||||
{
|
||||
$decoder = new StringColorDecoder();
|
||||
$result = $decoder->decode('hsb(0,0,0)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([0, 0, 0], $result->toArray());
|
||||
|
||||
$result = $decoder->decode('hsb(0, 100, 100)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([0, 100, 100], $result->toArray());
|
||||
|
||||
$result = $decoder->decode('hsb(360, 100, 100)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([360, 100, 100], $result->toArray());
|
||||
|
||||
|
||||
$result = $decoder->decode('hsb(180, 100%, 100%)');
|
||||
$this->assertInstanceOf(Color::class, $result);
|
||||
$this->assertEquals([180, 100, 100], $result->toArray());
|
||||
}
|
||||
}
|
@@ -3,6 +3,10 @@
|
||||
namespace Intervention\Image\Tests\Colors\Rgb;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Colors\Hsv\Color as HsvColor;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Blue;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Green;
|
||||
use Intervention\Image\Colors\Rgb\Channels\Red;
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Colors\Rgb\Colorspace;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
@@ -13,7 +17,7 @@ use Intervention\Image\Tests\TestCase;
|
||||
*/
|
||||
class ColorspaceTest extends TestCase
|
||||
{
|
||||
public function testConvertColor(): void
|
||||
public function testConvertCmykColor(): void
|
||||
{
|
||||
$colorspace = new Colorspace();
|
||||
$this->assertInstanceOf(
|
||||
@@ -23,4 +27,14 @@ class ColorspaceTest extends TestCase
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testConvertHsvColor(): void
|
||||
{
|
||||
$colorspace = new Colorspace();
|
||||
$result = $colorspace->convertColor(new HsvColor(240, 80, 50));
|
||||
$this->assertInstanceOf(RgbColor::class, $result);
|
||||
$this->assertEquals(26, $result->channel(Red::class)->value());
|
||||
$this->assertEquals(26, $result->channel(Green::class)->value());
|
||||
$this->assertEquals(128, $result->channel(Blue::class)->value());
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Intervention\Image\Tests\Drivers\Gd;
|
||||
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Colors\Hsv\Color as HsvColor;
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\Drivers\Gd\InputHandler;
|
||||
@@ -114,6 +116,22 @@ class GdInputHandlerTest extends TestCase
|
||||
$this->assertEquals([10, 20, 30, 255], $result->toArray());
|
||||
}
|
||||
|
||||
public function testHandleHsvString(): void
|
||||
{
|
||||
$handler = new InputHandler();
|
||||
$result = $handler->handle('hsv(10, 20, 30)');
|
||||
$this->assertInstanceOf(HsvColor::class, $result);
|
||||
$this->assertEquals([10, 20, 30], $result->toArray());
|
||||
}
|
||||
|
||||
public function testHandleCmykString(): void
|
||||
{
|
||||
$handler = new InputHandler();
|
||||
$result = $handler->handle('cmyk(10, 20, 30, 40)');
|
||||
$this->assertInstanceOf(CmykColor::class, $result);
|
||||
$this->assertEquals([10, 20, 30, 40], $result->toArray());
|
||||
}
|
||||
|
||||
public function testHandleTransparent(): void
|
||||
{
|
||||
$handler = new InputHandler();
|
||||
|
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace Intervention\Image\Tests\Drivers\Imagick;
|
||||
|
||||
use SplFileInfo;
|
||||
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
|
||||
use Intervention\Image\Colors\Hsv\Color as HsvColor;
|
||||
use Intervention\Image\Colors\Rgb\Color as RgbColor;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\Drivers\Imagick\InputHandler;
|
||||
use Intervention\Image\Exceptions\DecoderException;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* @requires extension imagick
|
||||
@@ -114,6 +116,22 @@ class InputHandlerTest extends TestCase
|
||||
$this->assertEquals([10, 20, 30, 255], $result->toArray());
|
||||
}
|
||||
|
||||
public function testHandleCmykString(): void
|
||||
{
|
||||
$handler = new InputHandler();
|
||||
$result = $handler->handle('cmyk(10, 20, 30, 40)');
|
||||
$this->assertInstanceOf(CmykColor::class, $result);
|
||||
$this->assertEquals([10, 20, 30, 40], $result->toArray());
|
||||
}
|
||||
|
||||
public function testHandleHsvString(): void
|
||||
{
|
||||
$handler = new InputHandler();
|
||||
$result = $handler->handle('hsv(10, 20, 30)');
|
||||
$this->assertInstanceOf(HsvColor::class, $result);
|
||||
$this->assertEquals([10, 20, 30], $result->toArray());
|
||||
}
|
||||
|
||||
public function testHandleTransparent(): void
|
||||
{
|
||||
$handler = new InputHandler();
|
||||
|
Reference in New Issue
Block a user