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

21
vendor/intervention/image/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013-present Oliver Vogel
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
vendor/intervention/image/composer.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "intervention/image",
"description": "PHP Image Processing",
"homepage": "https://image.intervention.io",
"keywords": [
"image",
"gd",
"imagick",
"watermark",
"thumbnail",
"resize"
],
"license": "MIT",
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io"
}
],
"require": {
"php": "^8.3",
"ext-mbstring": "*",
"intervention/gif": "^5"
},
"require-dev": {
"phpunit/phpunit": "^12.0",
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1",
"squizlabs/php_codesniffer": "^4",
"slevomat/coding-standard": "~8.0"
},
"suggest": {
"ext-exif": "Recommended to be able to read EXIF data properly."
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Intervention\\Image\\Tests\\": "tests"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

90
vendor/intervention/image/readme.md vendored Normal file
View File

@@ -0,0 +1,90 @@
# Intervention Image
## PHP Image Processing
[![Latest Version](https://img.shields.io/packagist/v/intervention/image.svg)](https://packagist.org/packages/intervention/image)
[![Build Status](https://github.com/Intervention/image/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Intervention/image/actions)
[![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image.svg)](https://packagist.org/packages/intervention/image/stats)
[![Support me on Ko-fi](https://raw.githubusercontent.com/Intervention/image/develop/.github/images/support.svg)](https://ko-fi.com/interventionphp)
Intervention Image is a **PHP image processing library** that provides a simple
and expressive way to create, edit, and compose images. It comes with a universal
interface for the popular PHP image manipulation extensions. You can
choose between the GD library, Imagick or libvips as the base layer for all operations.
- Fluent interface for common image editing tasks
- Interchangeable driver architecture with support for **GD, Imagick and libvips**
- Support for animated images with all drivers
- Framework-agnostic
## Installation
Install this library using [Composer](https://getcomposer.org). Simply request the package with the following command:
```bash
composer require intervention/image
```
## Getting Started
Learn the [basics](https://image.intervention.io/v4/basics/instantiation/) on
how to use Intervention Image and more with the [official documentation](https://image.intervention.io/v4/).
## Code Examples
```php
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Alignment;
use Intervention\Image\Color;
use Intervention\Image\Format;
// create image manager instance using the preferred driver
$manager = ImageManager::usingDriver(GdDriver::class);
// read image data from path
$image = $manager->decodePath('images/example.webp');
// scale image by height
$image->scale(height: 300);
// insert a watermark
$image->insert('images/watermark.png', alignment: Alignment::BOTTOM_RIGHT);
// encode edited image
$encoded = $image->encodeUsingFormat(Format::JPEG, quality: 65);
// save encoded image
$encoded->save('images/example.jpg');
```
## Requirements
Before you begin with the installation make sure that your server environment
supports the following requirements.
- PHP >= 8.3
- Mbstring PHP Extension
- Image Processing PHP Extension (GD, Imagick or libvips)
## Supported Image Libraries
Depending on your environment Intervention Image lets you choose between
different image processing extensions.
- GD Library
- Imagick PHP extension
- [libvips](https://github.com/Intervention/image-driver-vips)
## Security
If you discover any security related issues, please email oliver@intervention.io directly.
## Authors
This library is developed and maintained by [Oliver Vogel](https://intervention.io)
Thanks to the community of [contributors](https://github.com/Intervention/image/graphs/contributors) who have helped to improve this project.
## License
Intervention Image is licensed under the [MIT License](LICENSE).

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 . '"',
);
}
}
}

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