refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

View File

@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Error;
use Intervention\Image\Exceptions\InvalidArgumentException;
enum Alignment: string
{
case TOP = 'top';
case TOP_RIGHT = 'top-right';
case RIGHT = 'right';
case BOTTOM_RIGHT = 'bottom-right';
case BOTTOM = 'bottom';
case BOTTOM_LEFT = 'bottom-left';
case LEFT = 'left';
case TOP_LEFT = 'top-left';
case CENTER = 'center';
/**
* Create position from given identifier.
*
* @throws InvalidArgumentException
*/
public static function create(string|self $identifier): self
{
if ($identifier instanceof self) {
return $identifier;
}
try {
$position = self::from(strtolower($identifier));
} catch (Error) {
$position = match (strtolower($identifier)) {
'top-center',
'top_center',
'topcenter',
'center-top',
'center_top',
'centertop',
'top-middle',
'top_middle',
'topmiddle',
'middle-top',
'middle_top',
'middletop' => self::TOP,
'top_right',
'topright',
'right-top',
'right_top',
'righttop' => self::TOP_RIGHT,
'right-center',
'right_center',
'rightcenter',
'center-right',
'center_right',
'centerright',
'right-middle',
'right_middle',
'rightmiddle',
'middle-right',
'middle_right',
'middleright' => self::RIGHT,
'bottom_right',
'bottomright',
'right-bottom',
'right_bottom',
'rightbottom' => self::BOTTOM_RIGHT,
'bottom-center',
'bottom_center',
'bottomcenter',
'center-bottom',
'center_bottom',
'centerbottom',
'bottom-middle',
'bottom_middle',
'bottommiddle',
'middle-bottom',
'middle_bottom',
'middlebottom' => self::BOTTOM,
'bottom_left',
'bottomleft',
'left-bottom',
'left_bottom',
'leftbottom' => self::BOTTOM_LEFT,
'left-center',
'left_center',
'leftcenter',
'center-left',
'center_left',
'centerleft',
'left-middle',
'left_middle',
'leftmiddle',
'middle-left',
'middle_left',
'middleleft' => self::LEFT,
'top_left',
'topleft',
'left-top',
'left_top',
'lefttop' => self::TOP_LEFT,
'middle',
'center-center',
'center_center',
'centercenter',
'center-middle',
'center_middle',
'centermiddle',
'middle-center',
'middle_center',
'middlecenter' => self::CENTER,
default => throw new InvalidArgumentException(
'Unable to create ' . self::class . ' from "' . $identifier . '"',
),
};
}
return $position;
}
/**
* Try to create position from given identifier or return null on failure.
*/
public static function tryCreate(string|self $identifier): ?self
{
try {
return self::create($identifier);
} catch (InvalidArgumentException) {
return null;
}
}
/**
* Change the current alignment by adjusting only the horizontal axis to the specified value.
*/
public function alignHorizontally(string|self $alignment): self
{
// handle "leftish" alignments
if (in_array($alignment, [self::LEFT, self::BOTTOM_LEFT, self::TOP_LEFT])) {
return match ($this) {
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP_LEFT,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM_LEFT,
self::CENTER, self::LEFT, self::RIGHT => self::LEFT,
};
}
// handle "rightish" alignments
if (in_array($alignment, [self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT])) {
return match ($this) {
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP_RIGHT,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM_RIGHT,
self::CENTER, self::LEFT, self::RIGHT => self::RIGHT,
};
}
// handle centering
if (in_array($alignment, [self::CENTER, self::TOP, self::BOTTOM])) {
return match ($this) {
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM,
self::CENTER, self::LEFT, self::RIGHT => self::CENTER,
};
}
return $this;
}
/**
* Change the current alignment by adjusting only the vertical axis to the specified value.
*/
public function alignVertically(string|self $alignment): self
{
// handle "bottomish" alignments
if (in_array($alignment, [self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT])) {
return match ($this) {
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::BOTTOM_LEFT,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::BOTTOM_RIGHT,
self::CENTER, self::TOP, self::BOTTOM => self::BOTTOM,
};
}
// handle "topish" alignments
if (in_array($alignment, [self::TOP, self::TOP_RIGHT, self::TOP_LEFT])) {
return match ($this) {
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::TOP_LEFT,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::TOP_RIGHT,
self::CENTER, self::TOP, self::BOTTOM => self::TOP,
};
}
// handle centering
if (in_array($alignment, [self::CENTER, self::RIGHT, self::LEFT])) {
return match ($this) {
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::LEFT,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::RIGHT,
self::CENTER, self::TOP, self::BOTTOM => self::CENTER,
};
}
return $this;
}
/**
* Return only the horizontal alignment.
*/
public function horizontal(): self
{
return match ($this) {
self::TOP, self::CENTER, self::BOTTOM => self::CENTER,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::RIGHT,
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::LEFT,
};
}
/**
* Return only the vertical alignment.
*/
public function vertical(): self
{
return match ($this) {
self::CENTER, self::RIGHT, self::LEFT => self::CENTER,
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM,
};
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class ColorspaceAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class HeightAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class PixelColorAnalyzer extends SpecializableAnalyzer
{
public function __construct(
public int $x,
public int $y,
public int $frame = 0
) {
//
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class PixelColorsAnalyzer extends SpecializableAnalyzer
{
public function __construct(
public int $x,
public int $y
) {
//
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class ProfileAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class ResolutionAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class WidthAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Error;
use Intervention\Gif\DisposalMethod;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\FilesystemException;
use Intervention\Image\Interfaces\AnimationFactoryInterface;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
class AnimationFactory implements AnimationFactoryInterface
{
/**
* Current frame number.
*/
protected int $currentFrameNumber = 0;
/**
* Image sources of animation frames.
*
* @var array<mixed>
*/
protected array $sources = [];
/**
* Frame delays of animation frames in seconds.
*
* @var array<float>
*/
protected array $delays = [];
/**
* Frame processing call names.
*
* @var array<null|string>
*/
protected array $processingCalls = [];
/**
* Frame processing arguments of calls.
*
* @var array<null|array<mixed>>
*/
protected array $processingArguments = [];
/**
* Create new instance.
*/
public function __construct(
protected int $width,
protected int $height,
null|callable $animation = null,
) {
if (is_callable($animation)) {
$animation($this);
}
}
/**
* {@inheritdoc}
*
* @see AnimationFactoryInterface::build()
*/
public static function build(
int $width,
int $height,
callable $animation,
DriverInterface $driver,
): ImageInterface {
return (new self($width, $height, $animation))->image($driver);
}
/**
* {@inheritdoc}
*
* @see AnimationFactoryInterface::add()
*/
public function add(mixed $source, float $delay = 1): AnimationFactoryInterface
{
$this->currentFrameNumber++;
$this->sources[$this->currentFrameNumber] = $source;
$this->delays[$this->currentFrameNumber] = $delay;
$this->processingCalls[$this->currentFrameNumber] = null;
$this->processingArguments[$this->currentFrameNumber] = null;
return $this;
}
/**
* {@inheritdoc}
*
* @see AnimationFactoryInterface::image()
*/
public function image(DriverInterface $driver): ImageInterface
{
if (count($this->sources) === 0) {
return $driver->createImage($this->width, $this->height);
}
$frames = array_map(
$this->buildFrame(...),
array_fill(0, count($this->sources), $driver),
$this->sources,
$this->delays,
$this->processingCalls,
$this->processingArguments,
);
return new Image($driver, $driver->createCore($frames));
}
/**
* Build frame from given image source and delay.
*
* @param null|array<mixed> $processingArguments
*/
private function buildFrame(
DriverInterface $driver,
mixed $source,
float $delay,
?string $processingCall = null,
?array $processingArguments = null,
): FrameInterface {
try {
// try to decode image source
$image = $driver->decodeImage($source);
} catch (DecoderException | FilesystemException) {
// create empty image with colored background
$image = $driver->createImage($this->width, $this->height)
->fill($driver->decodeColor($source));
}
// adjust size if necessary
if ($image->width() !== $this->width || $image->height() !== $this->height) {
$image->cover($this->width, $this->height);
}
// apply processing call if available
if ($processingCall !== null) {
call_user_func_array([$image, $processingCall], $processingArguments);
}
// return ready-made frame with all attributes
return $image
->core()
->first()
->setDelay($delay)
->setDisposalMethod(DisposalMethod::BACKGROUND->value);
}
/**
* Collect processing calls on frame images.
*
* @param array<null|array<mixed>> $arguments
* @throws Error
*/
public function __call(string $name, array $arguments): self
{
if (!method_exists(Image::class, $name)) {
throw new Error('Call to undefined method ' . Image::class . '::' . $name . '()');
}
$this->processingCalls[$this->currentFrameNumber] = $name;
$this->processingArguments[$this->currentFrameNumber] = $arguments;
return $this;
}
}

View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Intervention\Image\Interfaces\CollectionInterface;
use ArrayIterator;
use Countable;
use Traversable;
use IteratorAggregate;
/**
* @implements IteratorAggregate<int|string, mixed>
*/
class Collection implements CollectionInterface, IteratorAggregate, Countable
{
/**
* Create new collection object.
*
* @param array<int|string, mixed> $items
*/
public function __construct(protected array $items = [])
{
//
}
/**
* Static constructor.
*
* @param array<int|string, mixed> $items
* @return self<int|string, mixed>
*/
public static function create(array $items = []): self
{
return new self($items);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::has()
*/
public function has(int|string $key): bool
{
return array_key_exists($key, $this->items);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::set()
*/
public function set(int|string $key, mixed $item): self
{
$this->items[$key] = $item;
return $this;
}
/**
* Returns Iterator.
*
* @return Traversable<int|string, mixed>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::toArray()
*/
public function toArray(): array
{
return $this->items;
}
/**
* Count items in collection.
*
* @return int<0, max>
*/
public function count(): int
{
return count($this->items);
}
/**
* Append new item to collection.
*
* @return CollectionInterface<int|string, mixed>
*/
public function push(mixed $item): CollectionInterface
{
$this->items[] = $item;
return $this;
}
/**
* Return first item in collection.
*/
public function first(): mixed
{
if (count($this->items) === 0) {
return null;
}
return reset($this->items);
}
/**
* Returns last item in collection.
*/
public function last(): mixed
{
if (count($this->items) === 0) {
return null;
}
return end($this->items);
}
/**
* Return item at given position starting at 0.
*/
public function at(int $key = 0, mixed $default = null): mixed
{
if ($this->count() === 0) {
return $default;
}
$positions = array_values($this->items);
if (!array_key_exists($key, $positions)) {
return $default;
}
return $positions[$key];
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::get()
*/
public function get(int|string $query, mixed $default = null): mixed
{
if ($this->count() === 0) {
return $default;
}
if (is_int($query) && array_key_exists($query, $this->items)) {
return $this->items[$query];
}
if (is_string($query) && !str_contains($query, '.')) {
return array_key_exists($query, $this->items) ? $this->items[$query] : $default;
}
$query = explode('.', (string) $query);
$result = $default;
$items = $this->items;
foreach ($query as $key) {
if (!is_array($items) || !array_key_exists($key, $items)) {
$result = $default;
break;
}
$result = $items[$key];
$items = $result;
}
return $result;
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::map()
*/
public function map(callable $callback): self
{
return new self(
array_map(
fn(mixed $item) => $callback($item),
$this->items,
)
);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::map()
*/
public function filter(callable $callback): self
{
return new self(
array_filter(
$this->items,
fn(mixed $item) => $callback($item),
)
);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::clear()
*/
public function clear(): CollectionInterface
{
$this->items = [];
return $this;
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::slice()
*/
public function slice(int $offset, ?int $length = null): CollectionInterface
{
$this->items = array_slice($this->items, $offset, $length);
return $this;
}
}

173
vendor/intervention/image/src/Color.php vendored Normal file
View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
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\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Colors\Cmyk\Decoders\StringColorDecoder as CmykStringColorDecoder;
use Intervention\Image\Colors\Hsl\Decoders\StringColorDecoder as HslStringColorDecoder;
use Intervention\Image\Colors\Hsv\Decoders\StringColorDecoder as HsvStringColorDecoder;
use Intervention\Image\Colors\Oklab\Decoders\StringColorDecoder as OklabStringColorDecoder;
use Intervention\Image\Colors\Oklch\Decoders\StringColorDecoder as OklchStringColorDecoder;
use Intervention\Image\Colors\Rgb\Channels\Alpha as RgbAlpha;
use Intervention\Image\Colors\Cmyk\Channels\Alpha as CmykAlpha;
use Intervention\Image\Colors\Hsl\Channels\Alpha as HslAlpha;
use Intervention\Image\Colors\Hsl\Channels\Hue as HslHue;
use Intervention\Image\Colors\Hsl\Channels\Luminance;
use Intervention\Image\Colors\Hsl\Channels\Saturation as HslSaturation;
use Intervention\Image\Colors\Hsv\Channels\Alpha as HsvAlpha;
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\Oklab\Channels\A;
use Intervention\Image\Colors\Oklab\Channels\Alpha as OklabAlpha;
use Intervention\Image\Colors\Oklab\Channels\B;
use Intervention\Image\Colors\Oklab\Channels\Lightness as OklabLightness;
use Intervention\Image\Colors\Oklch\Channels\Alpha as OklchAlpha;
use Intervention\Image\Colors\Oklch\Channels\Chroma;
use Intervention\Image\Colors\Oklch\Channels\Lightness as OklchLightness;
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\Decoders\HexColorDecoder as RgbHexColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\NamedColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\StringColorDecoder as RgbStringColorDecoder;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\NotSupportedException;
class Color
{
/**
* Parse color from string value.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): ColorInterface
{
try {
$color = InputHandler::usingDecoders([
RgbStringColorDecoder::class,
CmykStringColorDecoder::class,
HsvStringColorDecoder::class,
HslStringColorDecoder::class,
OklabStringColorDecoder::class,
OklchStringColorDecoder::class,
NamedColorDecoder::class,
RgbHexColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse RGB color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof ColorInterface) {
throw new ColorException('Result must be instance of ' . self::class . ', got ' . $color::class);
}
return $color;
}
/**
* Create new RGB color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function rgb(int|Red $r, int|Green $g, int|Blue $b, float|RgbAlpha $a = 1): RgbColor
{
return new RgbColor($r, $g, $b, $a);
}
/**
* Create new CMYK color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function cmyk(
int|Cyan $c,
int|Magenta $m,
int|Yellow $y,
int|Key $k,
float|CmykAlpha $a = 1,
): CmykColor {
return new CmykColor($c, $m, $y, $k, $a);
}
/**
* Create new HSL color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function hsl(int|HslHue $h, int|HslSaturation $s, int|Luminance $l, float|HslAlpha $a = 1): HslColor
{
return new HslColor($h, $s, $l, $a);
}
/**
* Create new HSV color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function hsv(int|Hue $h, int|Saturation $s, int|Value $v, float|HsvAlpha $a = 1): HsvColor
{
return new HsvColor($h, $s, $v, $a);
}
/**
* Create new OKLAB color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function oklab(
float|OklabLightness $l,
float|A $a,
float|B $b,
float|OklabAlpha $alpha = 1,
): OklabColor {
return new OklabColor($l, $a, $b, $alpha);
}
/**
* Create new OKLCH color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function oklch(
float|OklchLightness $l,
float|Chroma $c,
float|Hue $h,
float|OklchAlpha $a = 1,
): OklchColor {
return new OklchColor($l, $c, $h, $a);
}
/**
* Create transparent RGB color.
*/
public static function transparent(): ColorInterface
{
// @phpstan-ignore missingType.checkedException
return new RgbColor(255, 255, 255, 0);
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Colors\Hsl\Channels\Luminance;
use Intervention\Image\Colors\Hsl\Channels\Saturation;
use Intervention\Image\Colors\Hsl\Colorspace as HslColorspace;
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\Colorspace as RgbColorspace;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use ReflectionClass;
use Stringable;
abstract class AbstractColor implements ColorInterface, Stringable
{
/**
* Color channels.
*
* @var array<ColorChannelInterface>
*/
protected array $channels;
/**
* {@inheritdoc}
*
* @see ColorInterface::channels()
*/
public function channels(): array
{
return $this->channels;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::channel()
*
* @throws InvalidArgumentException
*/
public function channel(string $classname): ColorChannelInterface
{
$channels = array_filter(
$this->channels(),
fn(ColorChannelInterface $channel): bool => $channel::class === $classname,
);
if (count($channels) === 0) {
throw new InvalidArgumentException('Color channel ' . $classname . ' could not be found');
}
return reset($channels);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toColorspace()
*
* @throws InvalidArgumentException
*/
public function toColorspace(string|ColorspaceInterface $colorspace): ColorInterface
{
if (is_string($colorspace) && !class_exists($colorspace)) {
throw new InvalidArgumentException('Unknown color space (' . $colorspace . ') as conversion target');
}
$colorspace = is_string($colorspace) ? new $colorspace() : $colorspace;
if (!$colorspace instanceof ColorspaceInterface) {
throw new InvalidArgumentException('Given color space must implement ' . ColorspaceInterface::class);
}
return $colorspace->importColor($this);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return $this->alpha()->value() < $this->alpha()->max();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return floatval($this->alpha()->value()) === 0.0;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withTransparency()
*
* @throws InvalidArgumentException
*/
public function withTransparency(float $transparency): ColorInterface
{
$color = clone $this;
$color->channels = array_map(
fn(ColorChannelInterface $channel): ColorChannelInterface =>
$channel instanceof AlphaChannel ? $channel::fromNormalized($transparency) : $channel,
$this->channels
);
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withBrightness()
*
* @throws InvalidArgumentException
*/
public function withBrightness(int $level): ColorInterface
{
$hsl = clone $this->toColorspace(HslColorspace::class);
$hsl->channel(Luminance::class)->scale($level);
return $hsl->toColorspace($this->colorspace());
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withSaturation()
*
* @throws InvalidArgumentException
*/
public function withSaturation(int $level): ColorInterface
{
$hsl = clone $this->toColorspace(HslColorspace::class);
$hsl->channel(Saturation::class)->scale($level);
return $hsl->toColorspace($this->colorspace());
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withInversion()
*
* @throws ColorException
*/
public function withInversion(): ColorInterface
{
try {
$rgb = $this->toColorspace(RgbColorspace::class);
} catch (InvalidArgumentException) {
throw new ColorException('Failed to invert color');
}
try {
$inverted = new \Intervention\Image\Colors\Rgb\Color(
255 - $rgb->channel(Red::class)->value(),
255 - $rgb->channel(Green::class)->value(),
255 - $rgb->channel(Blue::class)->value(),
$rgb->alpha()->normalized(),
);
return $inverted->toColorspace($this->colorspace());
} catch (InvalidArgumentException) {
throw new ColorException('Failed to invert color');
}
}
/**
* Show debug info for the current color.
*
* @return array<string, string>
*/
public function __debugInfo(): array
{
return array_reduce($this->channels(), function (array $result, ColorChannelInterface $item) {
$key = strtolower((new ReflectionClass($item))->getShortName());
$result[$key] = $item->toString();
return $result;
}, []);
}
/**
* Clone color.
*/
public function __clone(): void
{
foreach ($this->channels as $key => $channel) {
$this->channels[$key] = clone $channel;
}
}
/**
* {@inheritdoc}
*
* @see ColorInterface::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Stringable;
abstract class AbstractColorChannel implements ColorChannelInterface, Stringable
{
/**
* Main color channel value.
*/
protected int|float $value;
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::normalized()
*/
public function normalized(int $precision = 32): float
{
return round(($this->value() - $this->min()) / ($this->max() - $this->min()), $precision);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::scale()
*
* @throws InvalidArgumentException
*/
public function scale(int $percent): self
{
if ($percent === 0) {
return $this;
}
if ($percent < -100 || $percent > 100) {
throw new InvalidArgumentException('Percentage value must be between -100 and 100');
}
$normalized = $this->normalized();
$base = $percent >= 0 ? (1 - $normalized) : $normalized;
$scaled = min(1.0, max(0.0, $normalized + $base / 100 * $percent));
$this->value = static::fromNormalized($scaled)->value();
return $this;
}
/**
* Throw exception if the given value is not applicable for channel
* otherwise the value is returned unchanged.
*
* @throws InvalidArgumentException
*/
protected function validValueOrFail(int|float $value): mixed
{
if ($value < $this->min() || $value > $this->max()) {
throw new InvalidArgumentException(
'Color channel ' . $this::class . ' 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();
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Interfaces\ColorspaceInterface;
abstract class AbstractColorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
protected static array $channels = [];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::channels()
*/
public static function channels(): array
{
return static::$channels;
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\InvalidArgumentException;
class AlphaChannel extends AbstractColorChannel
{
/**
* @throws InvalidArgumentException
*/
final public function __construct(float $value = 1)
{
if ($value < 0 || $value > 1) {
throw new InvalidArgumentException(
'Color channel value of ' . static::class . ' must be in range 0 to 1',
);
}
$this->value = (int) $this->validValueOrFail(intval(round($value * $this->max())));
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::fromNormalized()
*
* @throws InvalidArgumentException
*/
public static function fromNormalized(float $normalized): self
{
return new static($normalized);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::value()
*/
public function value(): int
{
return $this->value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 255;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::toString()
*/
public function toString(): string
{
return strval($this->normalized(2));
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
use Intervention\Image\Colors\AlphaChannel;
class Alpha extends AlphaChannel
{
//
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Cyan extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 100;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
class Key extends Cyan
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
class Magenta extends Cyan
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
class Yellow extends Cyan
{
//
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Cmyk\Channels\Alpha;
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\Decoders\StringColorDecoder;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new instance.
*
* @throws InvalidArgumentException
*/
public function __construct(int|Cyan $c, int|Magenta $m, int|Yellow $y, int|Key $k, float|Alpha $a = 1)
{
$this->channels = [
is_int($c) ? new Cyan($c) : $c,
is_int($m) ? new Magenta($m) : $m,
is_int($y) ? new Yellow($y) : $y,
is_int($k) ? new Key($k) : $k,
is_float($a) ? new Alpha($a) : $a,
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*
* @throws InvalidArgumentException
*/
public static function create(int|Cyan $c, int|Magenta $m, int|Yellow $y, int|Key $k, float|Alpha $a = 1): self
{
return new self($c, $m, $y, $k, $a);
}
/**
* Parse CMYK color from string.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): self
{
try {
$color = InputHandler::usingDecoders([
StringColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse CMYK color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof self) {
throw new ColorException('Result must be instance of ' . self::class);
}
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(bool $prefix = false): string
{
// @phpstan-ignore missingType.checkedException
return $this->toColorspace(Rgb::class)->toHex($prefix);
}
/**
* Return the CMYK cyan channel.
*/
public function cyan(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Cyan::class);
}
/**
* Return the CMYK magenta channel.
*/
public function magenta(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Magenta::class);
}
/**
* Return the CMYK yellow channel.
*/
public function yellow(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Yellow::class);
}
/**
* Return the CMYK key channel.
*/
public function key(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Key::class);
}
/**
* Return the CMYK alpha channel.
*/
public function alpha(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Alpha::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
if ($this->isTransparent()) {
return sprintf(
'cmyk(%d %d %d %d / %s)',
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
$this->key()->value(),
$this->alpha()->toString(),
);
}
return sprintf(
'cmyk(%d %d %d %d)',
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
$this->key()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGrayscale()
*/
public function isGrayscale(): bool
{
return 0 === array_sum([
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
]);
}
}

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk;
use Intervention\Image\Colors\AbstractColorspace;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Colors\Rgb\NamedColor;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use TypeError;
class Colorspace extends AbstractColorspace
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
public static array $channels = [
Channels\Cyan::class,
Channels\Magenta::class,
Channels\Yellow::class,
Channels\Key::class,
Channels\Alpha::class,
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*
* @throws InvalidArgumentException
*/
public static function colorFromNormalized(array $normalized): CmykColor
{
if (!in_array(count($normalized), [4, 5])) {
throw new InvalidArgumentException('Number of color channels must be 4 or 5 for ' . static::class);
}
// add alpha value if missing
$normalized = count($normalized) === 4 ? array_pad($normalized, 5, 1) : $normalized;
return new Color(...array_map(
function (string $channel, null|float $normalized) {
try {
return $channel::fromNormalized($normalized);
} catch (TypeError $e) {
throw new InvalidArgumentException(
'Normalized color value must be in range 0 to 1',
previous: $e
);
}
},
self::$channels,
$normalized
));
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*
* @throws ColorException
*/
public function importColor(ColorInterface $color): CmykColor
{
return match ($color::class) {
OklchColor::class,
OklabColor::class,
HsvColor::class,
NamedColor::class,
HslColor::class => $this->importViaRgbColor($color),
RgbColor::class => $this->importRgbColor($color),
CmykColor::class => $color,
default => throw new ColorException(
'Unable to import color ' . $color::class . ' to ' . $this::class,
),
};
}
/**
* Import given RGB color to CMYK colorspace.
*
* @throws ColorException
*/
private function importRgbColor(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));
try {
return new CmykColor($c, $m, $y, $k, $color->alpha()->normalized());
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given color to CMYK colorspace by converting it to RGB first.
*
* @throws ColorException
*/
private function importViaRgbColor(NamedColor|OklabColor|OklchColor|HslColor|HsvColor $color): CmykColor
{
try {
$color = $color->toColorspace(RgbColorspace::class);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
if (!$color instanceof RgbColor) {
throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class);
}
return $this->importRgbColor($color);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Decoders;
use Intervention\Image\Colors\Cmyk\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
private const string PATTERN =
'/^cmyk ?\(' .
'(?P<c>[0-9\.]+%?)((, ?)| )' .
'(?P<m>[0-9\.]+%?)((, ?)| )' .
'(?P<y>[0-9\.]+%?)((, ?)| )' .
'(?P<k>[0-9\.]+%?)\)$/i';
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (!str_starts_with(strtolower($input), 'cmyk')) {
return false;
}
return true;
}
/**
* Decode CMYK color strings
*
* @throws InvalidArgumentException
*/
public function decode(mixed $input): ColorInterface
{
if (preg_match(self::PATTERN, (string) $input, $matches) !== 1) {
throw new InvalidArgumentException('Invalid cmyk() color syntax "' . $input . '"');
}
$values = array_map(function (string $value): int {
return intval(round(floatval(trim(str_replace('%', '', $value)))));
}, [$matches['c'], $matches['m'], $matches['y'], $matches['k']]);
return new Color(...$values);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\InvalidArgumentException;
abstract class FloatColorChannel extends AbstractColorChannel
{
/**
* @throws InvalidArgumentException
*/
final public function __construct(float $value)
{
$this->value = (float) $this->validValueOrFail($value);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::fromNormalized()
*
* @throws InvalidArgumentException
*/
public static function fromNormalized(float $normalized): self
{
if ($normalized < 0 || $normalized > 1) {
throw new InvalidArgumentException(
'Normalized color channel value must be between 0 to 1',
);
}
return new static(static::min() + $normalized * (static::max() - static::min()));
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::value()
*/
public function value(): float
{
return (float) $this->value;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\AlphaChannel;
class Alpha extends AlphaChannel
{
//
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Hue extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 360;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Luminance extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 100;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Saturation extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 100;
}
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Hsl\Channels\Alpha;
use Intervention\Image\Colors\Hsl\Channels\Hue;
use Intervention\Image\Colors\Hsl\Channels\Luminance;
use Intervention\Image\Colors\Hsl\Channels\Saturation;
use Intervention\Image\Colors\Hsl\Decoders\StringColorDecoder;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object.
*
* @throws InvalidArgumentException
*/
public function __construct(int|Hue $h, int|Saturation $s, int|Luminance $l, float|Alpha $a = 1)
{
$this->channels = [
is_int($h) ? new Hue($h) : $h,
is_int($s) ? new Saturation($s) : $s,
is_int($l) ? new Luminance($l) : $l,
is_float($a) ? new Alpha($a) : $a,
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*
* @throws InvalidArgumentException
*/
public static function create(int|Hue $h, int|Saturation $s, int|Luminance $l, float|Alpha $a = 1): self
{
return new self($h, $s, $l, $a);
}
/**
* Parse HSL color from string.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): self
{
try {
$color = InputHandler::usingDecoders([
StringColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse HSL color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof self) {
throw new ColorException('Result must be instance of ' . self::class);
}
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* Return the Hue channel
*/
public function hue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel.
*/
public function saturation(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Saturation::class);
}
/**
* Return the Luminance channel.
*/
public function luminance(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Luminance::class);
}
/**
* Return the alpha channel.
*/
public function alpha(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Alpha::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*
* @throws NotSupportedException
*/
public function toHex(bool $prefix = false): string
{
// @phpstan-ignore missingType.checkedException
return $this->toColorspace(Rgb::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
if ($this->isTransparent()) {
return sprintf(
'hsl(%d %d%% %d%% / %s)',
$this->hue()->value(),
$this->saturation()->value(),
$this->luminance()->value(),
$this->alpha()->toString(),
);
}
return sprintf(
'hsl(%d %d%% %d%%)',
$this->hue()->value(),
$this->saturation()->value(),
$this->luminance()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGrayscale()
*/
public function isGrayscale(): bool
{
return floatval($this->saturation()->value()) === 0.0;
}
}

View File

@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl;
use Intervention\Image\Colors\AbstractColorspace;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Colors\Rgb\NamedColor;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use TypeError;
class Colorspace extends AbstractColorspace
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
public static array $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Luminance::class,
Channels\Alpha::class,
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*
* @throws InvalidArgumentException
*/
public static function colorFromNormalized(array $normalized): HslColor
{
if (!in_array(count($normalized), [3, 4])) {
throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class);
}
// add alpha value if missing
$normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized;
return new Color(...array_map(
function (string $channel, null|float $normalized) {
try {
return $channel::fromNormalized($normalized);
} catch (TypeError $e) {
throw new InvalidArgumentException(
'Normalized color value must be in range 0 to 1',
previous: $e
);
}
},
self::$channels,
$normalized
));
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*
* @throws ColorException
*/
public function importColor(ColorInterface $color): HslColor
{
return match ($color::class) {
HslColor::class => $color,
OklchColor::class,
OklabColor::class,
NamedColor::class,
CmykColor::class => $this->importViaRgbColor($color),
RgbColor::class => $this->importRgbColor($color),
HsvColor::class => $this->importHsvColor($color),
default => throw new ColorException(
'Unable to import color ' . $color::class . ' to ' . $this::class,
),
};
}
/**
* Import given RGB color to HSL colorspace.
*
* @throws ColorException
*/
private function importRgbColor(RgbColor $color): HslColor
{
// normalized values of rgb channels
$values = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalized(),
$color->channels(),
);
// take only RGB
$values = array_slice($values, 0, 3);
// calculate Luminance
$min = min(...$values);
$max = max(...$values);
$luminance = ($max + $min) / 2;
$delta = $max - $min;
// calculate saturation
$saturation = $delta === 0.0 ? 0 : $delta / (1 - abs(2 * $luminance - 1));
// calculate hue
[$r, $g, $b] = $values;
$hue = match (true) {
($delta === 0.0) => 0,
($max === $r) => 60 * fmod((($g - $b) / $delta), 6),
($max === $g) => 60 * ((($b - $r) / $delta) + 2),
($max === $b) => 60 * ((($r - $g) / $delta) + 4),
default => 0,
};
$hue = (round($hue) + 360) % 360; // normalize hue
try {
return new Color(
intval(round($hue)),
intval(round($saturation * 100)),
intval(round($luminance * 100)),
$color->alpha()->normalized(),
);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given HSV color to HSL colorspace.
*
* @throws ColorException
*/
private function importHsvColor(HsvColor $color): HslColor
{
// normalized values of hsv channels
[$h, $s, $v] = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalized(),
$color->channels(),
);
// calculate Luminance
$luminance = (2.0 - $s) * $v / 2.0;
// calculate Saturation
$saturation = match (true) {
$luminance === 0.0 => $s,
$luminance === 1.0 => 0,
$luminance < .5 => $s * $v / ($luminance * 2),
default => $s * $v / (2 - $luminance * 2),
};
try {
return new Color(
intval(round($h * 360)),
intval(round($saturation * 100)),
intval(round($luminance * 100)),
$color->alpha()->normalized(),
);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given color to HSL color space by converting it to RGB first.
*
* @throws ColorException
*/
private function importViaRgbColor(NamedColor|CmykColor|OklabColor|OklchColor $color): HslColor
{
try {
$color = $color->toColorspace(Rgb::class);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
if (!$color instanceof RgbColor) {
throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class);
}
return $this->importRgbColor($color);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Decoders;
use Intervention\Image\Colors\Hsl\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Regex pattern of HSL color syntax
*/
private const string PATTERN =
'/^hsla? ?\( ?' .
'(?P<h>[0-9\.]+)(?:deg)?((, ?)| )' .
'(?P<s>[0-9\.]+%?)((, ?)| )' .
'(?P<l>[0-9\.]+%?)' .
'(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' .
'(?<a>(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' .
' ?\)$/i';
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (!str_starts_with(strtolower($input), 'hsl')) {
return false;
}
return true;
}
/**
* Decode hsl color strings.
*
* @throws InvalidArgumentException
*/
public function decode(mixed $input): ColorInterface
{
if (preg_match(self::PATTERN, $input, $matches) !== 1) {
throw new InvalidArgumentException('Invalid hsl() color syntax "' . $input . '"');
}
$values = array_map(fn(string $value): int => match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(trim(str_replace('%', '', $value))),
}, [$matches['h'], $matches['s'], $matches['l']]);
// alpha value
if (array_key_exists('a', $matches)) {
$values[] = match (strpos($matches['a'], '%')) {
false => floatval(trim($matches['a'])),
default => floatval(trim(str_replace('%', '', $matches['a']))) / 100,
};
}
return new Color(...$values);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\AlphaChannel;
class Alpha extends AlphaChannel
{
//
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Hue extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 360;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Saturation extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 100;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Value extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 100;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Hsv\Channels\Alpha;
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\Decoders\StringColorDecoder;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object.
*
* @throws InvalidArgumentException
*/
public function __construct(int|Hue $h, int|Saturation $s, int|Value $v, float|Alpha $a = 1)
{
$this->channels = [
is_int($h) ? new Hue($h) : $h,
is_int($s) ? new Saturation($s) : $s,
is_int($v) ? new Value($v) : $v,
is_float($a) ? new Alpha($a) : $a,
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*
* @throws InvalidArgumentException
*/
public static function create(int|Hue $h, int|Saturation $s, int|Value $v, float|Alpha $a = 1): self
{
return new self($h, $s, $v, $a);
}
/**
* Parse HSV color from string.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): self
{
try {
$color = InputHandler::usingDecoders([
StringColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse HSV color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof self) {
throw new ColorException('Result must be instance of ' . self::class);
}
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* Return the Hue channel.
*/
public function hue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel.
*/
public function saturation(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Saturation::class);
}
/**
* Return the Value channel.
*/
public function value(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Value::class);
}
/**
* Return alpha channel.
*/
public function alpha(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Alpha::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(bool $prefix = false): string
{
// @phpstan-ignore missingType.checkedException
return $this->toColorspace(Rgb::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
if ($this->isTransparent()) {
return sprintf(
'hsv(%d %d%% %d%% / %s)',
$this->hue()->value(),
$this->saturation()->value(),
$this->value()->value(),
$this->alpha()->toString(),
);
}
return sprintf(
'hsv(%d %d%% %d%%)',
$this->hue()->value(),
$this->saturation()->value(),
$this->value()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGrayscale()
*/
public function isGrayscale(): bool
{
return floatval($this->saturation()->value()) === 0.0;
}
}

View File

@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv;
use Intervention\Image\Colors\AbstractColorspace;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Colors\Rgb\NamedColor;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use TypeError;
class Colorspace extends AbstractColorspace
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
public static array $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Value::class,
Channels\Alpha::class,
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*
* @throws InvalidArgumentException
*/
public static function colorFromNormalized(array $normalized): HsvColor
{
if (!in_array(count($normalized), [3, 4])) {
throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class);
}
// add alpha value if missing
$normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized;
return new Color(...array_map(
function (string $channel, null|float $normalized) {
try {
return $channel::fromNormalized($normalized);
} catch (TypeError $e) {
throw new InvalidArgumentException(
'Normalized color value must be in range 0 to 1',
previous: $e
);
}
},
self::$channels,
$normalized
));
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public function importColor(ColorInterface $color): HsvColor
{
return match ($color::class) {
CmykColor::class,
OklchColor::class,
NamedColor::class,
OklabColor::class => $this->importViaRgbColor($color),
RgbColor::class => $this->importRgbColor($color),
HslColor::class => $this->importHslColor($color),
HsvColor::class => $color,
default => throw new ColorException(
'Unable to import color ' . $color::class . ' to ' . $this::class,
),
};
}
/**
* Import given RGB color to HSV colorspace.
*
* @throws ColorException
*/
private function importRgbColor(RgbColor $color): HsvColor
{
// normalized values of rgb channels
$values = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalized(),
$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;
if ($chroma === 0.0) {
// grayscale color
try {
return new Color(0, 0, intval(round($v)), $color->alpha()->normalized());
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
// calculate saturation
$s = 100 * ($chroma / $max);
// calculate hue
[$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;
try {
return new Color(
intval(round($h)),
intval(round($s)),
intval(round($v)),
$color->alpha()->normalized(),
);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given HSL color to HSV colorspace.
*
* @throws InvalidArgumentException
*/
protected function importHslColor(ColorInterface $color): HsvColor
{
if (!$color instanceof HslColor) {
throw new InvalidArgumentException('Color must be of type ' . HslColor::class);
}
// normalized values of hsl channels
[$h, $s, $l] = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalized(),
$color->channels()
);
$v = $l + $s * min($l, 1 - $l);
$s = ($v === 0.0) ? 0 : 2 * (1 - $l / $v);
return $this->colorFromNormalized([$h, $s, $v, $color->alpha()->normalized()]);
}
/**
* Import given color to HSV color space by converting it to RGB first.
*
* @throws ColorException
*/
private function importViaRgbColor(NamedColor|CmykColor|OklchColor|OklabColor $color): HsvColor
{
try {
$color = $color->toColorspace(RgbColorspace::class);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
if (!$color instanceof RgbColor) {
throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class);
}
return $this->importRgbColor($color);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Decoders;
use Intervention\Image\Colors\Hsv\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Regex pattern of hsv/b color syntax.
*/
private const string PATTERN =
'/^hs(v|b) ?\( ?(' .
'?P<h>[0-9\.]+)(?:deg)?((, ?)| )' .
'(?P<s>[0-9\.]+%?)((, ?)| )' .
'(?P<v>[0-9\.]+%?)' .
'(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' .
'(?<a>(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' .
' ?\)$/i';
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (preg_match('/^hs(v|b)/i', $input) !== 1) {
return false;
}
return true;
}
/**
* Decode hsv/hsb color strings.
*
* @throws InvalidArgumentException
*/
public function decode(mixed $input): ColorInterface
{
if (preg_match(self::PATTERN, $input, $matches) !== 1) {
throw new InvalidArgumentException('Invalid hsv() or hsb() color syntax "' . $input . '"');
}
$values = array_map(fn(string $value): int => match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(trim(str_replace('%', '', $value))),
}, [$matches['h'], $matches['s'], $matches['v']]);
// alpha value
if (array_key_exists('a', $matches)) {
$values[] = match (strpos($matches['a'], '%')) {
false => floatval(trim($matches['a'])),
default => floatval(trim(str_replace('%', '', $matches['a']))) / 100,
};
}
return new Color(...$values);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\InvalidArgumentException;
abstract class IntegerColorChannel extends AbstractColorChannel
{
/**
* @throws InvalidArgumentException
*/
final public function __construct(int $value)
{
$this->value = (int) $this->validValueOrFail($value);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::fromNormalized()
*
* @throws InvalidArgumentException
*/
public static function fromNormalized(float $normalized): self
{
if ($normalized < 0 || $normalized > 1) {
throw new InvalidArgumentException(
'Normalized color channel value must be between 0 to 1',
);
}
return new static(intval(round($normalized * static::max())));
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::value()
*/
public function value(): int
{
return (int) $this->value;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklab\Channels;
use Intervention\Image\Colors\FloatColorChannel;
class A extends FloatColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return -0.4;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 0.4;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklab\Channels;
use Intervention\Image\Colors\AlphaChannel;
class Alpha extends AlphaChannel
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklab\Channels;
class B extends A
{
//
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklab\Channels;
use Intervention\Image\Colors\FloatColorChannel;
class Lightness extends FloatColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 1;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklab;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Oklab\Channels\A;
use Intervention\Image\Colors\Oklab\Channels\B;
use Intervention\Image\Colors\Oklab\Channels\Alpha;
use Intervention\Image\Colors\Oklab\Channels\Lightness;
use Intervention\Image\Colors\Oklab\Decoders\StringColorDecoder;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object.
*
* @throws InvalidArgumentException
*/
public function __construct(float|Lightness $l, float|A $a, float|B $b, float|Alpha $alpha = 1)
{
$this->channels = [
is_float($l) ? new Lightness($l) : $l,
is_float($a) ? new A($a) : $a,
is_float($b) ? new B($b) : $b,
is_float($alpha) ? new Alpha($alpha) : $alpha,
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*
* @throws InvalidArgumentException
*/
public static function create(float|Lightness $l, float|A $a, float|B $b, float|Alpha $alpha = 1): self
{
return new self($l, $a, $b, $alpha);
}
/**
* Parse OKLAB color from string.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): self
{
try {
$color = InputHandler::usingDecoders([
StringColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse OKLAB color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof self) {
throw new ColorException('Result must be instance of ' . self::class);
}
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* Return the Lightness channel.
*/
public function lightness(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Lightness::class);
}
/**
* Return the a axis (green-red) channel.
*/
public function a(): ColorChannelInterface
{
/** @throws void */
return $this->channel(A::class);
}
/**
* Return the b axis (blue-yellow) channel.
*/
public function b(): ColorChannelInterface
{
/** @throws void */
return $this->channel(B::class);
}
/**
* Return alpha channel.
*/
public function alpha(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Alpha::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(bool $prefix = false): string
{
// @phpstan-ignore missingType.checkedException
return $this->toColorspace(Rgb::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
if ($this->isTransparent()) {
return sprintf(
'oklab(%s %s %s / %s)',
$this->lightness()->value(),
$this->a()->value(),
$this->b()->value(),
$this->alpha()->toString(),
);
}
return sprintf(
'oklab(%s %s %s)',
$this->lightness()->value(),
$this->a()->value(),
$this->b()->value(),
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGrayscale()
*/
public function isGrayscale(): bool
{
return $this->a()->value() === 0.0 && $this->b()->value() === 0.0;
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklab;
use Intervention\Image\Colors\AbstractColorspace;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Colors\Rgb\NamedColor;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use TypeError;
class Colorspace extends AbstractColorspace
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
public static array $channels = [
Channels\Lightness::class,
Channels\A::class,
Channels\B::class,
Channels\Alpha::class,
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*
* @throws InvalidArgumentException
*/
public static function colorFromNormalized(array $normalized): OklabColor
{
if (!in_array(count($normalized), [3, 4])) {
throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class);
}
// add alpha value if missing
$normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized;
return new Color(...array_map(
function (string $channel, null|float $normalized) {
try {
return $channel::fromNormalized($normalized);
} catch (TypeError $e) {
throw new InvalidArgumentException(
'Normalized color value must be in range 0 to 1',
previous: $e
);
}
},
self::$channels,
$normalized
));
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*
* @throws ColorException
*/
public function importColor(ColorInterface $color): OklabColor
{
return match ($color::class) {
CmykColor::class,
HsvColor::class,
NamedColor::class,
HslColor::class => $this->importViaRgbColor($color),
RgbColor::class => $this->importRgbColor($color),
OklchColor::class => $this->importOklchColor($color),
OklabColor::class => $color,
default => throw new ColorException(
'Unable to import color ' . $color::class . ' to ' . $this::class,
),
};
}
/**
* Import given RGB color OKLAB colorspace.
*
* @throws ColorException
*/
private function importRgbColor(RgbColor $color): OklabColor
{
$cbrt = fn(float $x): float => $x < 0 ? -abs($x) ** (1 / 3) : $x ** (1 / 3);
$rgbToLinear = fn(float $x): float => $x <= 0.04045 ? $x / 12.92 : (($x + 0.055) / 1.055) ** 2.4;
$r = $color->red()->normalized();
$g = $color->green()->normalized();
$b = $color->blue()->normalized();
$r = $rgbToLinear($r);
$g = $rgbToLinear($g);
$b = $rgbToLinear($b);
$l = 0.4122214708 * $r + 0.5363325363 * $g + 0.0514459929 * $b;
$m = 0.2119034982 * $r + 0.6806995451 * $g + 0.1073969566 * $b;
$s = 0.0883024619 * $r + 0.2817188376 * $g + 0.6299787005 * $b;
$l = $cbrt($l);
$m = $cbrt($m);
$s = $cbrt($s);
try {
return new Color(
0.2104542553 * $l + 0.7936177850 * $m - 0.0040720468 * $s,
1.9779984951 * $l - 2.4285922050 * $m + 0.4505937099 * $s,
0.0259040371 * $l + 0.7827717662 * $m - 0.8086757660 * $s,
$color->alpha()->normalized(),
);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given OKLCH color OKLAB colorspace.
*
* @throws ColorException
*/
private function importOklchColor(OklchColor $color): OklabColor
{
$hRad = deg2rad($color->hue()->value());
try {
return new Color(
$color->lightness()->value(),
$color->chroma()->value() * cos($hRad),
$color->chroma()->value() * sin($hRad),
$color->alpha()->normalized(),
);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given color to OKLAB color space by converting it to RGB first.
*
* @throws ColorException
*/
private function importViaRgbColor(NamedColor|CmykColor|HslColor|HsvColor $color): OklabColor
{
try {
$color = $color->toColorspace(Rgb::class)->toColorspace($this::class);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
if (!$color instanceof OklabColor) {
throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class);
}
return $color;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklab\Decoders;
use Intervention\Image\Colors\Oklab\Color;
use Intervention\Image\Colors\Oklab\Channels\Lightness;
use Intervention\Image\Colors\Oklab\Channels\A;
use Intervention\Image\Colors\Oklab\Channels\B;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Regex pattern of oklab color syntax.
*/
private const string PATTERN =
'/^oklab ?\( ?' .
'(?P<l>(1|0|0?\.[0-9]+)|[0-9\.]+%)((, ?)|( ))' .
'(?P<a>(-?0|-?0?\.[0-9\.]+)|(-?[0-9\.]+%))((, ?)|( ))' .
'(?P<b>(-?0|-?0?\.[0-9\.]+)|(-?[0-9\.]+%))' .
'(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' .
'(?<alpha>(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' .
' ?\)$/i';
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (!str_starts_with(strtolower($input), 'oklab')) {
return false;
}
return true;
}
/**
* Decode hsl color strings.
*
* @throws InvalidArgumentException
*/
public function decode(mixed $input): ColorInterface
{
if (preg_match(self::PATTERN, $input, $matches) !== 1) {
throw new InvalidArgumentException('Invalid oklab() color syntax "' . $input . '"');
}
$values = [
$this->decodeChannelValue($matches['l'], Lightness::class),
$this->decodeChannelValue($matches['a'], A::class),
$this->decodeChannelValue($matches['b'], B::class),
];
// alpha value
if (array_key_exists('alpha', $matches)) {
$values[] = $this->decodeAlphaChannelValue($matches['alpha']);
}
return new Color(...$values);
}
/**
* Decode channel value.
*/
private function decodeChannelValue(string $value, string $channel): float
{
if (strpos($value, '%')) {
return floatval(trim(str_replace('%', '', $value))) * $channel::max() / 100;
}
return floatval(trim($value));
}
private function decodeAlphaChannelValue(string $value): float
{
if (strpos($value, '%')) {
return floatval(trim(str_replace('%', '', $value))) / 100;
}
return floatval(trim($value));
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklch\Channels;
use Intervention\Image\Colors\AlphaChannel;
class Alpha extends AlphaChannel
{
//
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklch\Channels;
use Intervention\Image\Colors\FloatColorChannel;
class Chroma extends FloatColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return -0.4;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 0.4;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklch\Channels;
use Intervention\Image\Colors\FloatColorChannel;
class Hue extends FloatColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 360;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklch\Channels;
use Intervention\Image\Colors\FloatColorChannel;
class Lightness extends FloatColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 1;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklch;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Oklch\Channels\Alpha;
use Intervention\Image\Colors\Oklch\Channels\Chroma;
use Intervention\Image\Colors\Oklch\Channels\Hue;
use Intervention\Image\Colors\Oklch\Channels\Lightness;
use Intervention\Image\Colors\Oklch\Decoders\StringColorDecoder;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object.
*
* @throws InvalidArgumentException
*/
public function __construct(float|Lightness $l, float|Chroma $c, float|Hue $h, float|Alpha $a = 1)
{
$this->channels = [
is_float($l) ? new Lightness($l) : $l,
is_float($c) ? new Chroma($c) : $c,
is_float($h) ? new Hue($h) : $h,
is_float($a) ? new Alpha($a) : $a,
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*
* @throws InvalidArgumentException
*/
public static function create(float|Lightness $l, float|Chroma $c, float|Hue $h, float|Alpha $a = 1): self
{
return new self($l, $c, $h, $a);
}
/**
* Parse OKLCH color from string.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): self
{
try {
$color = InputHandler::usingDecoders([
StringColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse OKLCH color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof self) {
throw new ColorException('Result must be instance of ' . self::class);
}
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* Return the Lightness channel.
*/
public function lightness(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Lightness::class);
}
/**
* Return the chroma channel.
*/
public function chroma(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Chroma::class);
}
/**
* Return the hue channel.
*/
public function hue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Hue::class);
}
/**
* Return the alpha channel.
*/
public function alpha(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Alpha::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(bool $prefix = false): string
{
// @phpstan-ignore missingType.checkedException
return $this->toColorspace(Rgb::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
if ($this->isTransparent()) {
return sprintf(
'oklch(%s %s %s / %s)',
$this->lightness()->value(),
$this->chroma()->value(),
$this->hue()->value(),
$this->alpha()->toString(),
);
}
return sprintf(
'oklch(%s %s %s)',
$this->lightness()->value(),
$this->chroma()->value(),
$this->hue()->value(),
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGrayscale()
*/
public function isGrayscale(): bool
{
return $this->chroma()->value() === 0.0;
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklch;
use Intervention\Image\Colors\AbstractColorspace;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklch\Channels\Alpha;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklab\Colorspace as Oklab;
use Intervention\Image\Colors\Oklch\Channels\Chroma;
use Intervention\Image\Colors\Oklch\Channels\Hue;
use Intervention\Image\Colors\Oklch\Channels\Lightness;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Colors\Rgb\NamedColor;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use TypeError;
class Colorspace extends AbstractColorspace
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
public static array $channels = [
Lightness::class,
Chroma::class,
Hue::class,
Alpha::class,
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*
* @throws InvalidArgumentException
*/
public static function colorFromNormalized(array $normalized): OklchColor
{
if (!in_array(count($normalized), [3, 4])) {
throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class);
}
// add alpha value if missing
$normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized;
return new Color(...array_map(
function (string $channel, null|float $normalized) {
try {
return $channel::fromNormalized($normalized);
} catch (TypeError $e) {
throw new InvalidArgumentException(
'Normalized color value must be in range 0 to 1',
previous: $e
);
}
},
self::$channels,
$normalized
));
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*
* @throws ColorException
*/
public function importColor(ColorInterface $color): OklchColor
{
return match ($color::class) {
CmykColor::class,
HsvColor::class,
NamedColor::class,
HslColor::class => $this->importViaRgbColor($color),
OklabColor::class => $this->importOklabColor($color),
RgbColor::class => $this->importRgbColor($color),
OklchColor::class => $color,
default => throw new ColorException(
'Unable to import color ' . $color::class . ' to ' . $this::class,
),
};
}
/**
* Import given OKLAB color OKLCH colorspace.
*
* @throws ColorException
*/
private function importOklabColor(OklabColor $color): OklchColor
{
$a = $color->a()->value();
$b = $color->b()->value();
$c = sqrt($a * $a + $b * $b);
$h = rad2deg(atan2($b, $a));
$h = $h < 0 ? $h + 360 : $h;
try {
return new Color($color->lightness()->value(), $c, $h, $color->alpha()->normalized());
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given RGB color to OKLCH color space.
*
* @throws ColorException
*/
private function importRgbColor(RgbColor $color): OklchColor
{
try {
$color = $color->toColorspace(Oklab::class);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
if (!$color instanceof OklabColor) {
throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class);
}
return $this->importOklabColor($color);
}
/**
* Import given color to OKLCH color space by converting it to RGB first.
*
* @throws ColorException
*/
private function importViaRgbColor(NamedColor|HslColor|HsvColor|CmykColor $color): OklchColor
{
try {
$color = $color->toColorspace(Rgb::class)->toColorspace($this::class);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
if (!$color instanceof OklchColor) {
throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class);
}
return $color;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Oklch\Decoders;
use Intervention\Image\Colors\Oklch\Channels\Chroma;
use Intervention\Image\Colors\Oklch\Channels\Hue;
use Intervention\Image\Colors\Oklch\Channels\Lightness;
use Intervention\Image\Colors\Oklch\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Regex pattern for oklch color syntax.
*/
protected const string PATTERN =
'/^oklch ?\( ?' .
'(?P<l>(1|0|0?\.[0-9]+)|[0-9\.]+%)((, ?)|( ))' .
'(?P<c>(-?0|-?0?\.[0-9\.]+)|(-?[0-9\.]+%))((, ?)|( ))' .
'(?P<h>[0-9\.]+)' .
'(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' .
'(?<a>(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' .
' ?\)$/i';
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (!str_starts_with(strtolower($input), 'oklch')) {
return false;
}
return true;
}
/**
* Decode hsl color string.
*
* @throws InvalidArgumentException
*/
public function decode(mixed $input): ColorInterface
{
if (preg_match(self::PATTERN, $input, $matches) !== 1) {
throw new InvalidArgumentException('Invalid oklch() color syntax "' . $input . '"');
}
$values = [
$this->decodeChannelValue($matches['l'], Lightness::class),
$this->decodeChannelValue($matches['c'], Chroma::class),
$this->decodeChannelValue($matches['h'], Hue::class),
];
// alpha value
if (array_key_exists('a', $matches)) {
$values[] = $this->decodeAlphaChannelValue($matches['a']);
}
return new Color(...$values);
}
/**
* Decode channel value.
*/
private function decodeChannelValue(string $value, string $channel): float
{
if (strpos($value, '%')) {
return floatval(trim(str_replace('%', '', $value))) * $channel::max() / 100;
}
return floatval(trim($value));
}
private function decodeAlphaChannelValue(string $value): float
{
if (strpos($value, '%')) {
return floatval(trim(str_replace('%', '', $value))) / 100;
}
return floatval(trim($value));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\File;
use Intervention\Image\Interfaces\ProfileInterface;
class Profile extends File implements ProfileInterface
{
/**
* Create color profile instance from given path in file system.
*/
public static function fromPath(string $path): self
{
$stream = fopen(self::readableFilePathOrFail($path), 'r');
return new self($stream);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Channels;
use Intervention\Image\Colors\AlphaChannel;
class Alpha extends AlphaChannel
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Channels;
class Blue extends Red
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Channels;
class Green extends Red
{
//
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Red extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 255;
}
}

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Rgb\Channels\Alpha;
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\Decoders\HexColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\NamedColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\StringColorDecoder;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new instance.
*
* @throws InvalidArgumentException
*/
public function __construct(int|Red $r, int|Green $g, int|Blue $b, float|Alpha $a = 1)
{
$this->channels = [
is_int($r) ? new Red($r) : $r,
is_int($g) ? new Green($g) : $g,
is_int($b) ? new Blue($b) : $b,
is_float($a) ? new Alpha($a) : $a,
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*
* @throws InvalidArgumentException
*/
public static function create(int|Red $r, int|Green $g, int|Blue $b, float|Alpha $a = 1): self
{
return new self($r, $g, $b, $a);
}
/**
* Parse RGB color from string.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): self
{
try {
$color = InputHandler::usingDecoders([
StringColorDecoder::class,
NamedColorDecoder::class,
HexColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse RGB color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof self) {
throw new ColorException('Result must be instance of ' . self::class);
}
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* Return the RGB red color channel.
*/
public function red(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Red::class);
}
/**
* Return the RGB green color channel.
*/
public function green(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Green::class);
}
/**
* Return the RGB blue color channel.
*/
public function blue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Blue::class);
}
/**
* Return the colors alpha channel.
*/
public function alpha(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Alpha::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(bool $prefix = false): string
{
if ($this->isTransparent()) {
return sprintf(
'%s%02x%02x%02x%02x',
$prefix ? '#' : '',
$this->red()->value(),
$this->green()->value(),
$this->blue()->value(),
$this->alpha()->value()
);
}
return sprintf(
'%s%02x%02x%02x',
$prefix ? '#' : '',
$this->red()->value(),
$this->green()->value(),
$this->blue()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
if ($this->isTransparent()) {
return sprintf(
'rgb(%d %d %d / %s)',
$this->red()->value(),
$this->green()->value(),
$this->blue()->value(),
$this->alpha()->toString(),
);
}
return sprintf(
'rgb(%d %d %d)',
$this->red()->value(),
$this->green()->value(),
$this->blue()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGrayscale()
*/
public function isGrayscale(): bool
{
$values = [$this->red()->value(), $this->green()->value(), $this->blue()->value()];
return count(array_unique($values, SORT_REGULAR)) === 1;
}
}

View File

@@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb;
use Intervention\Image\Colors\AbstractColorspace;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklab\Colorspace as Oklab;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Decoders\HexColorDecoder;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use TypeError;
class Colorspace extends AbstractColorspace
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
public static array $channels = [
Channels\Red::class,
Channels\Green::class,
Channels\Blue::class,
Channels\Alpha::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*
* @throws InvalidArgumentException
*/
public static function colorFromNormalized(array $normalized): RgbColor
{
if (!in_array(count($normalized), [3, 4])) {
throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class);
}
// add alpha value if missing
$normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized;
return new Color(...array_map(
function (string $channel, null|float $normalized) {
try {
return $channel::fromNormalized($normalized);
} catch (TypeError $e) {
throw new InvalidArgumentException(
'Normalized color value must be in range 0 to 1',
previous: $e
);
}
},
self::$channels,
$normalized
));
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*
* @throws ColorException
*/
public function importColor(ColorInterface $color): RgbColor
{
return match ($color::class) {
CmykColor::class => $this->importCmykColor($color),
HsvColor::class => $this->importHsvColor($color),
HslColor::class => $this->importHslColor($color),
OklabColor::class => $this->importOklabColor($color),
OklchColor::class => $this->importOklchColor($color),
NamedColor::class => $this->importNamedColor($color),
RgbColor::class => $color,
default => throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
),
};
}
/**
* @throws ColorException
*/
private function importCmykColor(CmykColor $color): RgbColor
{
try {
return new Color(
(int) (255 * (1 - $color->cyan()->normalized()) * (1 - $color->key()->normalized())),
(int) (255 * (1 - $color->magenta()->normalized()) * (1 - $color->key()->normalized())),
(int) (255 * (1 - $color->yellow()->normalized()) * (1 - $color->key()->normalized())),
$color->alpha()->normalized(),
);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given HSV color to RGB color space.
*
* @throws ColorException
*/
private function importHsvColor(HsvColor $color): RgbColor
{
$chroma = $color->value()->normalized() * $color->saturation()->normalized();
$hue = $color->hue()->normalized() * 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(
fn(float|int $value): float => max(0.0, min(1.0, $value + $color->value()->normalized() - $chroma)),
$values,
);
$values[] = $color->alpha()->normalized(); // append alpha channel value
try {
return $this->colorFromNormalized($values);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given HSL color to RGB color space.
*
* @throws ColorException
*/
private function importHslColor(HslColor $color): RgbColor
{
// normalized values of hsl channels
[$h, $s, $l] = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalized(),
$color->channels()
);
$c = (1 - abs(2 * $l - 1)) * $s;
$x = $c * (1 - abs(fmod($h * 6, 2) - 1));
$m = $l - $c / 2;
$values = match (true) {
$h < 1 / 6 => [$c, $x, 0],
$h < 2 / 6 => [$x, $c, 0],
$h < 3 / 6 => [0, $c, $x],
$h < 4 / 6 => [0, $x, $c],
$h < 5 / 6 => [$x, 0, $c],
default => [$c, 0, $x],
};
$values = array_map(fn(float|int $value): float => max(0.0, min(1.0, $value + $m)), $values);
$values[] = $color->alpha()->normalized(); // append alpha channel value
try {
$color = $this->colorFromNormalized($values);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
return $color;
}
/**
* Import given OKLAB color to RGB color space.
*
* @throws ColorException
*/
private function importOklabColor(OklabColor $color): RgbColor
{
$linearToRgb = function (float $c): float {
$c = max(0.0, min(1.0, $c));
if ($c <= 0.0031308) {
return 12.92 * $c;
}
return 1.055 * ($c ** (1 / 2.4)) - 0.055;
};
$l = $color->lightness()->value() + 0.3963377774 * $color->a()->value() + 0.2158037573 * $color->b()->value();
$m = $color->lightness()->value() - 0.1055613458 * $color->a()->value() - 0.0638541728 * $color->b()->value();
$s = $color->lightness()->value() - 0.0894841775 * $color->a()->value() - 1.2914855480 * $color->b()->value();
$l = $l ** 3;
$m = $m ** 3;
$s = $s ** 3;
$r = +4.0767416621 * $l - 3.3077115913 * $m + 0.2309699292 * $s;
$g = -1.2684380046 * $l + 2.6097574011 * $m - 0.3413193965 * $s;
$b = -0.0041960863 * $l - 0.7034186147 * $m + 1.7076147010 * $s;
$r = $linearToRgb($r);
$g = $linearToRgb($g);
$b = $linearToRgb($b);
try {
return new Color(
(int) round($r * 255),
(int) round($g * 255),
(int) round($b * 255),
$color->alpha()->normalized(),
);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
}
/**
* Import given OKLCH color to RGB color space.
*
* @throws ColorException
*/
private function importOklchColor(OklchColor $color): RgbColor
{
try {
$color = $color->toColorspace(Oklab::class);
} catch (InvalidArgumentException $e) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
previous: $e,
);
}
if (!$color instanceof OklabColor) {
throw new ColorException(
'Failed to import color ' . $color::class . ' to ' . $this::class,
);
}
return $this->importOklabColor($color);
}
/**
* Import given named color to RGB color space.
*
* @throws ColorException
*/
private function importNamedColor(NamedColor $color): RgbColor
{
try {
$output = InputHandler::usingDecoders([
HexColorDecoder::class,
])->handle($color->toHex());
} catch (InvalidArgumentException | NotSupportedException | DriverException $e) {
throw new ColorException('Failed to import named color to rgb color space', previous: $e);
}
return $output instanceof RgbColor
? $output
: throw new ColorException('Failed to import named color to rgb color space');
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class HexColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Regex pattern of hexadecimal color syntax.
*/
protected const string PATTERN = '/^#?(?P<hex>[a-f\d]{3}(?:[a-f\d]?|(?:[a-f\d]{3}(?:[a-f\d]{2})?)?)\b)$/i';
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (str_starts_with($input, '#')) {
return true;
}
// matching max. length & only hexadecimal
if (strlen($input) <= 8 && preg_match('/^[a-f\d]+$/i', $input) === 1) {
return true;
}
return preg_match(static::PATTERN, $input) === 1;
}
/**
* Decode hexadecimal rgb colors with and without transparency.
*
* @throws InvalidArgumentException
* @throws ColorDecoderException
*/
public function decode(mixed $input): ColorInterface
{
if (preg_match(static::PATTERN, $input, $matches) !== 1) {
throw new InvalidArgumentException('Hex color has an invalid format');
}
// split into hex chunks
$values = match (strlen($matches['hex'])) {
3, 4 => str_split($matches['hex']),
6, 8 => str_split($matches['hex'], 2),
default => throw new InvalidArgumentException('Hex color has an incorrect length'),
};
// convert to decimal
$values = array_map(function (string $value): int {
return match (strlen($value)) {
1 => (int) hexdec($value . $value),
2 => (int) hexdec($value),
default => throw new ColorDecoderException('Failed to decode hex color'),
};
}, $values);
// normalize
$values = count($values) === 3 ? array_pad($values, 4, 255) : $values;
$values = array_map(fn(int $value): float => $value / 255, $values);
return Rgb::colorFromNormalized($values);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Colors\Rgb\NamedColor;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class NamedColorDecoder extends HexColorDecoder implements DecoderInterface
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
return $this->stringToColorName($input) === null ? false : true;
}
/**
* Decode html color names.
*/
public function decode(mixed $input): ColorInterface
{
return parent::decode($this->stringToColorName($input)?->toHex());
}
private function stringToColorName(string $input): ?NamedColor
{
return NamedColor::tryFrom(strtolower($input));
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Regex pattern of rgb color syntax.
*/
private const string PATTERN =
'/^s?rgba? ?\( ?' .
'(?P<r>[0-9]{1,3})([, ]) ?' .
'(?P<g>[0-9]{1,3})\2 ?' .
'(?<b>[0-9]{1,3})' .
'(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' .
'(?<a>(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' .
' ?\)$/i';
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (preg_match('/^s?rgb/i', $input) !== 1) {
return false;
}
return true;
}
/**
* Decode rgb color strings.
*
* @throws InvalidArgumentException
* @throws ColorDecoderException
*/
public function decode(mixed $input): ColorInterface
{
if (preg_match(self::PATTERN, $input, $matches) !== 1) {
throw new InvalidArgumentException('Invalid rgb() color syntax "' . $input . '"');
}
// rgb values
$values = array_map(fn(string $value): int => match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(round(floatval(trim(str_replace('%', '', $value))) / 100 * 255)),
}, [$matches['r'], $matches['g'], $matches['b']]);
// alpha value
if (array_key_exists('a', $matches)) {
$values[] = match (strpos($matches['a'], '%')) {
false => floatval(trim($matches['a'])),
default => floatval(trim(str_replace('%', '', $matches['a']))) / 100,
};
}
try {
return new Color(...$values);
} catch (InvalidArgumentException $e) {
throw new ColorDecoderException('Failed to decode RGB color string', previous: $e);
}
}
}

View File

@@ -0,0 +1,502 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb;
use Error;
use Intervention\Image\Colors\Rgb\Channels\Alpha;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use TypeError;
use ValueError;
enum NamedColor: string implements ColorInterface
{
case ALICEBLUE = 'aliceblue';
case ANTIQUEWHITE = 'antiquewhite';
case AQUA = 'aqua';
case AQUAMARINE = 'aquamarine';
case AZURE = 'azure';
case BEIGE = 'beige';
case BISQUE = 'bisque';
case BLACK = 'black';
case BLANCHEDALMOND = 'blanchedalmond';
case BLUE = 'blue';
case BLUEVIOLET = 'blueviolet';
case BROWN = 'brown';
case BURLYWOOD = 'burlywood';
case CADETBLUE = 'cadetblue';
case CHARTREUSE = 'chartreuse';
case CHOCOLATE = 'chocolate';
case CORAL = 'coral';
case CORNFLOWERBLUE = 'cornflowerblue';
case CORNSILK = 'cornsilk';
case CRIMSON = 'crimson';
case CYAN = 'cyan';
case DARKBLUE = 'darkblue';
case DARKCYAN = 'darkcyan';
case DARKGRAY = 'darkgray';
case DARKGREEN = 'darkgreen';
case DARKKHAKI = 'darkkhaki';
case DARKMAGENTA = 'darkmagenta';
case DARKOLIVEGREEN = 'darkolivegreen';
case DARKORANGE = 'darkorange';
case DARKORCHID = 'darkorchid';
case DARKRED = 'darkred';
case DARKSALMON = 'darksalmon';
case DARKSEAGREEN = 'darkseagreen';
case DARKSLATEBLUE = 'darkslateblue';
case DARKSLATEGRAY = 'darkslategray';
case DARKTURQUOISE = 'darkturquoise';
case DARKVIOLET = 'darkviolet';
case DEEPPINK = 'deeppink';
case DEEPSKYBLUE = 'deepskyblue';
case DIMGRAY = 'dimgray';
case DODGERBLUE = 'dodgerblue';
case FIREBRICK = 'firebrick';
case FLORALWHITE = 'floralwhite';
case FORESTGREEN = 'forestgreen';
case FUCHSIA = 'fuchsia';
case GAINSBORO = 'gainsboro';
case GHOSTWHITE = 'ghostwhite';
case GOLD = 'gold';
case GOLDENROD = 'goldenrod';
case GRAY = 'gray';
case GREEN = 'green';
case GREENYELLOW = 'greenyellow';
case HONEYDEW = 'honeydew';
case HOTPINK = 'hotpink';
case INDIANRED = 'indianred';
case INDIGO = 'indigo';
case IVORY = 'ivory';
case KHAKI = 'khaki';
case LAVENDER = 'lavender';
case LAVENDERBLUSH = 'lavenderblush';
case LAWNGREEN = 'lawngreen';
case LEMONCHIFFON = 'lemonchiffon';
case LIGHTBLUE = 'lightblue';
case LIGHTCORAL = 'lightcoral';
case LIGHTCYAN = 'lightcyan';
case LIGHTGOLDENRODYELLOW = 'lightgoldenrodyellow';
case LIGHTGRAY = 'lightgray';
case LIGHTGREEN = 'lightgreen';
case LIGHTPINK = 'lightpink';
case LIGHTSALMON = 'lightsalmon';
case LIGHTSEAGREEN = 'lightseagreen';
case LIGHTSKYBLUE = 'lightskyblue';
case LIGHTSLATEGRAY = 'lightslategray';
case LIGHTSTEELBLUE = 'lightsteelblue';
case LIGHTYELLOW = 'lightyellow';
case LIME = 'lime';
case LIMEGREEN = 'limegreen';
case LINEN = 'linen';
case MAGENTA = 'magenta';
case MAROON = 'maroon';
case MEDIUMAQUAMARINE = 'mediumaquamarine';
case MEDIUMBLUE = 'mediumblue';
case MEDIUMORCHID = 'mediumorchid';
case MEDIUMPURPLE = 'mediumpurple';
case MEDIUMSEAGREEN = 'mediumseagreen';
case MEDIUMSLATEBLUE = 'mediumslateblue';
case MEDIUMSPRINGGREEN = 'mediumspringgreen';
case MEDIUMTURQUOISE = 'mediumturquoise';
case MEDIUMVIOLETRED = 'mediumvioletred';
case MIDNIGHTBLUE = 'midnightblue';
case MINTCREAM = 'mintcream';
case MISTYROSE = 'mistyrose';
case MOCCASIN = 'moccasin';
case NAVAJOWHITE = 'navajowhite';
case NAVY = 'navy';
case OLDLACE = 'oldlace';
case OLIVE = 'olive';
case OLIVEDRAB = 'olivedrab';
case ORANGE = 'orange';
case ORANGERED = 'orangered';
case ORCHID = 'orchid';
case PALEGOLDENROD = 'palegoldenrod';
case PALEGREEN = 'palegreen';
case PALETURQUOISE = 'paleturquoise';
case PALEVIOLETRED = 'palevioletred';
case PAPAYAWHIP = 'papayawhip';
case PEACHPUFF = 'peachpuff';
case PERU = 'peru';
case PINK = 'pink';
case PLUM = 'plum';
case POWDERBLUE = 'powderblue';
case PURPLE = 'purple';
case RED = 'red';
case ROSYBROWN = 'rosybrown';
case ROYALBLUE = 'royalblue';
case SADDLEBROWN = 'saddlebrown';
case SALMON = 'salmon';
case SANDYBROWN = 'sandybrown';
case SEAGREEN = 'seagreen';
case SEASHELL = 'seashell';
case SIENNA = 'sienna';
case SILVER = 'silver';
case SKYBLUE = 'skyblue';
case SLATEBLUE = 'slateblue';
case SLATEGRAY = 'slategray';
case SNOW = 'snow';
case SPRINGGREEN = 'springgreen';
case STEELBLUE = 'steelblue';
case TAN = 'tan';
case TEAL = 'teal';
case THISTLE = 'thistle';
case TOMATO = 'tomato';
case TURQUOISE = 'turquoise';
case VIOLET = 'violet';
case WHEAT = 'wheat';
case WHITE = 'white';
case WHITESMOKE = 'whitesmoke';
case YELLOW = 'yellow';
case YELLOWGREEN = 'yellowgreen';
/**
* Create new named color.
*
* @throws InvalidArgumentException
*/
public static function create(string $input): ColorInterface
{
try {
return self::from(strtolower($input));
} catch (TypeError | ValueError) {
throw new InvalidArgumentException('Failed to create color from input "' . $input . '"');
}
}
/**
* Create new named color or return null of creation fails.
*/
public static function tryCreate(string $input): ?ColorInterface
{
try {
return self::from(strtolower($input));
} catch (Error) {
return null;
}
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Rgb();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return $this->value;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(bool $prefix = false): string
{
return ($prefix ? '#' : '') . match ($this) {
self::ALICEBLUE => 'f0f8ff',
self::ANTIQUEWHITE => 'faebd7',
self::AQUA => '00ffff',
self::AQUAMARINE => '7fffd4',
self::AZURE => 'f0ffff',
self::BEIGE => 'f5f5dc',
self::BISQUE => 'ffe4c4',
self::BLACK => '000000',
self::BLANCHEDALMOND => 'ffebcd',
self::BLUE => '0000ff',
self::BLUEVIOLET => '8a2be2',
self::BROWN => 'a52a2a',
self::BURLYWOOD => 'deb887',
self::CADETBLUE => '5f9ea0',
self::CHARTREUSE => '7fff00',
self::CHOCOLATE => 'd2691e',
self::CORAL => 'ff7f50',
self::CORNFLOWERBLUE => '6495ed',
self::CORNSILK => 'fff8dc',
self::CRIMSON => 'dc143c',
self::CYAN => '00ffff',
self::DARKBLUE => '00008b',
self::DARKCYAN => '008b8b',
self::DARKGRAY => 'a9a9a9',
self::DARKGREEN => '006400',
self::DARKKHAKI => 'bdb76b',
self::DARKMAGENTA => '8b008b',
self::DARKOLIVEGREEN => '556b2f',
self::DARKORANGE => 'ff8c00',
self::DARKORCHID => '9932cc',
self::DARKRED => '8b0000',
self::DARKSALMON => 'e9967a',
self::DARKSEAGREEN => '8fbc8f',
self::DARKSLATEBLUE => '483d8b',
self::DARKSLATEGRAY => '2f4f4f',
self::DARKTURQUOISE => '00ced1',
self::DARKVIOLET => '9400d3',
self::DEEPPINK => 'ff1493',
self::DEEPSKYBLUE => '00bfff',
self::DIMGRAY => '696969',
self::DODGERBLUE => '1e90ff',
self::FIREBRICK => 'b22222',
self::FLORALWHITE => 'fffaf0',
self::FORESTGREEN => '228b22',
self::FUCHSIA => 'ff00ff',
self::GAINSBORO => 'dcdcdc',
self::GHOSTWHITE => 'f8f8ff',
self::GOLD => 'ffd700',
self::GOLDENROD => 'daa520',
self::GRAY => '808080',
self::GREEN => '008000',
self::GREENYELLOW => 'adff2f',
self::HONEYDEW => 'f0fff0',
self::HOTPINK => 'ff69b4',
self::INDIANRED => 'cd5c5c',
self::INDIGO => '4b0082',
self::IVORY => 'fffff0',
self::KHAKI => 'f0e68c',
self::LAVENDER => 'e6e6fa',
self::LAVENDERBLUSH => 'fff0f5',
self::LAWNGREEN => '7cfc00',
self::LEMONCHIFFON => 'fffacd',
self::LIGHTBLUE => 'add8e6',
self::LIGHTCORAL => 'f08080',
self::LIGHTCYAN => 'e0ffff',
self::LIGHTGOLDENRODYELLOW => 'fafad2',
self::LIGHTGRAY => 'd3d3d3',
self::LIGHTGREEN => '90ee90',
self::LIGHTPINK => 'ffb6c1',
self::LIGHTSALMON => 'ffa07a',
self::LIGHTSEAGREEN => '20b2aa',
self::LIGHTSKYBLUE => '87cefa',
self::LIGHTSLATEGRAY => '778899',
self::LIGHTSTEELBLUE => 'b0c4de',
self::LIGHTYELLOW => 'ffffe0',
self::LIME => '00ff00',
self::LIMEGREEN => '32cd32',
self::LINEN => 'faf0e6',
self::MAGENTA => 'ff00ff',
self::MAROON => '800000',
self::MEDIUMAQUAMARINE => '66cdaa',
self::MEDIUMBLUE => '0000cd',
self::MEDIUMORCHID => 'ba55d3',
self::MEDIUMPURPLE => '9370db',
self::MEDIUMSEAGREEN => '3cb371',
self::MEDIUMSLATEBLUE => '7b68ee',
self::MEDIUMSPRINGGREEN => '00fa9a',
self::MEDIUMTURQUOISE => '48d1cc',
self::MEDIUMVIOLETRED => 'c71585',
self::MIDNIGHTBLUE => '191970',
self::MINTCREAM => 'f5fffa',
self::MISTYROSE => 'ffe4e1',
self::MOCCASIN => 'ffe4b5',
self::NAVAJOWHITE => 'ffdead',
self::NAVY => '000080',
self::OLDLACE => 'fdf5e6',
self::OLIVE => '808000',
self::OLIVEDRAB => '6b8e23',
self::ORANGE => 'ffa500',
self::ORANGERED => 'ff4500',
self::ORCHID => 'da70d6',
self::PALEGOLDENROD => 'eee8aa',
self::PALEGREEN => '98fb98',
self::PALETURQUOISE => 'afeeee',
self::PALEVIOLETRED => 'db7093',
self::PAPAYAWHIP => 'ffefd5',
self::PEACHPUFF => 'ffdab9',
self::PERU => 'cd853f',
self::PINK => 'ffc0cb',
self::PLUM => 'dda0dd',
self::POWDERBLUE => 'b0e0e6',
self::PURPLE => '800080',
self::RED => 'ff0000',
self::ROSYBROWN => 'bc8f8f',
self::ROYALBLUE => '4169e1',
self::SADDLEBROWN => '8b4513',
self::SALMON => 'fa8072',
self::SANDYBROWN => 'f4a460',
self::SEAGREEN => '2e8b57',
self::SEASHELL => 'fff5ee',
self::SIENNA => 'a0522d',
self::SILVER => 'c0c0c0',
self::SKYBLUE => '87ceeb',
self::SLATEBLUE => '6a5acd',
self::SLATEGRAY => '708090',
self::SNOW => 'fffafa',
self::SPRINGGREEN => '00ff7f',
self::STEELBLUE => '4682b4',
self::TAN => 'd2b48c',
self::TEAL => '008080',
self::THISTLE => 'd8bfd8',
self::TOMATO => 'ff6347',
self::TURQUOISE => '40e0d0',
self::VIOLET => 'ee82ee',
self::WHEAT => 'f5deb3',
self::WHITE => 'ffffff',
self::WHITESMOKE => 'f5f5f5',
self::YELLOW => 'ffff00',
self::YELLOWGREEN => '9acd32',
};
}
/**
* {@inheritdoc}
*
* @see ColorInterface::channels()
*
* @throws ColorException
*/
public function channels(): array
{
return $this->toRgbColor()->channels();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::channel()
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public function channel(string $classname): ColorChannelInterface
{
return $this->toRgbColor()->channel($classname);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::alpha()
*/
public function alpha(): ColorChannelInterface
{
// @phpstan-ignore missingType.checkedException
return new Alpha();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toColorspace()
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public function toColorspace(string|ColorspaceInterface $colorspace): ColorInterface
{
return $this->toRgbColor()->toColorspace($colorspace);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGrayscale()
*
* @throws ColorException
*/
public function isGrayscale(): bool
{
return $this->toRgbColor()->isGrayscale();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withTransparency()
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public function withTransparency(float $transparency): ColorInterface
{
return $this->toRgbColor()->withTransparency($transparency);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withBrightness()
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public function withBrightness(int $level): ColorInterface
{
return $this->toRgbColor()->withBrightness($level);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withSaturation()
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public function withSaturation(int $level): ColorInterface
{
return $this->toRgbColor()->withSaturation($level);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withInversion()
*
* @throws ColorException
*/
public function withInversion(): ColorInterface
{
return $this->toRgbColor()->withInversion();
}
/**
* Convert current named color to rgb color object.
*
* @throws ColorException
*/
private function toRgbColor(): RgbColor
{
$color = $this->colorspace()->importColor($this);
return $color instanceof RgbColor
? $color
: throw new ColorException('Failed to convert named color to rgb object');
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
class Config
{
/**
* Create config object instance.
*/
public function __construct(
public bool $autoOrientation = true,
public bool $decodeAnimation = true,
public string|ColorInterface $backgroundColor = 'ffffff',
public bool $strip = false,
) {
//
}
/**
* Set values of given config options.
*
* @throws InvalidArgumentException
*/
public function setOptions(mixed ...$options): self
{
foreach ($this->prepareOptions($options) as $name => $value) {
if (!property_exists($this, $name)) {
throw new InvalidArgumentException('Property ' . $name . ' does not exists for ' . $this::class);
}
$this->{$name} = $value;
}
return $this;
}
/**
* This method makes it possible to call self::setOptions() with a single
* array instead of named parameters.
*
* @param array<mixed> $options
* @return array<string, mixed>
*/
private function prepareOptions(array $options): array
{
if ($options === []) {
return $options;
}
if (count($options) > 1) {
return $options;
}
if (!array_key_exists(0, $options)) {
return $options;
}
if (!is_array($options[0])) {
return $options;
}
return $options[0];
}
}

View File

@@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\DataUriInterface;
use Stringable;
class DataUri implements DataUriInterface
{
/**
* Pattern of data uri scheme.
*/
protected const string PATTERN = "/^data:(?P<mediaType>\w+\/[-+.\w]+)?" .
"(?P<parameters>(;[-\w]+=[-\w]+)*)(?P<base64>;base64)?,(?P<data>.*)/";
/**
* Media type of data uri output.
*/
protected ?string $mediaType = null;
/**
* Parameters of data uri output.
*
* @var array<string, string>
*/
protected array $parameters = [];
/**
* Create new data uri instance.
*
* @param array<string, string> $parameters
*/
public function __construct(
protected string|Stringable $data = '',
null|string|MediaType $mediaType = null,
array $parameters = [],
protected bool $base64 = true
) {
$this->setMediaType($mediaType);
$this->setParameters($parameters);
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::create()
*/
public static function create(
string $data,
null|string|MediaType $mediaType = null,
array $parameters = [],
bool $base64 = true
): self {
return new self(
data: $data,
mediaType: $mediaType,
parameters: $parameters,
base64: $base64,
);
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::parse()
*
* @throws InvalidArgumentException
*/
public static function parse(string|Stringable $dataUriScheme): self
{
$result = preg_match(self::PATTERN, (string) $dataUriScheme, $matches);
if ($result === false || $result === 0) {
throw new InvalidArgumentException('Invalid data uri scheme');
}
$isBase64Encoded = $matches['base64'] !== '';
$datauri = new self(
data: $isBase64Encoded ? base64_decode($matches['data'], strict: true) : rawurldecode($matches['data']),
mediaType: $matches['mediaType'],
base64: $isBase64Encoded,
);
if ($matches['parameters'] !== '') {
$parameters = explode(';', $matches['parameters']);
$parameters = array_filter($parameters, fn(string $value): bool => $value !== '');
$parameters = array_map(fn(string $value): array => explode('=', $value), $parameters);
foreach ($parameters as $parameter) {
$datauri->setParameter(...$parameter);
}
}
return $datauri;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::data()
*/
public function data(): string
{
return (string) $this->data;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::setData()
*/
public function setData(string|Stringable $data): self
{
$this->data = (string) $data;
return $this;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::mediaType()
*/
public function mediaType(): ?string
{
return $this->mediaType;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::setMediaType()
*/
public function setMediaType(null|string|MediaType $mediaType): self
{
$this->mediaType = $mediaType instanceof MediaType ? $mediaType->value : $mediaType;
return $this;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::parameters()
*/
public function parameters(): array
{
return $this->parameters;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::setParameters()
*/
public function setParameters(array $parameters): self
{
$this->parameters = $parameters;
return $this;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::appendParameters()
*/
public function appendParameters(array $parameters): self
{
foreach ($parameters as $key => $value) {
$this->setParameter((string) $key, (string) $value);
}
return $this;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::parameter()
*/
public function parameter(string $key): ?string
{
return $this->parameters[$key] ?? null;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::setParameter()
*/
public function setParameter(string $key, string $value): self
{
$this->parameters[$key] = $value;
return $this;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::charset()
*/
public function charset(): ?string
{
return $this->parameter('charset');
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::setCharset()
*/
public function setCharset(string $charset): self
{
$this->setParameter('charset', $charset);
return $this;
}
/**
* Prepare data for output.
*/
private function encodedData(): string
{
return $this->base64 === true ? (string) base64_encode($this->data) : rawurlencode((string) $this->data);
}
/**
* Prepare all set parameters for output.
*/
private function encodedParameters(): string
{
if (count($this->parameters) === 0 && $this->base64 === false) {
return '';
}
$parameters = array_map(function (mixed $key, mixed $value) {
return $key . '=' . $value;
}, array_keys($this->parameters), $this->parameters);
$parameterString = count($parameters) > 0 ? ';' . implode(';', $parameters) : '';
if ($this->base64 === true) {
$parameterString .= ';base64';
}
return $parameterString;
}
/**
* {@inheritdoc}
*
* @see DataUriInterface::toString()
*/
public function toString(): string
{
return 'data:' . $this->mediaType() . $this->encodedParameters() . ',' . $this->encodedData();
}
/**
* {@inheritdoc}
*
* @see Stringable::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
/**
* Show debug info for the current data uri scheme.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
return [
'mediaType' => $this->mediaType,
'size' => strlen($this->data),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class Base64ImageDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class BinaryImageDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
class ColorObjectDecoder extends AbstractDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $input instanceof ColorInterface;
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
*/
public function decode(mixed $input): ColorInterface
{
if (!$input instanceof ColorInterface) {
throw new InvalidArgumentException('Color object must be of type ' . ColorInterface::class);
}
return $input;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class DataUriImageDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class EncodedImageObjectDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class FilePathImageDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ColorInterface;
class ImageObjectDecoder extends AbstractDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $input instanceof ImageInterface;
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
return $input;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class NativeObjectDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class SplFileInfoImageDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
class StreamImageDecoder extends SpecializableDecoder
{
//
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
enum Direction
{
case HORIZONTAL;
case VERTICAL;
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Exception;
use Intervention\Image\Collection;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\CollectionInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Traits\CanBuildStream;
use Intervention\Image\Traits\CanParseFilePath;
use Stringable;
use Throwable;
abstract class AbstractDecoder implements DecoderInterface
{
use CanBuildStream;
use CanParseFilePath;
/**
* Determine if the given input is GIF data format.
*/
protected function isGifFormat(string $input): bool
{
return str_starts_with($input, 'GIF87a') || str_starts_with($input, 'GIF89a');
}
/**
* Extract and return EXIF data from given input which can be a file path
* or a stream stream resource.
*
* @throws InvalidArgumentException
* @throws DecoderException
* @return CollectionInterface<string, mixed>
*/
protected function extractExifData(string $input): CollectionInterface
{
if (!function_exists('exif_read_data')) {
return new Collection();
}
try {
// source might be file path
$source = self::readableFilePathOrFail($input);
} catch (Throwable) {
try {
// source might be stream resource
$source = self::buildStreamOrFail($input);
} catch (RuntimeException) {
return new Collection();
}
}
try {
// extract exif data
$data = @exif_read_data($source, null, true);
if (is_resource($source)) {
fclose($source);
}
} catch (Exception) {
$data = [];
}
return new Collection(is_array($data) ? $data : []);
}
/**
* Decodes given base64 encoded data.
*
* @throws InvalidArgumentException
* @throws DecoderException
*/
protected function decodeBase64Data(mixed $input): string
{
if (!is_string($input) && !$input instanceof Stringable) {
throw new InvalidArgumentException(
'Base64-encoded data must be either of type string or instance of Stringable',
);
}
$decoded = base64_decode((string) $input, true);
if ($decoded === false) {
throw new DecoderException('Input is not valid Base64-encoded data');
}
if (base64_encode($decoded) !== str_replace(["\n", "\r"], '', (string) $input)) {
throw new DecoderException('Input is not valid Base64-encoded data');
}
return $decoded;
}
}

View File

@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Intervention\Image\Config;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\MissingDependencyException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\AnalyzerInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
use Intervention\Image\Interfaces\SpecializableInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
abstract class AbstractDriver implements DriverInterface
{
/**
* @throws MissingDependencyException
*/
public function __construct(protected Config $config = new Config())
{
$this->checkHealth();
}
/**
* {@inheritdoc}
*
* @see DriverInterface::config()
*/
public function config(): Config
{
return $this->config;
}
/**
* {@inheritdoc}
*
* @see DriverInterface::decodeImage()
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws DriverException
*/
public function decodeImage(mixed $input, ?array $decoders = null): ImageInterface
{
$decoders = $decoders === null ? InputHandler::IMAGE_DECODERS : $decoders;
if (count($decoders) === 0) {
throw new InvalidArgumentException('No decoders in array');
}
try {
$result = InputHandler::usingDecoders($decoders, $this)->handle($input);
} catch (NotSupportedException) {
$type = is_object($input) ? $input::class : gettype($input);
throw new InvalidArgumentException('Unsupported image source type "' . $type . '"');
}
if (!$result instanceof ImageInterface) {
throw new ImageDecoderException('Result must be instance of ' . ImageInterface::class);
}
return $result;
}
/**
* {@inheritdoc}
*
* @see DriverInterface::decodeColor()
*
* @throws InvalidArgumentException
* @throws ColorDecoderException
* @throws DriverException
*/
public function decodeColor(mixed $input, ?array $decoders = null): ColorInterface
{
$decoders = $decoders === null ? InputHandler::COLOR_DECODERS : $decoders;
if (count($decoders) === 0) {
throw new InvalidArgumentException('No decoders in array');
}
try {
$result = InputHandler::usingDecoders($decoders, $this)->handle($input);
} catch (NotSupportedException) {
throw new ColorDecoderException('Unknown color format');
}
if (!$result instanceof ColorInterface) {
throw new ColorDecoderException('Result must be instance of ' . ColorInterface::class);
}
return $result;
}
/**
* {@inheritdoc}
*
* @see DriverInterface::specializeModifier()
*
* @throws NotSupportedException
*/
public function specializeModifier(ModifierInterface $modifier): ModifierInterface
{
return $this->specialize($modifier);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::specializeAnalyzer()
*
* @throws NotSupportedException
*/
public function specializeAnalyzer(AnalyzerInterface $analyzer): AnalyzerInterface
{
return $this->specialize($analyzer);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::specializeEncoder()
*
* @throws NotSupportedException
*/
public function specializeEncoder(EncoderInterface $encoder): EncoderInterface
{
return $this->specialize($encoder);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::specializeDecoder()
*
* @throws NotSupportedException
*/
public function specializeDecoder(DecoderInterface $decoder): DecoderInterface
{
return $this->specialize($decoder);
}
/**
* @throws NotSupportedException
*/
private function specialize(
ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface $object
): ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface {
// return object directly if no specializing is possible
if (!$object instanceof SpecializableInterface) {
return $object;
}
// return directly and only attach driver if object is already specialized
if ($object instanceof SpecializedInterface) {
$object->setDriver($this);
return $object;
}
// resolve classname for specializable object
$objectShortname = substr($object::class, (int) strrpos($object::class, '\\') + 1);
$specializedClassname = implode("\\", [
substr($this::class, 0, (int) strrpos($this::class, '\\')), // driver's namespace
match (true) {
$object instanceof ModifierInterface => 'Modifiers',
$object instanceof AnalyzerInterface => 'Analyzers',
$object instanceof EncoderInterface => 'Encoders',
$object instanceof DecoderInterface => 'Decoders',
},
$objectShortname,
]);
// fail if driver specialized classname does not exists
if (!class_exists($specializedClassname)) {
throw new NotSupportedException(
"Class '" . $objectShortname . "' is not supported by " . $this->id() . " driver"
);
}
// create a driver specialized object with the specializable properties of generic object
$specialized = new $specializedClassname(...$object->specializationArguments());
// attach driver
return $specialized->setDriver($this);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Intervention\Image\EncodedImage;
use Intervention\Image\Exceptions\LogicException;
use Intervention\Image\Exceptions\StreamException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\EncodedImageInterface;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Traits\CanBuildStream;
abstract class AbstractEncoder implements EncoderInterface
{
use CanBuildStream;
/**
* Default encoding quality.
*/
public const int DEFAULT_QUALITY = 75;
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*
* @throws LogicException
*/
public function encode(ImageInterface $image): EncodedImageInterface
{
if ($this instanceof SpecializedInterface) {
throw new LogicException(
"Specialized class '" . static::class . "' must override encode()"
);
}
return $image->encode($this);
}
/**
* Build new stream, run callback with it and return result as encoded image.
*
* @throws InvalidArgumentException
* @throws StreamException
*/
protected function createEncodedImage(callable $callback, ?string $mediaType = null): EncodedImage
{
$stream = self::buildStreamOrFail();
$callback($stream);
return is_string($mediaType) ? new EncodedImage($stream, $mediaType) : new EncodedImage($stream);
}
/**
* {@inheritdoc}
*
* @see EncoderInterface::setOptions()
*
* @throws InvalidArgumentException
*/
public function setOptions(mixed ...$options): self
{
foreach ($options as $key => $value) {
if (!property_exists($this, (string) $key)) {
throw new InvalidArgumentException(
'Option $' . $key . ' does not exist on ' . $this::class,
);
}
$this->{$key} = $value;
}
return $this;
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Intervention\Image\Alignment;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\FontProcessorInterface;
use Intervention\Image\Interfaces\PointInterface;
use Intervention\Image\Size;
use Intervention\Image\Typography\Line;
use Intervention\Image\Typography\TextBlock;
abstract class AbstractFontProcessor implements FontProcessorInterface
{
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::textBlock()
*
* @throws InvalidArgumentException
*/
public function textBlock(string $text, FontInterface $font, PointInterface $position): TextBlock
{
$lines = $this->wrapTextBlock(new TextBlock($text), $font);
$pivot = $this->buildPivot($lines, $font, $position);
$leading = $this->leading($font);
$blockWidth = $this->boxSize((string) $lines->longestLine(), $font)->width();
$x = $pivot->x();
$y = $font->hasFile() ? $pivot->y() + $this->capHeight($font) : $pivot->y();
$xAdjustment = 0;
// adjust line positions according to alignment
$horizontalAlignment = $font->alignmentHorizontal();
foreach ($lines as $line) {
$lineBoxSize = $this->boxSize((string) $line, $font);
$lineWidth = $lineBoxSize->width() + $lineBoxSize->pivot()->x();
$xAdjustment = $horizontalAlignment === Alignment::LEFT ? 0 : $blockWidth - $lineWidth;
$xAdjustment = $horizontalAlignment === Alignment::RIGHT ? intval(round($xAdjustment)) : $xAdjustment;
$xAdjustment = $horizontalAlignment === Alignment::CENTER ? intval(round($xAdjustment / 2)) : $xAdjustment;
$position = new Point($x + $xAdjustment, $y);
$position->rotate($font->angle(), $pivot);
$line->setPosition($position);
$y += $leading;
}
return $lines;
}
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::nativeFontSize()
*/
public function nativeFontSize(FontInterface $font): float
{
return $font->size();
}
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::typographicalSize()
*/
public function typographicalSize(FontInterface $font): int
{
return $this->boxSize('Hy', $font)->height();
}
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::capHeight()
*/
public function capHeight(FontInterface $font): int
{
return $this->boxSize('T', $font)->height();
}
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::leading()
*/
public function leading(FontInterface $font): int
{
return intval(round($this->typographicalSize($font) * $font->lineHeight()));
}
/**
* Reformat a text block by wrapping each line before the given maximum width.
*/
protected function wrapTextBlock(TextBlock $block, FontInterface $font): TextBlock
{
$newLines = [];
foreach ($block as $line) {
foreach ($this->wrapLine($line, $font) as $newLine) {
$newLines[] = $newLine;
}
}
return $block->setLines($newLines);
}
/**
* Check if a line exceeds the given maximum width and wrap it if necessary.
* The output will be an array of formatted lines that are all within the
* maximum width.
*
* @return array<Line>
*/
protected function wrapLine(Line $line, FontInterface $font): array
{
// no wrap width - no wrapping
if (is_null($font->wrapWidth())) {
return [$line];
}
$wrapped = [];
$formattedLine = new Line();
foreach ($line as $word) {
// calculate width of newly formatted line
$lineWidth = $this->boxSize(match ($formattedLine->count()) {
0 => $word,
default => $formattedLine . ' ' . $word,
}, $font)->width();
// decide if word fits on current line or a new line must be created
if ($line->count() === 1 || $lineWidth <= $font->wrapWidth()) {
$formattedLine->add($word);
} else {
if ($formattedLine->count() !== 0) {
$wrapped[] = $formattedLine;
}
$formattedLine = new Line($word);
}
}
$wrapped[] = $formattedLine;
return $wrapped;
}
/**
* Build pivot point of textblock according to the font settings and based on given position.
*
* @throws InvalidArgumentException
*/
protected function buildPivot(TextBlock $block, FontInterface $font, PointInterface $position): PointInterface
{
// bounding box
$box = Polygon::fromSize(new Size(
$this->boxSize((string) $block->longestLine(), $font)->width(),
$this->leading($font) * ($block->count() - 1) + $this->capHeight($font)
));
// set position
$box->setPivot($position);
// alignment
$box->alignHorizontally($font->alignmentHorizontal());
$box->alignVertically($font->alignmentVertical());
$box->rotate($font->angle());
return $box->last();
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Intervention\Image\Interfaces\FrameInterface;
abstract class AbstractFrame implements FrameInterface
{
/**
* Show debug info for the current image.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
return [
'delay' => $this->delay(),
'left' => $this->offsetLeft(),
'top' => $this->offsetTop(),
'disposalMethod' => $this->disposalMethod(),
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Analyzers\ColorspaceAnalyzer as GenericColorspaceAnalyzer;
use Intervention\Image\Colors\Rgb\Colorspace;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class ColorspaceAnalyzer extends GenericColorspaceAnalyzer implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*/
public function analyze(ImageInterface $image): mixed
{
return new Colorspace();
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Analyzers\HeightAnalyzer as GenericHeightAnalyzer;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class HeightAnalyzer extends GenericHeightAnalyzer implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*/
public function analyze(ImageInterface $image): mixed
{
return imagesy($image->core()->native());
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Analyzers\PixelColorAnalyzer as GenericPixelColorAnalyzer;
use Intervention\Image\Exceptions\AnalyzerException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use ValueError;
class PixelColorAnalyzer extends GenericPixelColorAnalyzer implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*
* @throws InvalidArgumentException
* @throws AnalyzerException
* @throws StateException
*/
public function analyze(ImageInterface $image): mixed
{
$colorProcessor = $this->driver()->colorProcessor($image);
return $this->colorAt($colorProcessor, $image->core()->frame($this->frame));
}
/**
* @throws InvalidArgumentException
* @throws AnalyzerException
*/
protected function colorAt(ColorProcessorInterface $processor, FrameInterface $frame): ColorInterface
{
$gd = $frame->native();
$index = @imagecolorat($gd, $this->x, $this->y);
if (!is_int($index)) {
throw new InvalidArgumentException(
'The specified position (' . $this->x . ', ' . $this->y . ') is not within the image area',
);
}
try {
$index = imagecolorsforindex($gd, $index);
} catch (ValueError) {
throw new AnalyzerException(
'The specified index is outside of the range',
);
}
return $processor->import($index);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Collection;
use Intervention\Image\Interfaces\ImageInterface;
class PixelColorsAnalyzer extends PixelColorAnalyzer
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*/
public function analyze(ImageInterface $image): mixed
{
$colors = new Collection();
$colorProcessor = $this->driver()->colorProcessor($image);
foreach ($image as $frame) {
$colors->push(
parent::colorAt($colorProcessor, $frame)
);
}
return $colors;
}
}

View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Analyzers\ResolutionAnalyzer as GenericResolutionAnalyzer;
use Intervention\Image\Exceptions\AnalyzerException;
use Intervention\Image\Exceptions\StreamException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\MissingDependencyException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\OriginInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Resolution;
use Intervention\Image\Traits\CanBuildStream;
use Throwable;
class ResolutionAnalyzer extends GenericResolutionAnalyzer implements SpecializedInterface
{
use CanBuildStream;
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*
* @throws InvalidArgumentException
* @throws AnalyzerException
*/
public function analyze(ImageInterface $image): mixed
{
$result = imageresolution($image->core()->native());
if (!is_array($result)) {
throw new AnalyzerException('Failed to read image resolution');
}
// GD returns 96x96 as resolution by default even if the image has no resolution.
// This is problematic because it is impossible to tell whether the image
// really has this resolution or whether it just corresponds to the default value.
//
// If GD's default resolution is returned here and the resolution is still unchanged
// we will make an attempt to find the resolution from origin.
if ($this->isGdDefaultResolution($result) && $image->core()->meta()->get('resolutionChanged') !== true) {
try {
$alternativeResoltion = $this->readResolutionFromOrigin($image->origin());
} catch (Throwable) {
$alternativeResoltion = [96, 96];
}
$result = $alternativeResoltion !== $result ? $alternativeResoltion : $result;
}
return new Resolution(...$result);
}
/**
* @param array<int|float> $resolution
*/
private function isGdDefaultResolution(array $resolution): bool
{
return intval($resolution[0] ?? 0) === 96 && intval($resolution[1] ?? 0) === 96;
}
/**
* @throws AnalyzerException
* @throws InvalidArgumentException
* @throws StreamException
* @return array<float>
*/
private function readResolutionFromOrigin(OriginInterface $origin): array
{
$handle = self::buildStreamOrFail(file_get_contents($origin->filePath()));
try {
try {
return $this->resolutionFromJfifHeader($handle);
} catch (Throwable) {
# code ...
}
try {
return $this->resolutionFromExifHeader($handle);
} catch (Throwable) {
# code ...
}
try {
return $this->resolutionFromPngPhys($handle);
} catch (Throwable) {
# code ...
}
throw new AnalyzerException('Unable to read resolution from path');
} finally {
fclose($handle);
}
}
/**
* @param resource $handle
* @throws AnalyzerException
* @return array<float>
*/
private function resolutionFromJfifHeader($handle): array
{
// read first 20 bytes
rewind($handle);
$header = fread($handle, 20);
// find the JFIF segment
$offset = strpos($header, 'JFIF');
if ($offset === false) {
throw new AnalyzerException('Unable to read JFIF header');
}
// read bytes at known offsets relative to JFIF
$units = ord($header[$offset + 7]);
$x = unpack('n', substr($header, $offset + 8, 2))[1];
$y = unpack('n', substr($header, $offset + 10, 2))[1];
if ($units === 2) { // unit is dots per cm → convert to DPI
return [round($x * 2.54), round($y * 2.54)];
}
return [$x, $y]; // unit is DPI or no unit
}
/**
* @param resource $handle
* @throws MissingDependencyException
* @throws AnalyzerException
* @return array<float>
*/
private function resolutionFromExifHeader($handle): array
{
if (!function_exists('exif_read_data')) {
throw new MissingDependencyException('Unable to read exif data');
}
rewind($handle);
$data = @exif_read_data($handle, null, true);
if ($data === false) {
throw new AnalyzerException('Unable to read exif data');
}
if (isset($data['XResolution']) && isset($data['YResolution'])) {
$resolution = [$data['XResolution'], $data['YResolution']];
}
if (isset($data['IFD0']) && isset($data['IFD0']['XResolution']) && isset($data['IFD0']['YResolution'])) {
$resolution = [$data['IFD0']['XResolution'], $data['IFD0']['YResolution']];
}
if (!isset($resolution)) {
throw new AnalyzerException('Unable to read exif data');
}
return array_map(function (mixed $value): int|float {
if (strpos($value, '/') === false) {
return $value;
}
$values = array_map(fn(string $value): int => intval($value), explode('/', $value));
if ($values[1] === 0) {
throw new AnalyzerException('Unable to read exif data, division by zero');
}
return $values[0] / $values[1];
}, $resolution);
}
/**
* @param resource $handle
* @throws AnalyzerException
* @return array<float>
*/
private function resolutionFromPngPhys($handle): array
{
rewind($handle);
$signature = fread($handle, 8);
// no PNG content
if ($signature !== "\x89PNG\x0D\x0A\x1A\x0A") {
throw new AnalyzerException('Input must be PNG format');
}
$marker = '';
while (!feof($handle)) {
$marker = strlen($marker) < 4 ? $marker . fread($handle, 1) : substr($marker, 1) . fread($handle, 1);
// find pHYs chunk
if ($marker === 'pHYs') {
// find length
fseek($handle, -8, SEEK_CUR);
$length = fread($handle, 4);
$length = unpack('N', $length)[1];
fseek($handle, 4, SEEK_CUR);
// read data
$data = fread($handle, $length);
$x = unpack('N', substr($data, 0, 4))[1];
$y = unpack('N', substr($data, 4, 4))[1];
return [
round($x * .0254),
round($y * .0254),
];
}
}
return [0, 0];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Analyzers\WidthAnalyzer as GenericWidthAnalyzer;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class WidthAnalyzer extends GenericWidthAnalyzer implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*/
public function analyze(ImageInterface $image): mixed
{
return imagesx($image->core()->native());
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use GdImage;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Size;
class Cloner
{
/**
* Create a clone of the given GdImage
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function clone(GdImage $gd): GdImage
{
// create empty canvas with same size
$clone = static::cloneEmpty($gd);
// transfer actual image to clone
imagecopy($clone, $gd, 0, 0, 0, 0, imagesx($gd), imagesy($gd));
return $clone;
}
/**
* Create an "empty" clone of the given GdImage
*
* This only retains the basic data without transferring the actual image.
* It is optionally possible to change the size of the result and set a
* background color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function cloneEmpty(
GdImage $gd,
?SizeInterface $size = null,
Color $background = new Color(255, 255, 255, 0)
): GdImage {
// define size
$size = $size ?: new Size(imagesx($gd), imagesy($gd));
if ($size->width() < 1 || $size->height() < 1) {
throw new InvalidArgumentException('Invalid image size');
}
// create new gd image with same size or new given size
$clone = imagecreatetruecolor($size->width(), $size->height());
if ($clone === false) {
throw new DriverException('Failed to create new image while cloning');
}
// copy resolution to clone
$resolution = imageresolution($gd);
if (is_array($resolution) && array_key_exists(0, $resolution) && array_key_exists(1, $resolution)) {
imageresolution($clone, $resolution[0], $resolution[1]);
}
// fill with background
$processor = new ColorProcessor();
imagefill($clone, 0, 0, $processor->export($background));
imagealphablending($clone, true);
imagesavealpha($clone, true);
// set background image as transparent if alpha channel value if color is below .5
// comes into effect when the end format only supports binary transparency (like GIF)
if ($background->alpha()->value() < .5) {
imagecolortransparent($clone, $processor->export($background));
}
return $clone;
}
/**
* Create a clone of an GdImage that is positioned on the specified background color.
* Possible transparent areas are mixed with this color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function cloneBlended(GdImage $gd, Color $background): GdImage
{
// create empty canvas with same size
$clone = static::cloneEmpty($gd, background: $background);
// transfer actual image to clone
imagecopy($clone, $gd, 0, 0, 0, 0, imagesx($gd), imagesy($gd));
return $clone;
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Colors\Rgb\Channels\Alpha;
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;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Traits\CanConvertRange;
class ColorProcessor implements ColorProcessorInterface
{
use CanConvertRange;
/**
* {@inheritdoc}
*
* @see ColorProcessorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Rgb();
}
/**
* {@inheritdoc}
*
* @see ColorProcessorInterface::export()
*
* @throws DriverException
*/
public function export(ColorInterface $color): int
{
// convert color to colorspace
$color = $color->toColorspace($this->colorspace());
// gd only supports rgb so the channels can be accessed directly
$r = $color->channel(Red::class)->value();
$g = $color->channel(Green::class)->value();
$b = $color->channel(Blue::class)->value();
$a = $color->channel(Alpha::class)->value();
try {
// convert alpha value to gd alpha
// ([opaque]1-0[transparent]) to ([opaque]0-127[transparent])
$a = (int) round(self::convertRange($a, Alpha::min(), Alpha::max(), 127, 0));
} catch (RuntimeException $e) {
throw new DriverException('Failed to export color', previous: $e);
}
return ($a << 24) + ($r << 16) + ($g << 8) + $b;
}
/**
* {@inheritdoc}
*
* @see ColorProcessorInterface::import()
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public function import(mixed $color): ColorInterface
{
if (!is_int($color) && !is_array($color)) {
throw new InvalidArgumentException('GD driver can only decode colors in integer or array format');
}
if (is_array($color)) {
// array conversion
if (!$this->isValidArrayColor($color)) {
throw new InvalidArgumentException(
'GD driver can only decode array color format array{red: int, green: int, blue: int, alpha: int}',
);
}
$r = $color['red'];
$g = $color['green'];
$b = $color['blue'];
$a = $color['alpha'];
} else {
// integer conversion
$a = ($color >> 24) & 0xFF;
$r = ($color >> 16) & 0xFF;
$g = ($color >> 8) & 0xFF;
$b = $color & 0xFF;
}
try {
// convert gd apha integer to intervention alpha integer
// ([opaque]0-127[transparent]) to ([opaque]1-0[transparent])
$a = self::convertRange($a, 127, 0, 0, 1);
} catch (RuntimeException $e) {
throw new DriverException('Failed to import color', previous: $e);
}
try {
return new Color($r, $g, $b, $a);
} catch (InvalidArgumentException $e) {
throw new DriverException('Failed to import color', previous: $e);
}
}
/**
* Check if given array is valid color format
* array{red: int, green: int, blue: int, alpha: int}
* i.e. result of imagecolorsforindex()
*
* @param array<mixed> $color
*/
private function isValidArrayColor(array $color): bool
{
if (!array_key_exists('red', $color)) {
return false;
}
if (!array_key_exists('green', $color)) {
return false;
}
if (!array_key_exists('blue', $color)) {
return false;
}
if (!array_key_exists('alpha', $color)) {
return false;
}
if (!is_int($color['red'])) {
return false;
}
if (!is_int($color['green'])) {
return false;
}
if (!is_int($color['blue'])) {
return false;
}
if (!is_int($color['alpha'])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Collection;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\CollectionInterface;
use Intervention\Image\Interfaces\CoreInterface;
use Intervention\Image\Interfaces\FrameInterface;
class Core extends Collection implements CoreInterface
{
protected int $loops = 0;
protected CollectionInterface $meta;
/**
* Create new core
*
* @param array<int|string, FrameInterface> $items
*/
public function __construct(array $items = [])
{
parent::__construct($items);
$this->meta = new Collection();
}
/**
* {@inheritdoc}
*
* @see CoreInterface::add()
*/
public function add(FrameInterface $frame): CoreInterface
{
$this->push($frame);
return $this;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::native()
*/
public function native(): mixed
{
return $this->first()->native();
}
/**
* {@inheritdoc}
*
* @see CoreInterface::setNative()
*/
public function setNative(mixed $native): CoreInterface
{
$this->clear()->push(new Frame($native));
return $this;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::frame()
*
* @throws InvalidArgumentException
*/
public function frame(int $position): FrameInterface
{
$frame = $this->at($position);
if ($frame === null || $position < 0 || $position > $this->count()) {
throw new InvalidArgumentException('Frame #' . $position . ' could not be found in the image');
}
return $frame;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::loops()
*/
public function loops(): int
{
return $this->loops;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::setLoops()
*/
public function setLoops(int $loops): CoreInterface
{
$this->loops = $loops;
return $this;
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::first()
*/
public function first(): FrameInterface
{
return parent::first();
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::last()
*/
public function last(): FrameInterface
{
return parent::last();
}
/**
* {@inheritdoc}
*
* @see CoreInterface::meta()
*/
public function meta(): CollectionInterface
{
return $this->meta;
}
/**
* Clone instance
*/
public function __clone(): void
{
$this->meta = clone $this->meta;
foreach ($this->items as $key => $frame) {
$this->items[$key] = clone $frame;
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
use Intervention\Image\Exceptions\DirectoryNotFoundException;
use Intervention\Image\Exceptions\FileNotFoundException;
use Intervention\Image\Exceptions\FileNotReadableException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\MediaType;
use Intervention\Image\Traits\CanParseFilePath;
use TypeError;
use ValueError;
abstract class AbstractDecoder extends SpecializableDecoder implements SpecializedInterface
{
use CanParseFilePath;
/**
* Return media (mime) type of the file at given file path
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws NotSupportedException
* @throws DirectoryNotFoundException
* @throws FileNotFoundException
* @throws FileNotReadableException
*/
protected function mediaTypeByFilePath(string $filepath): MediaType
{
$filepath = self::readableFilePathOrFail($filepath);
if (function_exists('finfo_file') && function_exists('finfo_open')) {
$mediaType = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $filepath);
if (is_string($mediaType)) {
try {
return MediaType::from($mediaType);
} catch (ValueError | TypeError) {
throw new NotSupportedException('Unsupported media type (MIME) ' . $mediaType . '.');
}
}
}
$info = @getimagesize($filepath);
if (!is_array($info)) {
throw new ImageDecoderException('Failed to read media (MIME) type from data in file path');
}
try {
return MediaType::from($info['mime']);
} catch (ValueError | TypeError) {
throw new NotSupportedException('Unsupported media type (MIME) ' . $info['mime'] . '.');
}
}
/**
* Return media (mime) type of the given image data
*
* @throws ImageDecoderException
* @throws NotSupportedException
*/
protected function mediaTypeByBinary(string $data): MediaType
{
if (function_exists('finfo_buffer') && function_exists('finfo_open')) {
$mediaType = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
if (is_string($mediaType)) {
try {
return MediaType::from($mediaType);
} catch (ValueError | TypeError) {
throw new NotSupportedException('Unsupported media type (MIME) ' . $mediaType . '.');
}
}
}
$info = @getimagesizefromstring($data);
if (!is_array($info)) {
throw new ImageDecoderException('Failed to read media (MIME) type from binary data');
}
try {
return MediaType::from($info['mime']);
} catch (ValueError | TypeError) {
throw new NotSupportedException('Unsupported media type (MIME) ' . $info['mime'] . '.');
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Traits\CanDetectImageSources;
class Base64ImageDecoder extends BinaryImageDecoder implements DecoderInterface
{
use CanDetectImageSources;
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $this->couldBeBase64Data($input);
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface
{
try {
$data = $this->decodeBase64Data($input);
} catch (DecoderException) {
throw new ImageDecoderException('Unable to Base64-decode image from string');
}
try {
return parent::decode($data);
} catch (DecoderException) {
throw new ImageDecoderException('Base64-encoded data contains unsupported image type');
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Format;
use Intervention\Image\Modifiers\OrientModifier;
use Intervention\Image\Traits\CanDetectImageSources;
use Stringable;
class BinaryImageDecoder extends NativeObjectDecoder implements DecoderInterface
{
use CanDetectImageSources;
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $this->couldBeBinaryData($input);
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws DriverException
* @throws StateException
* @throws NotSupportedException
*/
public function decode(mixed $input): ImageInterface
{
if (!is_string($input) && !$input instanceof Stringable) {
throw new InvalidArgumentException(
'Image source must be binary data of type string or instance of ' . Stringable::class,
);
}
$input = (string) $input;
if ($input === '') {
throw new InvalidArgumentException('Unable to decode binary data from empty string');
}
return $this->isGifFormat($input) ? $this->decodeGif($input) : $this->decodeBinary($input);
}
/**
* Decode image from given binary data
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws DriverException
* @throws StateException
* @throws NotSupportedException
*/
private function decodeBinary(string $input): ImageInterface
{
$gd = @imagecreatefromstring($input);
if ($gd === false) {
throw new ImageDecoderException('Failed to decode unsupported image format from binary data');
}
// create image instance
$image = parent::decode($gd);
// get media type
$mediaType = $this->mediaTypeByBinary($input);
// extract & set exif data for appropriate formats
if (in_array($mediaType->format(), [Format::JPEG, Format::TIFF])) {
$image->setExif($this->extractExifData($input));
}
// set mediaType on origin
$image->origin()->setMediaType($mediaType);
// adjust image orientation
if ($this->driver()->config()->autoOrientation) {
$image->modify(new OrientModifier());
}
return $image;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\DataUri;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Traits\CanDetectImageSources;
class DataUriImageDecoder extends BinaryImageDecoder implements DecoderInterface
{
use CanDetectImageSources;
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $this->couldBeDataUrl($input);
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
* @throws DriverException
* @throws ImageDecoderException
* @throws StateException
* @throws NotSupportedException
*/
public function decode(mixed $input): ImageInterface
{
if ($input instanceof DataUri) {
try {
return parent::decode($input->data());
} catch (DecoderException) {
throw new ImageDecoderException('Data Uri contains unsupported image type');
}
}
if (!is_string($input)) {
throw new InvalidArgumentException(
'Image source must be data uri scheme of type string or ' . DataUri::class,
);
}
try {
return parent::decode(DataUri::parse($input)->data());
} catch (DecoderException) {
throw new ImageDecoderException('Data Uri contains unsupported image type');
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\EncodedImage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\EncodedImageInterface;
class EncodedImageObjectDecoder extends BinaryImageDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $input instanceof EncodedImageInterface;
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws DriverException
* @throws StateException
* @throws NotSupportedException
*/
public function decode(mixed $input): ImageInterface
{
if (!$input instanceof EncodedImageInterface) {
throw new InvalidArgumentException('Image source must be of type ' . EncodedImage::class);
}
try {
return parent::decode($input->toString());
} catch (DecoderException) {
throw new ImageDecoderException(EncodedImage::class . ' contains unsupported image type');
}
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\DirectoryNotFoundException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\FileNotFoundException;
use Intervention\Image\Exceptions\FileNotReadableException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Format;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\MediaType;
use Intervention\Image\Modifiers\OrientModifier;
use Intervention\Image\Traits\CanDetectImageSources;
use Throwable;
class FilePathImageDecoder extends NativeObjectDecoder implements DecoderInterface
{
use CanDetectImageSources;
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $this->couldBeFilePath($input);
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws DriverException
* @throws StateException
* @throws FileNotFoundException
* @throws FileNotReadableException
* @throws DirectoryNotFoundException
*/
public function decode(mixed $input): ImageInterface
{
// make sure path is valid
$path = self::readableFilePathOrFail($input);
try {
// detect media (mime) type
$mediaType = $this->mediaTypeByFilePath($path);
} catch (Throwable) {
throw new ImageDecoderException('File contains unsupported image format');
}
$image = match ($mediaType->format()) {
// gif files might be animated and therefore cannot
// be handled by the standard GD decoder.
Format::GIF => $this->decodeGif($path),
default => $this->decodeDefault($path, $mediaType),
};
// set file path & mediaType on origin
$image->origin()->setFilePath($path);
$image->origin()->setMediaType($mediaType);
// extract exif for the appropriate formats
if ($mediaType->format() === Format::JPEG) {
$image->setExif($this->extractExifData($path));
}
// adjust image orientation
if ($this->driver()->config()->autoOrientation) {
$image->modify(new OrientModifier());
}
return $image;
}
/**
* Try to decode data from file path as given image format
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws StateException
* @throws DriverException
*/
private function decodeDefault(string $path, MediaType $mediaType): ImageInterface
{
$gdImage = match ($mediaType->format()) {
Format::JPEG => @imagecreatefromjpeg($path),
Format::WEBP => @imagecreatefromwebp($path),
Format::PNG => @imagecreatefrompng($path),
Format::AVIF => @imagecreatefromavif($path),
Format::BMP => @imagecreatefrombmp($path),
default => throw new ImageDecoderException('File contains unsupported image format'),
};
if ($gdImage === false) {
throw new ImageDecoderException(
'Failed to decode data from file "' . $path . '" as image format "' . $mediaType->value . '"',
);
}
try {
return parent::decode($gdImage);
} catch (DecoderException) {
throw new ImageDecoderException(
'Failed to decode data from file "' . $path . '" as image format "' . $mediaType->value . '"',
);
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use GdImage;
use Intervention\Gif\Exceptions\GifException;
use Intervention\Gif\Splitter as GifSplitter;
use Intervention\Image\Drivers\Gd\Core;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ImageInterface;
class NativeObjectDecoder extends AbstractDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $input instanceof GdImage;
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
* @throws DriverException
* @throws StateException
*/
public function decode(mixed $input): ImageInterface
{
if (!$input instanceof GdImage) {
throw new InvalidArgumentException('Image source must be of type ' . GdImage::class);
}
if (!imageistruecolor($input)) {
$result = imagepalettetotruecolor($input);
if ($result === false) {
throw new DriverException('Failed to convert image to true color');
}
}
imagesavealpha($input, true);
// build image instance
return new Image(
$this->driver(),
new Core([
new Frame($input)
])
);
}
/**
* Decode image from given GIF source which can be either a file path or binary data.
*
* Depending on the configuration, this is taken over by the native GD function
* or, if animations are required, by our own extended decoder.
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws DriverException
* @throws StateException
*/
protected function decodeGif(mixed $input): ImageInterface
{
// create non-animated image depending on config
if ($this->driver()->config()->decodeAnimation === false) {
$native = $this->isGifFormat($input) ? @imagecreatefromstring($input) : @imagecreatefromgif($input);
if ($native === false) {
throw new ImageDecoderException('Failed to decode GIF format');
}
$image = self::decode($native);
$image->origin()->setMediaType('image/gif');
return $image;
}
try {
// create empty core
$core = new Core();
// add frames to core
$splitter = GifSplitter::decode($input)
->split()
->flatten()
->each(function (GdImage $native, int $delay) use ($core): void {
$core->push(new Frame($native, $delay / 100));
});
// set loops on core
$core->setLoops($splitter->loops());
} catch (GifException $e) {
throw new ImageDecoderException('Failed to decode GIF format', previous: $e);
}
// create (possibly) animated image
$image = new Image($this->driver(), $core);
// set media type
$image->origin()->setMediaType('image/gif');
return $image;
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use SplFileInfo;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\DirectoryNotFoundException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\FileNotFoundException;
use Intervention\Image\Exceptions\FileNotReadableException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Traits\CanParseFilePath;
class SplFileInfoImageDecoder extends FilePathImageDecoder implements DecoderInterface
{
use CanParseFilePath;
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return $input instanceof SplFileInfo;
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
* @throws ImageDecoderException
* @throws DriverException
* @throws StateException
* @throws DirectoryNotFoundException
* @throws FileNotFoundException
* @throws FileNotReadableException
*/
public function decode(mixed $input): ImageInterface
{
if (!$input instanceof SplFileInfo) {
throw new InvalidArgumentException('Image source must be of type ' . SplFileInfo::class);
}
try {
return parent::decode(self::filePathFromSplFileInfoOrFail($input));
} catch (DecoderException) {
throw new ImageDecoderException(SplFileInfo::class . ' contains unsupported image type');
}
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\StreamException;
use Intervention\Image\Exceptions\ImageDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\ImageInterface;
class StreamImageDecoder extends BinaryImageDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::supports()
*/
public function supports(mixed $input): bool
{
return is_resource($input);
}
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*
* @throws InvalidArgumentException
* @throws StreamException
* @throws DriverException
* @throws StateException
* @throws ImageDecoderException
* @throws NotSupportedException
*/
public function decode(mixed $input): ImageInterface
{
if (!is_resource($input) || !in_array(get_resource_type($input), ['file', 'stream'])) {
throw new InvalidArgumentException("Image source must be a resource of type 'file' or 'stream'");
}
$contents = '';
$result = rewind($input);
if ($result === false) {
throw new StreamException('Failed to rewind position of stream');
}
while (!feof($input)) {
$chunk = fread($input, 1024);
if ($chunk === false) {
throw new StreamException('Failed to read image from stream');
}
$contents .= $chunk;
}
try {
return parent::decode($contents);
} catch (DecoderException) {
throw new ImageDecoderException(
'Failed to decode image from stream, could be unsupported image format',
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More