1
0
mirror of https://github.com/Intervention/image.git synced 2025-01-17 12:18:14 +01:00

Merge branch 'next'

This commit is contained in:
Oliver Vogel 2023-12-08 14:57:00 +01:00
commit 4c1938f0d3
633 changed files with 19715 additions and 17479 deletions

7
.gitattributes vendored
View File

@ -2,8 +2,11 @@
/.github export-ignore
/tests export-ignore
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/phpunit.xml export-ignore
/README.md export-ignore
/phpunit.xml.dist export-ignore
/readme.md export-ignore
/docker-compose.yml export-ignore
/Dockerfile export-ignore

102
.github/workflows/run-tests.yml vendored Normal file
View File

@ -0,0 +1,102 @@
name: Tests
on: [ push, pull_request ]
jobs:
run:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ '8.1', '8.2', '8.3' ]
imagemagick: [ '6.9.12-55', '7.1.0-40' ]
imagick: [ '3.7.0' ]
stability: [ prefer-stable ]
name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - ImageMagick ${{ matrix.imagemagick }}
steps:
- name: Checkout project
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, gd
coverage: none
- name: Prepare environment for Imagemagick
run: |
sudo apt-get -y remove imagemagick imagemagick-6-common libmagic-dev
sudo apt-get update
sudo apt-get install -y libjpeg-dev libgif-dev libtiff-dev libpng-dev libwebp-dev libavif-dev libheif-dev
sudo apt-get install -y libmagickwand-dev
- name: Cache ImageMagick
uses: actions/cache@v2
id: cache-imagemagick
with:
path: /home/runner/im/imagemagick-${{ matrix.imagemagick }}
key: ${{ runner.os }}-ImageMagick-${{ matrix.imagemagick }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-ImageMagick-${{ matrix.imagemagick }}-
- name: Check ImageMagick cache exists
uses: andstor/file-existence-action@v1
id: cache-imagemagick-exists
with:
files: /home/runner/im/imagemagick-${{ matrix.imagemagick }}
- name: Install ImageMagick
if: ( steps.cache-imagemagick.outputs.cache-hit != 'true' || steps.cache-imagemagick-exists.outputs.files_exists != 'true' )
run: |
curl -o /tmp/ImageMagick.tar.xz -sL https://imagemagick.org/archive/releases/ImageMagick-${{ matrix.imagemagick }}.tar.xz
(
cd /tmp || exit 1
tar xf ImageMagick.tar.xz
cd ImageMagick-${{ matrix.imagemagick }}
sudo ./configure --prefix=/home/runner/im/imagemagick-${{ matrix.imagemagick }}
sudo make -j$(nproc)
sudo make install
)
- name: Install PHP ImageMagick extension
run: |
curl -o /tmp/imagick.tgz -sL http://pecl.php.net/get/imagick-${{ matrix.imagick }}.tgz
(
cd /tmp || exit 1
tar -xzf imagick.tgz
cd imagick-${{ matrix.imagick }}
phpize
sudo ./configure --with-imagick=/home/runner/im/imagemagick-${{ matrix.imagemagick }}
sudo make -j$(nproc)
sudo make install
)
sudo bash -c 'echo "extension=imagick.so" >> /etc/php/${{ matrix.php }}/cli/php.ini'
php --ri imagick;
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ matrix.stability }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ matrix.stability }}-
- name: Install dependencies
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Which Imagick Version
run: php -r 'var_dump(Imagick::getVersion());'
- name: Supported Imagick Formats
run: php -r 'var_dump(Imagick::queryFormats());'
- name: Execute tests
run: vendor/bin/phpunit --no-coverage
- name: Run analyzer
run: vendor/bin/phpstan analyze --level=4 ./src

10
.gitignore vendored
View File

@ -1,5 +1,7 @@
.DS_Store
composer.lock
vendor/
dev/
.idea/
build/
vendor/
.DS_Store
.phpunit.result.cache
composer.lock
phpunit.xml

View File

@ -1,36 +0,0 @@
language: php
sudo: false
jobs:
include:
- dist: trusty
php: 5.4
- dist: trusty
php: 5.5
- dist: xenial
php: 5.6
- dist: xenial
php: 7.0
- dist: xenial
php: 7.1
- dist: xenial
php: 7.2
- dist: xenial
php: 7.3
- dist: xenial
php: 7.4
- dist: xenial
php: nightly
- dist: xenial
php: hhvm
allow_failures:
- php: nightly
- php: hhvm
before_script:
- printf "\n" | pecl install imagick
- composer self-update || true
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader
script: vendor/bin/phpunit

36
Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM php:8-cli
RUN apt update \
&& apt install -y \
libpng-dev \
libicu-dev \
libavif-dev \
libpq-dev \
libzip-dev \
zip \
zlib1g-dev \
locales \
locales-all \
libmagickwand-dev \
libwebp-dev \
&& pecl install imagick \
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp --with-avif \
&& docker-php-ext-enable imagick \
&& docker-php-ext-install \
intl \
opcache \
pdo \
pdo_pgsql \
pdo_mysql \
pgsql \
fileinfo \
mysqli \
gd \
bcmath \
exif \
zip \
&& apt-get clean
# install composer
#
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View File

@ -1,9 +1,9 @@
The MIT License (MIT)
Copyright (c) 2014 Oliver Vogel
Copyright (c) 2023 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.
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.

View File

@ -1,55 +0,0 @@
# Intervention Image
Intervention Image is a **PHP image handling and manipulation** library providing an easier and expressive way to create, edit, and compose images. The package includes ServiceProviders and Facades for easy **Laravel** integration.
[![Latest Version](https://img.shields.io/packagist/v/intervention/image.svg)](https://packagist.org/packages/intervention/image)
[![Build Status](https://travis-ci.org/Intervention/image.png?branch=master)](https://travis-ci.org/Intervention/image)
[![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image.svg)](https://packagist.org/packages/intervention/image/stats)
## Requirements
- PHP >=5.4
- Fileinfo Extension
## Supported Image Libraries
- GD Library (>=2.0)
- Imagick PHP extension (>=6.5.7)
## Getting started
- [Installation](https://image.intervention.io/v2/introduction/installation)
- [Laravel Framework Integration](https://image.intervention.io/v2/introduction/installation#integration-in-laravel)
- [Basic Usage](https://image.intervention.io/v2/usage/overview#basic-usage)
## Code Examples
```php
// open an image file
$img = Image::make('public/foo.jpg');
// resize image instance
$img->resize(320, 240);
// insert a watermark
$img->insert('public/watermark.png');
// save image in desired format
$img->save('public/bar.jpg');
```
Refer to the [official documentation](http://image.intervention.io/) to learn more about Intervention Image.
## Contributing
Contributions to the Intervention Image library are welcome. Please note the following guidelines before submitting your pull request.
- Follow [PSR-2](http://www.php-fig.org/psr/psr-2/) coding standards.
- Write tests for new functions and added features
- API calls should work consistently with both GD and Imagick drivers
## License
Intervention Image is licensed under the [MIT License](http://opensource.org/licenses/MIT).
Copyright 2017 [Oliver Vogel](http://olivervogel.com/)

View File

@ -1,8 +1,8 @@
{
"name": "intervention/image",
"description": "Image handling and manipulation library with support for Laravel integration",
"homepage": "http://image.intervention.io/",
"keywords": ["image", "gd", "imagick", "laravel", "watermark", "thumbnail"],
"description": "PHP image manipulation",
"homepage": "https://image.intervention.io/",
"keywords": ["image", "gd", "imagick", "watermark", "thumbnail", "resize"],
"license": "MIT",
"authors": [
{
@ -12,36 +12,22 @@
}
],
"require": {
"php": ">=5.4.0",
"ext-fileinfo": "*",
"guzzlehttp/psr7": "~1.1 || ^2.0"
"php": "^8.1",
"intervention/gif": "^3"
},
"require-dev": {
"phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15",
"mockery/mockery": "~0.9.2"
},
"suggest": {
"ext-gd": "to use GD library based image processing.",
"ext-imagick": "to use Imagick based image processing.",
"intervention/imagecache": "Caching extension for the Intervention Image library"
"phpunit/phpunit": "^9",
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^1"
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
"Intervention\\Image\\": "src"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
},
"laravel": {
"providers": [
"Intervention\\Image\\ImageServiceProvider"
],
"aliases": {
"Image": "Intervention\\Image\\Facades\\Image"
}
"autoload-dev": {
"psr-4": {
"Intervention\\Image\\Tests\\": "tests"
}
},
"minimum-stability": "stable"
}
}

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
version: '3'
services:
tests:
build: ./
working_dir: /project
command: bash -c "composer install && ./vendor/bin/phpunit -vvv"
volumes:
- ./:/project
analysis:
build: ./
working_dir: /project
command: bash -c "composer install && ./vendor/bin/phpstan analyze --level=4 ./src"
volumes:
- ./:/project

View File

@ -1,18 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
beStrictAboutTestsThatDoNotTestAnything="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

23
phpunit.xml.dist Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

View File

@ -1,11 +0,0 @@
{
"providers": [
"Intervention\\Image\\ImageServiceProvider"
],
"aliases": [
{
"alias": "Image",
"facade": "Intervention\\Image\\Facades\\Image"
}
]
}

86
readme.md Normal file
View File

@ -0,0 +1,86 @@
# Intervention Image
## PHP Image Manipulation
[![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)
Intervention Image is a **image handling and manipulation library written in
PHP** providing an easy and expressive way to create, edit, and compose
images. GD library or Imagick can be selected as the base layer for all
operations.
- Simple interface for common image manipulation tasks
- Interchangable driver architecture
- Support for animated images
- Framework-agnostic
- PSR-12 compliant
## Installation
You can install this package easily with [Composer](https://getcomposer.org/). Just require the package with the following command:
```bash
composer require intervention/image
```
## Getting started
Learn the [basics](https://image.intervention.io/v3/basics/instantiation/) on
how to use Intervention Image and more with the [official
documentation](https://image.intervention.io/v3/).
## Code Examples
```php
// create image manager with desired driver
$manager = new ImageManager(
new Intervention\Image\Drivers\Gd\Driver()
);
// open an image file
$image = $manager->read('images/example.gif');
// resize image instance
$image->resize(height: 300);
// insert a watermark
$image->place('images/watermark.png');
// encode edited image
$encoded = $image->toJpg();
// save encoded image
$encoded->save('images/example.jpg');
```
## Requirements
- PHP >= 8.1
## Supported Image Libraries
- GD Library
- Imagick PHP extension
## Development & Testing
With this package comes a Docker image to build a test suite and analysis
container. To build this container you have to have Docker installed on your
system. You can run all tests with this command.
```bash
docker-compose run --rm --build tests
```
Run the static analyzer on the code base.
```bash
docker-compose run --rm --build analysis
```
## License
Intervention Image is licensed under the [MIT License](http://opensource.org/licenses/MIT).
Copyright 2020 [Oliver Vogel](http://intervention.io/)

View File

@ -0,0 +1,19 @@
<?php
namespace Intervention\Image\Analyzers;
use Intervention\Image\Interfaces\AnalyzerInterface;
use Intervention\Image\Interfaces\ImageInterface;
abstract class AbstractAnalyzer implements AnalyzerInterface
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*/
public function analyze(ImageInterface $image): mixed
{
return $image->analyze($this);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Intervention\Image\Analyzers;
class ColorspaceAnalyzer extends AbstractAnalyzer
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Intervention\Image\Analyzers;
class HeightAnalyzer extends AbstractAnalyzer
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace Intervention\Image\Analyzers;
class PixelColorAnalyzer extends AbstractAnalyzer
{
public function __construct(
public int $x,
public int $y,
public int $frame_key = 0
) {
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Intervention\Image\Analyzers;
class PixelColorsAnalyzer extends AbstractAnalyzer
{
public function __construct(
public int $x,
public int $y
) {
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Intervention\Image\Analyzers;
class ProfileAnalyzer extends AbstractAnalyzer
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Intervention\Image\Analyzers;
class ResolutionAnalyzer extends AbstractAnalyzer
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Intervention\Image\Analyzers;
class WidthAnalyzer extends AbstractAnalyzer
{
}

194
src/Collection.php Normal file
View File

@ -0,0 +1,194 @@
<?php
namespace Intervention\Image;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\CollectionInterface;
use ArrayIterator;
use Countable;
use Traversable;
use IteratorAggregate;
class Collection implements CollectionInterface, IteratorAggregate, Countable
{
public function __construct(protected array $items = [])
{
}
/**
* Static constructor
*
* @param array $items
* @return self
*/
public static function create(array $items = []): self
{
return new self($items);
}
public function has(int|string $key): bool
{
return array_key_exists($key, $this->items);
}
/**
* Returns Iterator
*
* @return Traversable
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
public function toArray(): array
{
return $this->items;
}
/**
* Count items in collection
*
* @return integer
*/
public function count(): int
{
return count($this->items);
}
/**
* Append new item to collection
*
* @param mixed $item
* @return CollectionInterface
*/
public function push($item): CollectionInterface
{
$this->items[] = $item;
return $this;
}
/**
* Return first item in collection
*
* @return mixed
*/
public function first(): mixed
{
if ($item = reset($this->items)) {
return $item;
}
return null;
}
/**
* Returns last item in collection
*
* @return mixed
*/
public function last(): mixed
{
if ($item = end($this->items)) {
return $item;
}
return null;
}
/**
* Return item at given position starting at 0
*
* @param integer $key
* @return mixed
*/
public function getAtPosition(int $key = 0, $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];
}
public function get(int|string $query, $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) && strpos($query, '.') === false) {
return array_key_exists($query, $this->items) ? $this->items[$query] : $default;
}
$query = explode('.', $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;
}
public function map(callable $callback): self
{
$items = array_map(function ($item) use ($callback) {
return $callback($item);
}, $this->items);
return new self($items);
}
/**
* Run callback on each item of the collection an remove it if it does not return true
*
* @param callable $callback
* @return Collection
*/
public function filter(callable $callback): self
{
$items = array_filter($this->items, function ($item) use ($callback) {
return $callback($item);
});
return new self($items);
}
public function pushEach(array $data, ?callable $callback = null): CollectionInterface
{
if (! is_iterable($data)) {
throw new RuntimeException('Unable to iterate given data.');
}
foreach ($data as $item) {
$this->push(is_callable($callback) ? $callback($item) : $item);
}
return $this;
}
public function empty(): CollectionInterface
{
$this->items = [];
return $this;
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
abstract class AbstractColor implements ColorInterface
{
/**
* Color channels
*/
protected array $channels;
public function channels(): array
{
return $this->channels;
}
public function channel(string $classname): ColorChannelInterface
{
$channels = array_filter($this->channels(), function (ColorChannelInterface $channel) use ($classname) {
return get_class($channel) == $classname;
});
if (count($channels) == 0) {
throw new ColorException('Channel ' . $classname . ' could not be found.');
}
return reset($channels);
}
public function normalize(): array
{
return array_map(function (ColorChannelInterface $channel) {
return $channel->normalize();
}, $this->channels());
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toArray()
*/
public function toArray(): array
{
return array_map(function (ColorChannelInterface $channel) {
return $channel->value();
}, $this->channels());
}
/**
* {@inheritdoc}
*
* @see ColorInterface::convertTo()
*/
public function convertTo(string|ColorspaceInterface $colorspace): ColorInterface
{
$colorspace = match (true) {
is_object($colorspace) => $colorspace,
default => new $colorspace(),
};
return $colorspace->importColor($this);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
abstract class AbstractColorChannel implements ColorChannelInterface
{
protected int $value;
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::__construct()
*/
public function __construct(int $value = null, float $normalized = null)
{
$this->value = $this->validate(
match (true) {
is_null($value) && is_numeric($normalized) => intval(round($normalized * $this->max())),
is_numeric($value) && is_null($normalized) => $value,
default => throw new ColorException('Color channels must either have a value or a normalized value')
}
);
}
/**
* Alias of value()
*
* @return int
*/
public function toInt(): int
{
return $this->value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::value()
*/
public function value(): int
{
return $this->value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::normalize()
*/
public function normalize($precision = 32): float
{
return round(($this->value() - $this->min()) / ($this->max() - $this->min()), $precision);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::validate()
*/
public function validate(mixed $value): mixed
{
if ($value < $this->min() || $value > $this->max()) {
throw new ColorException('Color channel value must be in range ' . $this->min() . ' to ' . $this->max());
}
return $value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::toString()
*/
public function toString(): string
{
return (string) $this->value();
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Cyan extends AbstractColorChannel
{
public function min(): int
{
return 0;
}
public function max(): int
{
return 100;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Channels;
class Key extends Cyan
{
//
}

View File

@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Channels;
class Magenta extends Cyan
{
//
}

View File

@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Channels;
class Yellow extends Cyan
{
//
}

86
src/Colors/Cmyk/Color.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace Intervention\Image\Colors\Cmyk;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Cmyk\Channels\Cyan;
use Intervention\Image\Colors\Cmyk\Channels\Magenta;
use Intervention\Image\Colors\Cmyk\Channels\Yellow;
use Intervention\Image\Colors\Cmyk\Channels\Key;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\AbstractInputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
public function __construct(int $c, int $m, int $y, int $k)
{
$this->channels = [
new Cyan($c),
new Magenta($m),
new Yellow($y),
new Key($k),
];
}
public static function create(mixed $input): ColorInterface
{
return (new class ([
Decoders\StringColorDecoder::class,
]) extends AbstractInputHandler
{
})->handle($input);
}
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
public function cyan(): ColorChannelInterface
{
return $this->channel(Cyan::class);
}
public function magenta(): ColorChannelInterface
{
return $this->channel(Magenta::class);
}
public function yellow(): ColorChannelInterface
{
return $this->channel(Yellow::class);
}
public function key(): ColorChannelInterface
{
return $this->channel(Key::class);
}
public function toString(): string
{
return sprintf(
'cmyk(%d%%, %d%%, %d%%, %d%%)',
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
$this->key()->value()
);
}
public function isGreyscale(): bool
{
return 0 === array_sum([
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
]);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Intervention\Image\Colors\Cmyk;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
public static $channels = [
Channels\Cyan::class,
Channels\Magenta::class,
Channels\Yellow::class,
Channels\Key::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::createColor()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
$values = array_map(function ($classname, $value_normalized) {
return (new $classname(normalized: $value_normalized))->value();
}, self::$channels, $normalized);
return new Color(...$values);
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match (get_class($color)) {
RgbColor::class => $this->importRgbColor($color),
HsvColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
HslColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
default => $color,
};
}
protected 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));
return new CmykColor($c, $m, $y, $k);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Intervention\Image\Colors\Cmyk\Decoders;
use Intervention\Image\Colors\Cmyk\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode CMYK color strings
*
* @param mixed $input
* @return ImageInterface|ColorInterface
*/
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^cmyk\((?P<c>[0-9\.]+%?), ?(?P<m>[0-9\.]+%?), ?(?P<y>[0-9\.]+%?), ?(?P<k>[0-9\.]+%?)\)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
$values = array_map(function ($value) {
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,28 @@
<?php
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Hue extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 360;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Luminance extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Saturation extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

104
src/Colors/Hsl/Color.php Normal file
View File

@ -0,0 +1,104 @@
<?php
namespace Intervention\Image\Colors\Hsl;
use Intervention\Image\Colors\AbstractColor;
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\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\AbstractInputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
public function __construct(int $h, int $s, int $l)
{
$this->channels = [
new Hue($h),
new Saturation($s),
new Luminance($l),
];
}
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return (new class ([
Decoders\StringColorDecoder::class,
]) extends AbstractInputHandler
{
})->handle($input);
}
/**
* Return the Hue channel
*
* @return ColorChannelInterface
*/
public function hue(): ColorChannelInterface
{
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel
*
* @return ColorChannelInterface
*/
public function saturation(): ColorChannelInterface
{
return $this->channel(Saturation::class);
}
/**
* Return the Luminance channel
*
* @return ColorChannelInterface
*/
public function luminance(): ColorChannelInterface
{
return $this->channel(Luminance::class);
}
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return sprintf(
'hsl(%d, %d%%, %d%%)',
$this->hue()->value(),
$this->saturation()->value(),
$this->luminance()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
return $this->saturation()->value() == 0;
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace Intervention\Image\Colors\Hsl;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
public static $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Luminance::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
$values = array_map(function ($classname, $value_normalized) {
return (new $classname(normalized: $value_normalized))->value();
}, self::$channels, $normalized);
return new Color(...$values);
}
public function importColor(ColorInterface $color): ColorInterface
{
return match (get_class($color)) {
CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
RgbColor::class => $this->importRgbColor($color),
HsvColor::class => $this->importHsvColor($color),
default => $color,
};
}
protected function importRgbColor(RgbColor $color): ColorInterface
{
// normalized values of rgb channels
$values = array_map(function ($channel) {
return $channel->normalize();
}, $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 = match (true) {
$delta == 0 => 0,
default => $delta / (1 - abs(2 * $luminance - 1)),
};
// calculate hue
list($r, $g, $b) = $values;
$hue = match (true) {
($delta == 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 = ($hue + 360) % 360; // normalize hue
return new Color(
intval(round($hue)),
intval(round($saturation * 100)),
intval(round($luminance * 100)),
);
}
protected function importHsvColor(HsvColor $color): ColorInterface
{
// normalized values of hsv channels
list($h, $s, $v) = array_map(function ($channel) {
return $channel->normalize();
}, $color->channels());
// calculate Luminance
$luminance = (2 - $s) * $v / 2;
// calculate Saturation
$saturation = match (true) {
$luminance == 0 => $s,
$luminance == 1 => 0,
$luminance < .5 => $s * $v / ($luminance * 2),
default => $s * $v / (2 - $luminance * 2),
};
return new Color(
intval(round($h * 360)),
intval(round($saturation * 100)),
intval(round($luminance * 100)),
);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Intervention\Image\Colors\Hsl\Decoders;
use Intervention\Image\Colors\Hsl\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode hsl color strings
*
* @param mixed $input
* @return ImageInterface|ColorInterface
*/
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^hsl\((?P<h>[0-9\.]+), ?(?P<s>[0-9\.]+%?), ?(?P<l>[0-9\.]+%?)?\)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
$values = array_map(function ($value) {
return match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(trim(str_replace('%', '', $value))),
};
}, [$matches['h'], $matches['s'], $matches['l']]);
return new Color(...$values);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Hue extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 360;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Saturation extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Value extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

104
src/Colors/Hsv/Color.php Normal file
View File

@ -0,0 +1,104 @@
<?php
namespace Intervention\Image\Colors\Hsv;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Hsv\Channels\Hue;
use Intervention\Image\Colors\Hsv\Channels\Saturation;
use Intervention\Image\Colors\Hsv\Channels\Value;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\AbstractInputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
public function __construct(int $h, int $s, int $v)
{
$this->channels = [
new Hue($h),
new Saturation($s),
new Value($v),
];
}
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return (new class ([
Decoders\StringColorDecoder::class,
]) extends AbstractInputHandler
{
})->handle($input);
}
/**
* Return the Hue channel
*
* @return ColorChannelInterface
*/
public function hue(): ColorChannelInterface
{
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel
*
* @return ColorChannelInterface
*/
public function saturation(): ColorChannelInterface
{
return $this->channel(Saturation::class);
}
/**
* Return the Value channel
*
* @return ColorChannelInterface
*/
public function value(): ColorChannelInterface
{
return $this->channel(Value::class);
}
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return sprintf(
'hsv(%d, %d%%, %d%%)',
$this->hue()->value(),
$this->saturation()->value(),
$this->value()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
return $this->saturation()->value() == 0;
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace Intervention\Image\Colors\Hsv;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
public static $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Value::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
$values = array_map(function ($classname, $value_normalized) {
return (new $classname(normalized: $value_normalized))->value();
}, self::$channels, $normalized);
return new Color(...$values);
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match (get_class($color)) {
CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
RgbColor::class => $this->importRgbColor($color),
HslColor::class => $this->importHslColor($color),
default => $color,
};
}
protected function importRgbColor(RgbColor $color): ColorInterface
{
// normalized values of rgb channels
$values = array_map(function ($channel) {
return $channel->normalize();
}, $color->channels());
// take only RGB
$values = array_slice($values, 0, 3);
// calculate chroma
$min = min(...$values);
$max = max(...$values);
$chroma = $max - $min;
// calculate value
$v = 100 * $max;
if ($chroma == 0) {
// greyscale color
return new Color(0, 0, intval(round($v)));
}
// calculate saturation
$s = 100 * ($chroma / $max);
// calculate hue
list($r, $g, $b) = $values;
$h = match (true) {
($r == $min) => 3 - (($g - $b) / $chroma),
($b == $min) => 1 - (($r - $g) / $chroma),
default => 5 - (($b - $r) / $chroma),
} * 60;
return new Color(
intval(round($h)),
intval(round($s)),
intval(round($v))
);
}
protected function importHslColor(HslColor $color): ColorInterface
{
// normalized values of hsl channels
list($h, $s, $l) = array_map(function ($channel) {
return $channel->normalize();
}, $color->channels());
$v = $l + $s * min($l, 1 - $l);
$s = ($v == 0) ? 0 : 2 * (1 - $l / $v);
return $this->colorFromNormalized([$h, $s, $v]);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Intervention\Image\Colors\Hsv\Decoders;
use Intervention\Image\Colors\Hsv\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode hsv/hsb color strings
*
* @param mixed $input
* @return ImageInterface|ColorInterface
*/
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^hs(v|b)\((?P<h>[0-9\.]+), ?(?P<s>[0-9\.]+%?), ?(?P<v>[0-9\.]+%?)?\)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
$values = array_map(function ($value) {
return match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(trim(str_replace('%', '', $value))),
};
}, [$matches['h'], $matches['s'], $matches['v']]);
return new Color(...$values);
}
}

10
src/Colors/Profile.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Intervention\Image\Colors;
use Intervention\Image\File;
use Intervention\Image\Interfaces\ProfileInterface;
class Profile extends File implements ProfileInterface
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace Intervention\Image\Colors\Rgb\Channels;
class Alpha extends Red
{
public function toString(): string
{
return strval(round($this->normalize(), 6));
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Rgb\Channels;
class Blue extends Red
{
//
}

View File

@ -0,0 +1,8 @@
<?php
namespace Intervention\Image\Colors\Rgb\Channels;
class Green extends Red
{
//
}

View File

@ -0,0 +1,28 @@
<?php
namespace Intervention\Image\Colors\Rgb\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Red extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 255;
}
}

166
src/Colors/Rgb/Color.php Normal file
View File

@ -0,0 +1,166 @@
<?php
namespace Intervention\Image\Colors\Rgb;
use Intervention\Image\Colors\AbstractColor;
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\Channels\Alpha;
use Intervention\Image\Drivers\AbstractInputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new instance
*
* @param int $r
* @param int $g
* @param int $b
* @param int $a
* @return ColorInterface
*/
public function __construct(int $r, int $g, int $b, int $a = 255)
{
$this->channels = [
new Red($r),
new Green($g),
new Blue($b),
new Alpha($a),
];
}
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return (new class ([
Decoders\HexColorDecoder::class,
Decoders\StringColorDecoder::class,
Decoders\TransparentColorDecoder::class,
Decoders\HtmlColornameDecoder::class,
]) extends AbstractInputHandler
{
})->handle($input);
}
/**
* Return the RGB red color channel
*
* @return ColorChannelInterface
*/
public function red(): ColorChannelInterface
{
return $this->channel(Red::class);
}
/**
* Return the RGB green color channel
*
* @return ColorChannelInterface
*/
public function green(): ColorChannelInterface
{
return $this->channel(Green::class);
}
/**
* Return the RGB blue color channel
*
* @return ColorChannelInterface
*/
public function blue(): ColorChannelInterface
{
return $this->channel(Blue::class);
}
/**
* Return the colors alpha channel
*
* @return ColorChannelInterface
*/
public function alpha(): ColorChannelInterface
{
return $this->channel(Alpha::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(string $prefix = ''): string
{
if ($this->isFullyOpaque()) {
return sprintf(
'%s%02x%02x%02x',
$prefix,
$this->red()->value(),
$this->green()->value(),
$this->blue()->value()
);
}
return sprintf(
'%s%02x%02x%02x%02x',
$prefix,
$this->red()->value(),
$this->green()->value(),
$this->blue()->value(),
$this->alpha()->value()
);
}
public function isFullyOpaque(): bool
{
return $this->alpha()->value() === 255;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
if ($this->isFullyOpaque()) {
return sprintf(
'rgb(%d, %d, %d)',
$this->red()->value(),
$this->green()->value(),
$this->blue()->value()
);
}
return sprintf(
'rgba(%d, %d, %d, %.1F)',
$this->red()->value(),
$this->green()->value(),
$this->blue()->value(),
$this->alpha()->normalize(),
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
$values = [$this->red()->value(), $this->green()->value(), $this->blue()->value()];
return count(array_unique($values, SORT_REGULAR)) === 1;
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Intervention\Image\Colors\Rgb;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
public static $channels = [
Channels\Red::class,
Channels\Green::class,
Channels\Blue::class,
Channels\Alpha::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
$values = array_map(function ($classname, $value_normalized) {
return (new $classname(normalized: $value_normalized))->value();
}, self::$channels, $normalized);
return new Color(...$values);
}
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::importColor()
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match (get_class($color)) {
CmykColor::class => $this->importCmykColor($color),
HsvColor::class => $this->importHsvColor($color),
HslColor::class => $this->importHslColor($color),
default => $color,
};
}
protected function importCmykColor(CmykColor $color): ColorInterface
{
return new Color(
(int) (255 * (1 - $color->cyan()->normalize()) * (1 - $color->key()->normalize())),
(int) (255 * (1 - $color->magenta()->normalize()) * (1 - $color->key()->normalize())),
(int) (255 * (1 - $color->yellow()->normalize()) * (1 - $color->key()->normalize())),
);
}
protected function importHsvColor(HsvColor $color): ColorInterface
{
$chroma = $color->value()->normalize() * $color->saturation()->normalize();
$hue = $color->hue()->normalize() * 6;
$x = $chroma * (1 - abs(fmod($hue, 2) - 1));
// connect channel values
$values = match (true) {
$hue < 1 => [$chroma, $x, 0],
$hue < 2 => [$x, $chroma, 0],
$hue < 3 => [0, $chroma, $x],
$hue < 4 => [0, $x, $chroma],
$hue < 5 => [$x, 0, $chroma],
default => [$chroma, 0, $x],
};
// add to each value
$values = array_map(function ($value) use ($color, $chroma) {
return $value + ($color->value()->normalize() - $chroma);
}, $values);
array_push($values, 1); // append alpha channel value
return $this->colorFromNormalized($values);
}
protected function importHslColor(HslColor $color): ColorInterface
{
// normalized values of hsl channels
list($h, $s, $l) = array_map(function ($channel) {
return $channel->normalize();
}, $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(function ($value) use ($m) {
return $value + $m;
}, $values);
array_push($values, 1); // append alpha channel value
return $this->colorFromNormalized($values);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class HexColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode hexadecimal rgb colors with and without transparency
*
* @param mixed $input
* @return ImageInterface|ColorInterface
*/
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^#?(?P<hex>[a-f\d]{3}(?:[a-f\d]?|(?:[a-f\d]{3}(?:[a-f\d]{2})?)?)\b)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
$values = str_split($matches['hex']);
$values = match (strlen($matches['hex'])) {
3, 4 => str_split($matches['hex']),
6, 8 => str_split($matches['hex'], 2),
default => throw new DecoderException('Unable to decode input'),
};
$values = array_map(function ($value) {
return match (strlen($value)) {
1 => hexdec($value . $value),
2 => hexdec($value),
default => throw new DecoderException('Unable to decode input'),
};
}, $values);
return new Color(...$values);
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class HtmlColornameDecoder extends HexColorDecoder implements DecoderInterface
{
protected static $names = [
'lightsalmon' => '#ffa07a',
'salmon' => '#fa8072',
'darksalmon' => '#e9967a',
'lightcoral' => '#f08080',
'indianred' => '#cd5c5c',
'crimson' => '#dc143c',
'firebrick' => '#b22222',
'red' => '#ff0000',
'darkred' => '#8b0000',
'coral' => '#ff7f50',
'tomato' => '#ff6347',
'orangered' => '#ff4500',
'gold' => '#ffd700',
'orange' => '#ffa500',
'darkorange' => '#ff8c00',
'lightyellow' => '#ffffe0',
'lemonchiffon' => '#fffacd',
'lightgoldenrodyellow' => '#fafad2',
'papayawhip' => '#ffefd5',
'moccasin' => '#ffe4b5',
'peachpuff' => '#ffdab9',
'palegoldenrod' => '#eee8aa',
'khaki' => '#f0e68c',
'darkkhaki' => '#bdb76b',
'yellow' => '#ffff00',
'lawngreen' => '#7cfc00',
'chartreuse' => '#7fff00',
'limegreen' => '#32cd32',
'lime' => '#00ff00',
'forestgreen' => '#228b22',
'green' => '#008000',
'darkgreen' => '#006400',
'greenyellow' => '#adff2f',
'yellowgreen' => '#9acd32',
'springgreen' => '#00ff7f',
'mediumspringgreen' => '#00fa9a',
'lightgreen' => '#90ee90',
'palegreen' => '#98fb98',
'darkseagreen' => '#8fbc8f',
'mediumseagre' => 'en #3cb371',
'seagreen' => '#2e8b57',
'olive' => '#808000',
'darkolivegreen' => '#556b2f',
'olivedrab' => '#6b8e23',
'lightcyan' => '#e0ffff',
'cyan' => '#00ffff',
'aqua' => '#00ffff',
'aquamarine' => '#7fffd4',
'mediumaquamarine' => '#66cdaa',
'paleturquoise' => '#afeeee',
'turquoise' => '#40e0d0',
'mediumturquoise' => '#48d1cc',
'darkturquoise' => '#00ced1',
'lightseagreen' => '#20b2aa',
'cadetblue' => '#5f9ea0',
'darkcyan' => '#008b8b',
'teal' => '#008080',
'powderblue' => '#b0e0e6',
'lightblue' => '#add8e6',
'lightskyblue' => '#87cefa',
'skyblue' => '#87ceeb',
'deepskyblue' => '#00bfff',
'lightsteelblue' => '#b0c4de',
'dodgerblue' => '#1e90ff',
'cornflowerblue' => '#6495ed',
'steelblue' => '#4682b4',
'royalblue' => '#4169e1',
'blue' => '#0000ff',
'mediumblue' => '#0000cd',
'darkblue' => '#00008b',
'navy' => '#000080',
'midnightblue' => '#191970',
'mediumslateblue' => '#7b68ee',
'slateblue' => '#6a5acd',
'darkslateblue' => '#483d8b',
'lavender' => '#e6e6fa',
'thistle' => '#d8bfd8',
'plum' => '#dda0dd',
'violet' => '#ee82ee',
'orchid' => '#da70d6',
'fuchsia' => '#ff00ff',
'magenta' => '#ff00ff',
'mediumorchid' => '#ba55d3',
'mediumpurple' => '#9370db',
'blueviolet' => '#8a2be2',
'darkviolet' => '#9400d3',
'darkorchid' => '#9932cc',
'darkmagenta' => '#8b008b',
'purple' => '#800080',
'indigo' => '#4b0082',
'pink' => '#ffc0cb',
'lightpink' => '#ffb6c1',
'hotpink' => '#ff69b4',
'deeppink' => '#ff1493',
'palevioletred' => '#db7093',
'mediumvioletred' => '#c71585',
'white' => '#ffffff',
'snow' => '#fffafa',
'honeydew' => '#f0fff0',
'mintcream' => '#f5fffa',
'azure' => '#f0ffff',
'aliceblue' => '#f0f8ff',
'ghostwhite' => '#f8f8ff',
'whitesmoke' => '#f5f5f5',
'seashell' => '#fff5ee',
'beige' => '#f5f5dc',
'oldlace' => '#fdf5e6',
'floralwhite' => '#fffaf0',
'ivory' => '#fffff0',
'antiquewhite' => '#faebd7',
'linen' => '#faf0e6',
'lavenderblush' => '#fff0f5',
'mistyrose' => '#ffe4e1',
'gainsboro' => '#dcdcdc',
'lightgray' => '#d3d3d3',
'silver' => '#c0c0c0',
'darkgray' => '#a9a9a9',
'gray' => '#808080',
'dimgray' => '#696969',
'lightslategray' => '#778899',
'slategray' => '#708090',
'darkslategray' => '#2f4f4f',
'black' => '#000000',
'cornsilk' => '#fff8dc',
'blanchedalmond' => '#ffebcd',
'bisque' => '#ffe4c4',
'navajowhite' => '#ffdead',
'wheat' => '#f5deb3',
'burlywood' => '#deb887',
'tan' => '#d2b48c',
'rosybrown' => '#bc8f8f',
'sandybrown' => '#f4a460',
'goldenrod' => '#daa520',
'peru' => '#cd853f',
'chocolate' => '#d2691e',
'saddlebrown' => '#8b4513',
'sienna' => '#a0522d',
'brown' => '#a52a2a',
'maroon' => '#800000',
];
/**
* Decode html color names
*
* @param mixed $input
* @return ImageInterface|ColorInterface
*/
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
if (!array_key_exists(strtolower($input), static::$names)) {
throw new DecoderException('Unable to decode input');
}
return parent::decode(static::$names[strtolower($input)]);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode rgb color strings
*
* @param mixed $input
* @return ImageInterface|ColorInterface
*/
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^s?rgba?\((?P<r>[0-9\.]+%?), ?(?P<g>[0-9\.]+%?), ?(?P<b>[0-9\.]+%?)(?:, ?(?P<a>(?:1)|(?:1\.0*)|(?:0)|(?:0?\.\d+%?)|(?:\d{1,3}%)))?\)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
// rgb values
$values = array_map(function ($value) {
return 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 (true) {
strpos($matches['a'], '%') => round(intval(trim(str_replace('%', '', $matches['a']))) / 2.55),
default => intval(round(floatval(trim($matches['a'])) * 255)),
};
}
return new Color(...$values);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ColorInterface;
class TransparentColorDecoder extends HexColorDecoder
{
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
if (strtolower($input) != 'transparent') {
throw new DecoderException('Unable to decode input');
}
return parent::decode('#ff00ff00');
}
}

View File

@ -0,0 +1,179 @@
<?php
namespace Intervention\Image\Drivers;
use Exception;
use Intervention\Image\Collection;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\CollectionInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Traits\CanBuildFilePointer;
abstract class AbstractDecoder implements DecoderInterface
{
use CanBuildFilePointer;
public function __construct(protected ?AbstractDecoder $successor = null)
{
//
}
/**
* Try to decode given input to image or color object
*
* @param mixed $input
* @return ImageInterface|ColorInterface
* @throws DecoderException
*/
final public function handle($input): ImageInterface|ColorInterface
{
try {
$decoded = $this->decode($input);
} catch (DecoderException $e) {
if (!$this->hasSuccessor()) {
throw new DecoderException($e->getMessage());
}
return $this->successor->handle($input);
}
return $decoded;
}
/**
* Determine if current decoder has a successor
*
* @return bool
*/
protected function hasSuccessor(): bool
{
return $this->successor !== null;
}
/**
* Return media type (MIME) of given input
*
* @param string $input
* @return string
*/
protected function mediaType(string $input): string
{
$pointer = $this->buildFilePointer($input);
$type = mime_content_type($pointer);
fclose($pointer);
return $type;
}
/**
* Extract and return EXIF data from given image data string
*
* @param string $image_data
* @return CollectionInterface
*/
protected function extractExifData(string $image_data): CollectionInterface
{
if (!function_exists('exif_read_data')) {
return new Collection();
}
try {
$pointer = $this->buildFilePointer($image_data);
$data = @exif_read_data($pointer, null, true);
fclose($pointer);
} catch (Exception $e) {
$data = [];
}
return new Collection(is_array($data) ? $data : []);
}
/**
* Determine if given input is base64 encoded data
*
* @param mixed $input
* @return bool
*/
protected function isValidBase64(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
return base64_encode(base64_decode($input)) === str_replace(["\n", "\r"], '', $input);
}
/**
* Parse data uri
*
* @param mixed $value
* @return object
*/
protected function parseDataUri($value): object
{
$pattern = "/^data:(?P<mediatype>\w+\/[-+.\w]+)?" .
"(?P<parameters>(;[-\w]+=[-\w]+)*)(?P<base64>;base64)?,(?P<data>.*)/";
$result = preg_match($pattern, $value, $matches);
return new class ($matches, $result)
{
private $matches;
private $result;
public function __construct($matches, $result)
{
$this->matches = $matches;
$this->result = $result;
}
public function isValid(): bool
{
return (bool) $this->result;
}
public function mediaType(): ?string
{
if (isset($this->matches['mediatype']) && !empty($this->matches['mediatype'])) {
return $this->matches['mediatype'];
}
return null;
}
public function hasMediaType(): bool
{
return !empty($this->mediaType());
}
public function parameters(): array
{
if (isset($this->matches['parameters']) && !empty($this->matches['parameters'])) {
return explode(';', trim($this->matches['parameters'], ';'));
}
return [];
}
public function isBase64Encoded(): bool
{
if (isset($this->matches['base64']) && $this->matches['base64'] === ';base64') {
return true;
}
return false;
}
public function data(): ?string
{
if (isset($this->matches['data']) && !empty($this->matches['data'])) {
return $this->matches['data'];
}
return null;
}
};
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Intervention\Image\Drivers;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DrawableInterface;
use Intervention\Image\Interfaces\PointInterface;
/**
* @property DrawableInterface $drawable
*/
abstract class AbstractDrawModifier extends DriverSpecializedModifier
{
public function position(): PointInterface
{
return $this->drawable->position();
}
public function backgroundColor(): ColorInterface
{
try {
$color = $this->driver()->handleInput($this->drawable->backgroundColor());
} catch (DecoderException $e) {
return $this->driver()->handleInput('transparent');
}
return $color;
}
public function borderColor(): ColorInterface
{
try {
$color = $this->driver()->handleInput($this->drawable->borderColor());
} catch (DecoderException $e) {
return $this->driver()->handleInput('transparent');
}
return $color;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Intervention\Image\Drivers;
use Intervention\Image\Analyzers\AbstractAnalyzer;
use Intervention\Image\Encoders\AbstractEncoder;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Modifiers\AbstractModifier;
use ReflectionClass;
abstract class AbstractDriver implements DriverInterface
{
/**
* Return a specialized version for the current driver of the given object
*
* @param object $input
* @return object
* @throws NotSupportedException
*/
public function resolve(object $input): object
{
if ($this->isExternal($input)) {
return $input;
}
$driver_namespace = (new ReflectionClass($this))->getNamespaceName();
$class_path = substr(get_class($input), strlen("Intervention\\Image\\"));
$specialized = $driver_namespace . "\\" . $class_path;
if (! class_exists($specialized)) {
throw new NotSupportedException(
"Class '" . $class_path . "' is not supported by " . $this->id() . " driver."
);
}
return new $specialized($input, $this);
}
/**
* Determine if given object is external custom modifier, analyzer or encoder
*
* @param object $input
* @return bool
*/
private function isExternal(object $input): bool
{
if ($input instanceof AbstractModifier) {
return false;
}
if ($input instanceof AbstractAnalyzer) {
return false;
}
if ($input instanceof AbstractEncoder) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Intervention\Image\Drivers;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\InputHandlerInterface;
abstract class AbstractInputHandler implements InputHandlerInterface
{
protected array $decoders = [];
public function __construct(array $decoders = [])
{
$this->decoders = count($decoders) ? $decoders : $this->decoders;
}
/**
* {@inheritdoc}
*
* @see InputHandlerInterface::handle()
*/
public function handle($input): ImageInterface|ColorInterface
{
return $this->chain()->handle($input);
}
/**
* Stack the decoder array into a nested decoder object
*
* @return AbstractDecoder
*/
protected function chain(): AbstractDecoder
{
if (count($this->decoders) == 0) {
throw new DecoderException('No decoders found in ' . get_class($this));
}
// get instance of last decoder in stack
list($classname) = array_slice(array_reverse($this->decoders), 0, 1);
$chain = new $classname();
// build decoder chain
foreach (array_slice(array_reverse($this->decoders), 1) as $classname) {
$chain = new $classname($chain);
}
return $chain;
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Intervention\Image\Drivers;
use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Typography\TextBlock;
use Intervention\Image\Typography\Line;
/**
* @property FontInterface $font
*/
abstract class AbstractTextModifier extends DriverSpecializedModifier
{
abstract protected function boxSize(string $text): Polygon;
public function leadingInPixels(): int
{
return intval(round($this->fontSizeInPixels() * $this->font->lineHeight()));
}
public function capHeight(): int
{
return $this->boxSize('T')->height();
}
public function fontSizeInPixels(): int
{
return $this->boxSize('Hy')->height();
}
/**
* Build TextBlock object from text string and align every line
* according to text writers font object and position.
*
* @return TextBlock
*/
public function alignedTextBlock(Point $position, string $text): TextBlock
{
$lines = new TextBlock($text);
$boundingBox = $this->boundingBox($lines, $position);
$pivot = $boundingBox->last();
$leading = $this->leadingInPixels();
$blockWidth = $this->lineWidth($lines->longestLine());
$x = $pivot->x();
$y = $this->font->hasFilename() ? $pivot->y() + $this->capHeight() : $pivot->y();
$x_adjustment = 0;
foreach ($lines as $line) {
$line_width = $this->lineWidth($line);
$x_adjustment = $this->font->alignment() == 'left' ? 0 : $blockWidth - $line_width;
$x_adjustment = $this->font->alignment() == 'right' ? intval(round($x_adjustment)) : $x_adjustment;
$x_adjustment = $this->font->alignment() == 'center' ? intval(round($x_adjustment / 2)) : $x_adjustment;
$position = new Point($x + $x_adjustment, $y);
$position->rotate($this->font->angle(), $pivot);
$line->setPosition($position);
$y += $leading;
}
return $lines;
}
public function boundingBox(TextBlock $block, Point $pivot = null): Polygon
{
$pivot = $pivot ? $pivot : new Point();
// bounding box
$box = (new Rectangle(
$this->lineWidth($block->longestLine()),
$this->leadingInPixels() * ($block->count() - 1) + $this->capHeight()
));
// set pivot
$box->setPivot($pivot);
// align
$box->align($this->font->alignment());
$box->valign($this->font->valignment());
$box->rotate($this->font->angle());
return $box;
}
private function lineWidth(Line $line): int
{
return $this->boxSize((string) $line)->width();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Intervention\Image\Drivers;
use Intervention\Image\Interfaces\AnalyzerInterface;
use Intervention\Image\Interfaces\DriverInterface;
abstract class DriverSpecializedAnalyzer implements AnalyzerInterface
{
public function __construct(
protected AnalyzerInterface $analyzer,
protected DriverInterface $driver
) {
}
public function driver(): DriverInterface
{
return $this->driver;
}
/**
* Magic method to read attributes of underlying analyzer
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
return $this->analyzer->$name;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Intervention\Image\Drivers;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\EncoderInterface;
abstract class DriverSpecializedEncoder implements EncoderInterface
{
public function __construct(
protected EncoderInterface $encoder,
protected DriverInterface $driver
) {
}
public function driver(): DriverInterface
{
return $this->driver;
}
/**
* Magic method to read attributes of underlying endcoder
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
return $this->encoder->$name;
}
/**
* Get return value of callback through output buffer
*
* @param callable $callback
* @return string
*/
protected function getBuffered(callable $callback): string
{
ob_start();
$callback();
$buffer = ob_get_contents();
ob_end_clean();
return $buffer;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Intervention\Image\Drivers;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\ModifierInterface;
abstract class DriverSpecializedModifier implements ModifierInterface
{
public function __construct(
protected ModifierInterface $modifier,
protected DriverInterface $driver
) {
}
public function driver(): DriverInterface
{
return $this->driver;
}
/**
* Magic method to read attributes of underlying modifier
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
return $this->modifier->$name;
}
/**
* Magic method to call methods of underlying modifier
*
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call(string $name, array $arguments): mixed
{
return $this->modifier->$name(...$arguments);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Colors\Rgb\Colorspace;
use Intervention\Image\Drivers\DriverSpecializedAnalyzer;
use Intervention\Image\Interfaces\ImageInterface;
class ColorspaceAnalyzer extends DriverSpecializedAnalyzer
{
public function analyze(ImageInterface $image): mixed
{
return new Colorspace();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Drivers\DriverSpecializedAnalyzer;
use Intervention\Image\Interfaces\ImageInterface;
class HeightAnalyzer extends DriverSpecializedAnalyzer
{
public function analyze(ImageInterface $image): mixed
{
return imagesy($image->core()->native());
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Intervention\Image\Drivers\Gd\Analyzers;
use GdImage;
use Intervention\Image\Drivers\DriverSpecializedAnalyzer;
use Intervention\Image\Exceptions\GeometryException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $x
* @property int $y
* @property int $frame_key
*/
class PixelColorAnalyzer extends DriverSpecializedAnalyzer
{
public function analyze(ImageInterface $image): mixed
{
return $this->colorAt(
$image->colorspace(),
$image->core()->frame($this->frame_key)->native()
);
}
protected function colorAt(ColorspaceInterface $colorspace, GdImage $gd): ColorInterface
{
$index = @imagecolorat($gd, $this->x, $this->y);
if ($index === false) {
throw new GeometryException(
'The specified position is not in the valid image area.'
);
}
return $this->driver()->colorProcessor($colorspace)->nativeToColor($index);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Collection;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $x
* @property int $y
*/
class PixelColorsAnalyzer extends PixelColorAnalyzer
{
public function analyze(ImageInterface $image): mixed
{
$colors = new Collection();
$colorspace = $image->colorspace();
foreach ($image as $frame) {
$colors->push(
parent::colorAt($colorspace, $frame->native())
);
}
return $colors;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Drivers\DriverSpecializedAnalyzer;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Resolution;
class ResolutionAnalyzer extends DriverSpecializedAnalyzer
{
public function analyze(ImageInterface $image): mixed
{
return new Resolution(...imageresolution($image->core()->native()));
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Drivers\DriverSpecializedAnalyzer;
use Intervention\Image\Interfaces\ImageInterface;
class WidthAnalyzer extends DriverSpecializedAnalyzer
{
public function analyze(ImageInterface $image): mixed
{
return imagesx($image->core()->native());
}
}

View File

@ -0,0 +1,78 @@
<?php
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;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class ColorProcessor implements ColorProcessorInterface
{
public function __construct(protected ColorspaceInterface $colorspace = new Colorspace())
{
}
public function colorToNative(ColorInterface $color): int
{
// convert color to colorspace
$color = $color->convertTo($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();
// convert alpha value to gd alpha
// ([opaque]255-0[transparent]) to ([opaque]0-127[transparent])
$a = (int) $this->convertRange($a, 0, 255, 127, 0);
return ($a << 24) + ($r << 16) + ($g << 8) + $b;
}
public function nativeToColor(mixed $value): ColorInterface
{
if (! is_int($value)) {
throw new ColorException("GD driver can only decode colors in integer format.");
}
$a = ($value >> 24) & 0xFF;
$r = ($value >> 16) & 0xFF;
$g = ($value >> 8) & 0xFF;
$b = $value & 0xFF;
// convert gd apha integer to intervention alpha integer
// ([opaque]0-127[transparent]) to ([opaque]255-0[transparent])
$a = (int) static::convertRange($a, 127, 0, 0, 255);
return new Color($r, $g, $b, $a);
}
/**
* Convert input in range (min) to (max) to the corresponding value
* in target range (targetMin) to (targetMax).
*
* @param float|int $input
* @param float|int $min
* @param float|int $max
* @param float|int $targetMin
* @param float|int $targetMax
* @return float|int
*/
protected function convertRange(
float|int $input,
float|int $min,
float|int $max,
float|int $targetMin,
float|int $targetMax
): float|int {
return ceil(((($input - $min) * ($targetMax - $targetMin)) / ($max - $min)) + $targetMin);
}
}

58
src/Drivers/Gd/Core.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Collection;
use Intervention\Image\Interfaces\CoreInterface;
use Intervention\Image\Interfaces\FrameInterface;
class Core extends Collection implements CoreInterface
{
protected int $loops = 0;
public function add(FrameInterface $frame): CoreInterface
{
$this->push($frame);
return $this;
}
public function native(): mixed
{
return $this->first()->native();
}
public function setNative(mixed $native): self
{
$this->empty()->push(new Frame($native));
return $this;
}
public function frame(int $position): FrameInterface
{
return $this->getAtPosition($position);
}
public function loops(): int
{
return $this->loops;
}
public function setLoops(int $loops): self
{
$this->loops = $loops;
return $this;
}
public function first(): FrameInterface
{
return parent::first();
}
public function last(): FrameInterface
{
return parent::last();
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class Base64ImageDecoder extends BinaryImageDecoder implements DecoderInterface
{
public function decode($input): ImageInterface|ColorInterface
{
if (! $this->isValidBase64($input)) {
throw new DecoderException('Unable to decode input');
}
return parent::decode(base64_decode($input));
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Gif\Decoder as GifDecoder;
use Intervention\Gif\Splitter as GifSplitter;
use Intervention\Image\Drivers\Gd\Core;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Image;
class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
{
public function decode($input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
if ($this->mediaType($input) == 'image/gif') {
return $this->decodeGif($input); // decode (animated) gif
}
// build image instance
$image = new Image(
new Driver(),
$this->coreFromString($input),
$this->extractExifData($input)
);
// fix image orientation
return match ($image->exif('IFD0.Orientation')) {
2 => $image->flip(),
3 => $image->rotate(180),
4 => $image->rotate(180)->flip(),
5 => $image->rotate(270)->flip(),
6 => $image->rotate(270),
7 => $image->rotate(90)->flip(),
8 => $image->rotate(90),
default => $image
};
}
private function coreFromString(string $input): Core
{
$data = @imagecreatefromstring($input);
if ($data === false) {
throw new DecoderException('Unable to decode input');
}
if (!imageistruecolor($data)) {
imagepalettetotruecolor($data);
}
imagesavealpha($data, true);
return new Core([
new Frame($data)
]);
}
private function decodeGif(string $input): ImageInterface
{
$gif = GifDecoder::decode($input);
if (!$gif->isAnimated()) {
return new Image(
new Driver(),
$this->coreFromString($input)
);
}
$splitter = GifSplitter::create($gif)->split();
$delays = $splitter->getDelays();
// build core
$core = new Core();
$core->setLoops($gif->getMainApplicationExtension()?->getLoops());
foreach ($splitter->coalesceToResources() as $key => $data) {
$core->push(
(new Frame($data))->setDelay($delays[$key] / 100)
);
}
return new Image(
new Driver(),
$core
);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class ColorObjectDecoder extends AbstractDecoder implements DecoderInterface
{
public function decode($input): ImageInterface|ColorInterface
{
if (! is_a($input, ColorInterface::class)) {
throw new DecoderException('Unable to decode input');
}
return $input;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class DataUriImageDecoder extends BinaryImageDecoder implements DecoderInterface
{
public function decode($input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$uri = $this->parseDataUri($input);
if (! $uri->isValid()) {
throw new DecoderException('Unable to decode input');
}
if ($uri->isBase64Encoded()) {
return parent::decode(base64_decode($uri->data()));
}
return parent::decode(urldecode($uri->data()));
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use Exception;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterface
{
public function decode($input): ImageInterface|ColorInterface
{
if (! is_string($input)) {
throw new DecoderException('Unable to decode input');
}
try {
if (! @is_file($input)) {
throw new DecoderException('Unable to decode input');
}
} catch (Exception $e) {
throw new DecoderException('Unable to decode input');
}
return parent::decode(file_get_contents($input));
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ImageInterface;
class FilePointerImageDecoder extends BinaryImageDecoder
{
public function decode($input): ImageInterface|ColorInterface
{
if (!is_resource($input) || !in_array(get_resource_type($input), ['file', 'stream'])) {
throw new DecoderException('Unable to decode input');
}
$contents = '';
@rewind($input);
while (!feof($input)) {
$contents .= fread($input, 1024);
}
return parent::decode($contents);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class ImageObjectDecoder extends AbstractDecoder implements DecoderInterface
{
public function decode($input): ImageInterface|ColorInterface
{
if (! is_a($input, ImageInterface::class)) {
throw new DecoderException('Unable to decode input');
}
return $input;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Intervention\Image\Drivers\Gd\Decoders;
use SplFileInfo;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class SplFileInfoImageDecoder extends FilePathImageDecoder implements DecoderInterface
{
public function decode($input): ImageInterface|ColorInterface
{
if (! is_a($input, SplFileInfo::class)) {
throw new DecoderException('Unable to decode input');
}
return parent::decode($input->getRealPath());
}
}

105
src/Drivers/Gd/Driver.php Normal file
View File

@ -0,0 +1,105 @@
<?php
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Drivers\AbstractDriver;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\ImageInterface;
class Driver extends AbstractDriver
{
/**
* {@inheritdoc}
*
* @see DriverInterface::id()
*/
public function id(): string
{
return 'GD';
}
/**
* {@inheritdoc}
*
* @see DriverInterface::createImage()
*/
public function createImage(int $width, int $height): ImageInterface
{
// build new transparent GDImage
$data = imagecreatetruecolor($width, $height);
imagesavealpha($data, true);
$background = imagecolorallocatealpha($data, 255, 0, 255, 127);
imagealphablending($data, false);
imagefill($data, 0, 0, $background);
imagecolortransparent($data, $background);
return new Image(
$this,
new Core([
new Frame($data)
])
);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::createAnimation()
*/
public function createAnimation(callable $init): ImageInterface
{
$animation = new class ($this)
{
public function __construct(
protected DriverInterface $driver,
public Core $core = new Core()
) {
}
public function add($source, float $delay = 1): self
{
$this->core->add(
$this->driver->handleInput($source)->core()->first()->setDelay($delay)
);
return $this;
}
public function __invoke(): ImageInterface
{
return new Image(
$this->driver,
$this->core
);
}
};
$init($animation);
return call_user_func($animation);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::handleInput()
*/
public function handleInput(mixed $input): ImageInterface|ColorInterface
{
return (new InputHandler())->handle($input);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::colorProcessor()
*/
public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface
{
return new ColorProcessor($colorspace);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Drivers\DriverSpecializedEncoder;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $quality
*/
class AvifEncoder extends DriverSpecializedEncoder
{
public function encode(ImageInterface $image): EncodedImage
{
$gd = $image->core()->native();
$data = $this->getBuffered(function () use ($gd) {
imageavif($gd, null, $this->quality);
});
return new EncodedImage($data, 'image/avif');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Drivers\DriverSpecializedEncoder;
use Intervention\Image\Modifiers\LimitColorsModifier;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $color_limit
*/
class BmpEncoder extends DriverSpecializedEncoder
{
public function encode(ImageInterface $image): EncodedImage
{
$image = $image->modify(new LimitColorsModifier($this->color_limit));
$gd = $image->core()->native();
$data = $this->getBuffered(function () use ($gd) {
imagebmp($gd, null, false);
});
return new EncodedImage($data, 'image/bmp');
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Gif\Builder as GifBuilder;
use Intervention\Image\Drivers\DriverSpecializedEncoder;
use Intervention\Image\Modifiers\LimitColorsModifier;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $color_limit
*/
class GifEncoder extends DriverSpecializedEncoder
{
public function encode(ImageInterface $image): EncodedImage
{
if ($image->isAnimated()) {
return $this->encodeAnimated($image);
}
$image = $image->modify(new LimitColorsModifier($this->color_limit));
$gd = $image->core()->native();
$data = $this->getBuffered(function () use ($gd) {
imagegif($gd);
});
return new EncodedImage($data, 'image/gif');
}
protected function encodeAnimated(ImageInterface $image): EncodedImage
{
$builder = GifBuilder::canvas(
$image->width(),
$image->height(),
$image->loops()
);
foreach ($image as $frame) {
$builder->addFrame(
$this->encode($frame->toImage($image->driver())),
$frame->delay()
);
}
return new EncodedImage($builder->encode(), 'image/gif');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Drivers\DriverSpecializedEncoder;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $quality
*/
class JpegEncoder extends DriverSpecializedEncoder
{
public function encode(ImageInterface $image): EncodedImage
{
$gd = $image->core()->native();
$data = $this->getBuffered(function () use ($gd) {
imagejpeg($gd, null, $this->quality);
});
return new EncodedImage($data, 'image/jpeg');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Drivers\DriverSpecializedEncoder;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Modifiers\LimitColorsModifier;
/**
* @property int $color_limit
*/
class PngEncoder extends DriverSpecializedEncoder
{
public function encode(ImageInterface $image): EncodedImage
{
$image = $image->modify(new LimitColorsModifier($this->color_limit));
$gd = $image->core()->native();
$data = $this->getBuffered(function () use ($gd) {
imagepng($gd, null, -1);
});
return new EncodedImage($data, 'image/png');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Drivers\DriverSpecializedEncoder;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $quality
*/
class WebpEncoder extends DriverSpecializedEncoder
{
public function encode(ImageInterface $image): EncodedImage
{
$gd = $image->core()->native();
$data = $this->getBuffered(function () use ($gd) {
imagewebp($gd, null, $this->quality);
});
return new EncodedImage($data, 'image/webp');
}
}

102
src/Drivers/Gd/Frame.php Normal file
View File

@ -0,0 +1,102 @@
<?php
namespace Intervention\Image\Drivers\Gd;
use GdImage;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
class Frame implements FrameInterface
{
public function __construct(
protected GdImage $native,
protected float $delay = 0,
protected int $dispose = 1,
protected int $offset_left = 0,
protected int $offset_top = 0
) {
//
}
public function toImage(DriverInterface $driver): ImageInterface
{
return new Image($driver, new Core([$this]));
}
public function setNative($native): FrameInterface
{
$this->native = $native;
return $this;
}
public function native(): GdImage
{
return $this->native;
}
public function size(): SizeInterface
{
return new Rectangle(imagesx($this->native), imagesy($this->native));
}
public function delay(): float
{
return $this->delay;
}
public function setDelay(float $delay): FrameInterface
{
$this->delay = $delay;
return $this;
}
public function dispose(): int
{
return $this->dispose;
}
public function setDispose(int $dispose): FrameInterface
{
$this->dispose = $dispose;
return $this;
}
public function setOffset(int $left, int $top): FrameInterface
{
$this->offset_left = $left;
$this->offset_top = $top;
return $this;
}
public function offsetLeft(): int
{
return $this->offset_left;
}
public function setOffsetLeft(int $offset): FrameInterface
{
$this->offset_left = $offset;
return $this;
}
public function offsetTop(): int
{
return $this->offset_top;
}
public function setOffsetTop(int $offset): FrameInterface
{
$this->offset_top = $offset;
return $this;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Colors\Rgb\Decoders\HexColorDecoder as RgbHexColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\StringColorDecoder as RgbStringColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\HtmlColornameDecoder;
use Intervention\Image\Colors\Rgb\Decoders\TransparentColorDecoder;
use Intervention\Image\Colors\Cmyk\Decoders\StringColorDecoder as CmykStringColorDecoder;
use Intervention\Image\Colors\Hsv\Decoders\StringColorDecoder as HsvStringColorDecoder;
use Intervention\Image\Colors\Hsl\Decoders\StringColorDecoder as HslStringColorDecoder;
use Intervention\Image\Drivers\AbstractInputHandler;
use Intervention\Image\Drivers\Gd\Decoders\ImageObjectDecoder;
use Intervention\Image\Drivers\Gd\Decoders\ColorObjectDecoder;
use Intervention\Image\Drivers\Gd\Decoders\FilePointerImageDecoder;
use Intervention\Image\Drivers\Gd\Decoders\FilePathImageDecoder;
use Intervention\Image\Drivers\Gd\Decoders\BinaryImageDecoder;
use Intervention\Image\Drivers\Gd\Decoders\DataUriImageDecoder;
use Intervention\Image\Drivers\Gd\Decoders\Base64ImageDecoder;
use Intervention\Image\Drivers\Gd\Decoders\SplFileInfoImageDecoder;
class InputHandler extends AbstractInputHandler
{
protected array $decoders = [
ImageObjectDecoder::class,
ColorObjectDecoder::class,
RgbHexColorDecoder::class,
RgbStringColorDecoder::class,
CmykStringColorDecoder::class,
HsvStringColorDecoder::class,
HslStringColorDecoder::class,
TransparentColorDecoder::class,
HtmlColornameDecoder::class,
FilePointerImageDecoder::class,
FilePathImageDecoder::class,
SplFileInfoImageDecoder::class,
BinaryImageDecoder::class,
DataUriImageDecoder::class,
Base64ImageDecoder::class,
];
}

View File

@ -0,0 +1,23 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $amount
*/
class BlurModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
for ($i = 0; $i < $this->amount; $i++) {
imagefilter($frame->native(), IMG_FILTER_GAUSSIAN_BLUR);
}
}
return $image;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $level
*/
class BrightnessModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
imagefilter($frame->native(), IMG_FILTER_BRIGHTNESS, intval($this->level * 2.55));
}
return $image;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $red
* @property int $green
* @property int $blue
*/
class ColorizeModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
// normalize colorize levels
$red = round($this->red * 2.55);
$green = round($this->green * 2.55);
$blue = round($this->blue * 2.55);
foreach ($image as $frame) {
imagefilter($frame->native(), IMG_FILTER_COLORIZE, $red, $green, $blue);
}
return $image;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @method ColorspaceInterface targetColorspace()
*/
class ColorspaceModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
if (!is_a($this->targetColorspace(), RgbColorspace::class)) {
throw new NotSupportedException(
'Only RGB colorspace is supported with GD driver.'
);
}
return $image;
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Colors\Rgb\Channels\Blue;
use Intervention\Image\Colors\Rgb\Channels\Green;
use Intervention\Image\Colors\Rgb\Channels\Red;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Modifiers\FillModifier;
/**
* @method SizeInterface getCropSize(ImageInterface $image)
* @method SizeInterface getResizeSize(ImageInterface $image)
* @property int $width
* @property int $height
* @property mixed $background
* @property string $position
*/
class ContainModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
$crop = $this->getCropSize($image);
$resize = $this->getResizeSize($image);
$background = $this->driver()->handleInput($this->background);
foreach ($image as $frame) {
$this->modify($frame, $crop, $resize, $background);
}
return $image;
}
protected function modify(
FrameInterface $frame,
SizeInterface $crop,
SizeInterface $resize,
ColorInterface $background
): void {
// create new gd image
$modified = $this->driver()->createImage(
$resize->width(),
$resize->height()
)->modify(
new FillModifier($background)
)->core()->native();
// make image area transparent to keep transparency
// even if background-color is set
$transparent = imagecolorallocatealpha(
$modified,
$background->channel(Red::class)->value(),
$background->channel(Green::class)->value(),
$background->channel(Blue::class)->value(),
127,
);
imagealphablending($modified, false); // do not blend / just overwrite
imagecolortransparent($modified, $transparent);
imagefilledrectangle(
$modified,
$crop->pivot()->x(),
$crop->pivot()->y(),
$crop->pivot()->x() + $crop->width() - 1,
$crop->pivot()->y() + $crop->height() - 1,
$transparent
);
// copy image from original with blending alpha
imagealphablending($modified, true);
imagecopyresampled(
$modified,
$frame->native(),
$crop->pivot()->x(),
$crop->pivot()->y(),
0,
0,
$crop->width(),
$crop->height(),
$frame->size()->width(),
$frame->size()->height()
);
// set new content as recource
$frame->setNative($modified);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property int $level
*/
class ContrastModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
imagefilter($frame->native(), IMG_FILTER_CONTRAST, ($this->level * -1));
}
return $image;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\SizeInterface;
/**
* @property int $width
* @property int $height
*/
class CoverDownModifier extends CoverModifier
{
public function getResizeSize(SizeInterface $size): SizeInterface
{
return $size->scaleDown($this->width, $this->height);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
/**
* @method SizeInterface getResizeSize(ImageInterface $image)
* @method SizeInterface getCropSize(ImageInterface $image)
*/
class CoverModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
$crop = $this->getCropSize($image);
$resize = $this->getResizeSize($crop);
foreach ($image as $frame) {
$this->modifyFrame($frame, $crop, $resize);
}
return $image;
}
protected function modifyFrame(FrameInterface $frame, SizeInterface $crop, SizeInterface $resize): void
{
// create new image
$modified = $this->driver()->createImage(
$resize->width(),
$resize->height()
)->core()->native();
// get original image
$original = $frame->native();
// preserve transparency
$transIndex = imagecolortransparent($original);
if ($transIndex != -1) {
$rgba = imagecolorsforindex($modified, $transIndex);
$transColor = imagecolorallocatealpha($modified, $rgba['red'], $rgba['green'], $rgba['blue'], 127);
imagefill($modified, 0, 0, $transColor);
imagecolortransparent($modified, $transColor);
}
// copy content from resource
imagecopyresampled(
$modified,
$original,
0,
0,
$crop->pivot()->x(),
$crop->pivot()->y(),
$resize->width(),
$resize->height(),
$crop->width(),
$crop->height()
);
// set new content as resource
$frame->setNative($modified);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
/**
* @method SizeInterface crop(ImageInterface $image)
* @property int $offset_x
* @property int $offset_y
*/
class CropModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
$crop = $this->crop($image);
foreach ($image as $frame) {
$this->cropFrame($frame, $crop);
}
return $image;
}
protected function cropFrame(FrameInterface $frame, SizeInterface $resizeTo): void
{
// create new image
$modified = $this->driver()
->createImage($resizeTo->width(), $resizeTo->height())
->core()
->native();
// get original image
$original = $frame->native();
// preserve transparency
$transIndex = imagecolortransparent($original);
if ($transIndex != -1) {
$rgba = imagecolorsforindex($modified, $transIndex);
$transColor = imagecolorallocatealpha($modified, $rgba['red'], $rgba['green'], $rgba['blue'], 127);
imagefill($modified, 0, 0, $transColor);
imagecolortransparent($modified, $transColor);
}
// copy content from resource
imagecopyresampled(
$modified,
$original,
0,
0,
$resizeTo->pivot()->x() + $this->offset_x,
$resizeTo->pivot()->y() + $this->offset_y,
$resizeTo->width(),
$resizeTo->height(),
$resizeTo->width(),
$resizeTo->height(),
);
// set new content as recource
$frame->setNative($modified);
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\AbstractDrawModifier;
use Intervention\Image\Geometry\Ellipse;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property Ellipse $drawable
*/
class DrawEllipseModifier extends AbstractDrawModifier
{
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
if ($this->drawable->hasBorder()) {
// slightly smaller ellipse to keep 1px bordered edges clean
if ($this->drawable->hasBackgroundColor()) {
imagefilledellipse(
$frame->native(),
$this->position()->x(),
$this->position()->y(),
$this->drawable->width() - 1,
$this->drawable->height() - 1,
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
)
);
}
// gd's imageellipse ignores imagesetthickness
// so i use imagearc with 360 degrees instead.
imagesetthickness(
$frame->native(),
$this->drawable->borderSize(),
);
imagearc(
$frame->native(),
$this->position()->x(),
$this->position()->y(),
$this->drawable->width(),
$this->drawable->height(),
0,
360,
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->borderColor()
)
);
} else {
imagefilledellipse(
$frame->native(),
$this->position()->x(),
$this->position()->y(),
$this->drawable->width(),
$this->drawable->height(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
)
);
}
}
return $image;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\AbstractDrawModifier;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Geometry\Line;
/**
* @method ColorInterface backgroundColor()
* @property Line $drawable
*/
class DrawLineModifier extends AbstractDrawModifier
{
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
imageline(
$frame->native(),
$this->drawable->start()->x(),
$this->drawable->start()->y(),
$this->drawable->end()->x(),
$this->drawable->end()->y(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
)
);
}
return $image;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecializedModifier;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\PointInterface;
/**
* @property PointInterface $position
* @property mixed $color
*/
class DrawPixelModifier extends DriverSpecializedModifier
{
public function apply(ImageInterface $image): ImageInterface
{
$color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->driver()->handleInput($this->color)
);
foreach ($image as $frame) {
imagesetpixel(
$frame->native(),
$this->position->x(),
$this->position->y(),
$color
);
}
return $image;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\AbstractDrawModifier;
use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ColorInterface;
/**
* @method Point position()
* @method ColorInterface backgroundColor()
* @method ColorInterface borderColor()
* @property Polygon $drawable
*/
class DrawPolygonModifier extends AbstractDrawModifier
{
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
if ($this->drawable->hasBackgroundColor()) {
imagefilledpolygon(
$frame->native(),
$this->drawable->toArray(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
)
);
}
if ($this->drawable->hasBorder()) {
imagesetthickness($frame->native(), $this->drawable->borderSize());
imagepolygon(
$frame->native(),
$this->drawable->toArray(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->borderColor()
)
);
}
}
return $image;
}
}

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