charger une image

This commit is contained in:
2026-01-09 10:51:56 +01:00
parent d0f4ed4eb2
commit e5714b47c6
518 changed files with 40175 additions and 44 deletions

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
# Intervention Image
## PHP Image Processing
[![Latest Version](https://img.shields.io/packagist/v/intervention/image.svg)](https://packagist.org/packages/intervention/image)
[![Build Status](https://github.com/Intervention/image/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Intervention/image/actions)
[![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image.svg)](https://packagist.org/packages/intervention/image/stats)
[![Support me on Ko-fi](https://raw.githubusercontent.com/Intervention/image/develop/.github/images/support.svg)](https://ko-fi.com/interventionphp)
Intervention Image is a **PHP image processing library** that provides a simple
and expressive way to create, edit, and compose images. It comes with a universal
interface for the two most popular PHP image manipulation extensions. You can
choose between the GD library or Imagick as the base layer for all operations.
- Simple interface for common image editing tasks
- Interchangeable driver architecture
- Support for animated images
- Framework-agnostic
- PSR-12 compliant
## Installation
You can easily install this library using [Composer](https://getcomposer.org).
Simply request the package with the following command:
```bash
composer require intervention/image
```
## Getting Started
Learn the [basics](https://image.intervention.io/v3/basics/instantiation/) on
how to use Intervention Image and more with the [official
documentation](https://image.intervention.io/v3/).
## Code Examples
```php
use Intervention\Image\ImageManager;
// 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
Before you begin with the installation make sure that your server environment
supports the following requirements.
- PHP >= 8.1
- Mbstring PHP Extension
- Image Processing PHP Extension
## Supported Image Libraries
Depending on your environment Intervention Image lets you choose between
different image processing extensions.
- GD Library
- Imagick PHP extension
- [libvips](https://github.com/Intervention/image-driver-vips)
## Security
If you discover any security related issues, please email oliver@intervention.io directly.
## Authors
This library is developed and maintained by [Oliver Vogel](https://intervention.io)
Thanks to the community of [contributors](https://github.com/Intervention/image/graphs/contributors) who have helped to improve this project.
## License
Intervention Image is licensed under the [MIT License](LICENSE).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use ReflectionClass;
use Stringable;
abstract class AbstractColor implements ColorInterface, Stringable
{
/**
* Color channels
*
* @var array<ColorChannelInterface>
*/
protected array $channels;
/**
* {@inheritdoc}
*
* @see ColorInterface::channels()
*/
public function channels(): array
{
return $this->channels;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::channel()
*/
public function channel(string $classname): ColorChannelInterface
{
$channels = array_filter(
$this->channels(),
fn(ColorChannelInterface $channel): bool => $channel::class === $classname,
);
if (count($channels) == 0) {
throw new ColorException('Color channel ' . $classname . ' could not be found.');
}
return reset($channels);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::normalize()
*/
public function normalize(): array
{
return array_map(
fn(ColorChannelInterface $channel): float => $channel->normalize(),
$this->channels(),
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toArray()
*/
public function toArray(): array
{
return array_map(
fn(ColorChannelInterface $channel): int => $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);
}
/**
* Show debug info for the current color
*
* @return array<string, int>
*/
public function __debugInfo(): array
{
return array_reduce($this->channels(), function (array $result, ColorChannelInterface $item) {
$key = strtolower((new ReflectionClass($item))->getShortName());
$result[$key] = $item->value();
return $result;
}, []);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Stringable;
abstract class AbstractColorChannel implements ColorChannelInterface, Stringable
{
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()
*/
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(int $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,20 @@
<?php
declare(strict_types=1);
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,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
class Key extends Cyan
{
//
}

View File

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

View File

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

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
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\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new instance
*
* @return void
*/
public function __construct(int $c, int $m, int $y, int $k)
{
/** @throws void */
$this->channels = [
new Cyan($c),
new Magenta($m),
new Yellow($y),
new Key($k),
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return InputHandler::withDecoders([
Decoders\StringColorDecoder::class,
])->handle($input);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
/**
* Return the CMYK cyan channel
*/
public function cyan(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Cyan::class);
}
/**
* Return the CMYK magenta channel
*/
public function magenta(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Magenta::class);
}
/**
* Return the CMYK yellow channel
*/
public function yellow(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Yellow::class);
}
/**
* Return the CMYK key channel
*/
public function key(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Key::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return sprintf(
'cmyk(%d%%, %d%%, %d%%, %d%%)',
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
$this->key()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
return 0 === array_sum([
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
]);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return false;
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
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\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace
*
* @var array<string>
*/
public static array $channels = [
Channels\Cyan::class,
Channels\Magenta::class,
Channels\Yellow::class,
Channels\Key::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::createColor()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
return new Color(...array_map(
fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(),
self::$channels,
$normalized,
));
}
/**
* @throws ColorException
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match ($color::class) {
RgbColor::class => $this->importRgbColor($color),
HsvColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
HslColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
default => $color,
};
}
/**
* @throws ColorException
*/
protected function importRgbColor(ColorInterface $color): CmykColor
{
if (!($color instanceof RgbColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
$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,36 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Decoders;
use Intervention\Image\Colors\Cmyk\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\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
*/
public function decode(mixed $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 (string $value): int {
return intval(round(floatval(trim(str_replace('%', '', $value)))));
}, [$matches['c'], $matches['m'], $matches['y'], $matches['k']]);
return new Color(...$values);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
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,30 @@
<?php
declare(strict_types=1);
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,30 @@
<?php
declare(strict_types=1);
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;
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
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\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object
*
* @return void
*/
public function __construct(int $h, int $s, int $l)
{
/** @throws void */
$this->channels = [
new Hue($h),
new Saturation($s),
new Luminance($l),
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return InputHandler::withDecoders([
Decoders\StringColorDecoder::class,
])->handle($input);
}
/**
* Return the Hue channel
*/
public function hue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel
*/
public function saturation(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Saturation::class);
}
/**
* Return the Luminance channel
*/
public function luminance(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Luminance::class);
}
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;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return false;
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
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\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace
*
* @var array<string>
*/
public static array $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Luminance::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
return new Color(...array_map(
fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(),
self::$channels,
$normalized
));
}
/**
* @throws ColorException
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match ($color::class) {
CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
RgbColor::class => $this->importRgbColor($color),
HsvColor::class => $this->importHsvColor($color),
default => $color,
};
}
/**
* @throws ColorException
*/
protected function importRgbColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof RgbColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of rgb channels
$values = array_map(
fn(ColorChannelInterface $channel): float => $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
[$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)),
);
}
/**
* @throws ColorException
*/
protected function importHsvColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof HsvColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of hsv channels
[$h, $s, $v] = array_map(
fn(ColorChannelInterface $channel): float => $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,39 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Decoders;
use Intervention\Image\Colors\Hsl\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\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
*/
public function decode(mixed $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 (string $value): int {
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,30 @@
<?php
declare(strict_types=1);
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,30 @@
<?php
declare(strict_types=1);
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,30 @@
<?php
declare(strict_types=1);
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;
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
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\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object
*
* @return void
*/
public function __construct(int $h, int $s, int $v)
{
/** @throws void */
$this->channels = [
new Hue($h),
new Saturation($s),
new Value($v),
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return InputHandler::withDecoders([
Decoders\StringColorDecoder::class,
])->handle($input);
}
/**
* Return the Hue channel
*/
public function hue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel
*/
public function saturation(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Saturation::class);
}
/**
* Return the Value channel
*/
public function value(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Value::class);
}
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return sprintf(
'hsv(%d, %d%%, %d%%)',
$this->hue()->value(),
$this->saturation()->value(),
$this->value()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
return $this->saturation()->value() == 0;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return false;
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
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\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace
*
* @var array<string>
*/
public static array $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Value::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
return new Color(...array_map(
fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(),
self::$channels,
$normalized
));
}
/**
* @throws ColorException
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match ($color::class) {
CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
RgbColor::class => $this->importRgbColor($color),
HslColor::class => $this->importHslColor($color),
default => $color,
};
}
/**
* @throws ColorException
*/
protected function importRgbColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof RgbColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of rgb channels
$values = array_map(fn(ColorChannelInterface $channel): float => $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
[$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))
);
}
/**
* @throws ColorException
*/
protected function importHslColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof HslColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of hsl channels
[$h, $s, $l] = array_map(
fn(ColorChannelInterface $channel): float => $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,39 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Decoders;
use Intervention\Image\Colors\Hsv\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\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
*/
public function decode(mixed $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 (string $value): int {
return match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(trim(str_replace('%', '', $value))),
};
}, [$matches['h'], $matches['s'], $matches['v']]);
return new Color(...$values);
}
}

View File

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

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Channels;
class Alpha extends Red
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::toString()
*/
public function toString(): string
{
return strval(round($this->normalize(), 6));
}
}

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Channels;
use Intervention\Image\Colors\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;
}
}

View File

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

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
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\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace
*
* @var array<string>
*/
public static array $channels = [
Channels\Red::class,
Channels\Green::class,
Channels\Blue::class,
Channels\Alpha::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
return new Color(...array_map(
fn($classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(),
self::$channels,
$normalized,
));
}
/**
* @throws ColorException
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match ($color::class) {
CmykColor::class => $this->importCmykColor($color),
HsvColor::class => $this->importHsvColor($color),
HslColor::class => $this->importHslColor($color),
default => $color,
};
}
/**
* @throws ColorException
*/
protected function importCmykColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof CmykColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
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())),
);
}
/**
* @throws ColorException
*/
protected function importHsvColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof HsvColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
$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(fn(float|int $value): float => $value + $color->value()->normalize() - $chroma, $values);
$values[] = 1; // append alpha channel value
return $this->colorFromNormalized($values);
}
/**
* @throws ColorException
*/
protected function importHslColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof HslColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of hsl channels
[$h, $s, $l] = array_map(
fn(ColorChannelInterface $channel): float => $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(fn(float|int $value): float => $value + $m, $values);
$values[] = 1; // append alpha channel value
return $this->colorFromNormalized($values);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\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
*/
public function decode(mixed $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 = 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 (string $value): float|int {
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,176 @@
<?php
declare(strict_types=1);
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
{
/**
* Available color names and their corresponding hex codes
*
* @var array<string, string>
*/
protected static array $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
*/
public function decode(mixed $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
declare(strict_types=1);
namespace Intervention\Image\Colors\Rgb\Decoders;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\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
*/
public function decode(mixed $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 (string $value): int {
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,30 @@
<?php
declare(strict_types=1);
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
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $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('#ffffff00');
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ImageInterface;
class ColorObjectDecoder extends AbstractDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_a($input, ColorInterface::class)) {
throw new DecoderException('Unable to decode input');
}
return $input;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ColorInterface;
class ImageObjectDecoder extends AbstractDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_a($input, ImageInterface::class)) {
throw new DecoderException('Unable to decode input');
}
return $input;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Exception;
use Intervention\Image\Collection;
use Intervention\Image\Interfaces\CollectionInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Traits\CanBuildFilePointer;
abstract class AbstractDecoder implements DecoderInterface
{
use CanBuildFilePointer;
/**
* Determine if the given input is GIF data format
*/
protected function isGifFormat(string $input): bool
{
return str_starts_with($input, 'GIF87a') || str_starts_with($input, 'GIF89a');
}
/**
* Determine if given input is a path to an existing regular file
*/
protected function isFile(mixed $input): bool
{
if (!is_string($input)) {
return false;
}
if (strlen($input) > PHP_MAXPATHLEN) {
return false;
}
try {
if (!@is_file($input)) {
return false;
}
} catch (Exception) {
return false;
}
return true;
}
/**
* Extract and return EXIF data from given input which can be binary image
* data or a file path.
*
* @return CollectionInterface<string, mixed>
*/
protected function extractExifData(string $path_or_data): CollectionInterface
{
if (!function_exists('exif_read_data')) {
return new Collection();
}
try {
$source = match (true) {
$this->isFile($path_or_data) => $path_or_data, // path
default => $this->buildFilePointer($path_or_data), // data
};
// extract exif data
$data = @exif_read_data($source, null, true);
if (is_resource($source)) {
fclose($source);
}
} catch (Exception) {
$data = [];
}
return new Collection(is_array($data) ? $data : []);
}
/**
* Determine if given input is base64 encoded data
*/
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
*/
protected function parseDataUri(mixed $input): object
{
$pattern = "/^data:(?P<mediatype>\w+\/[-+.\w]+)?" .
"(?P<parameters>(;[-\w]+=[-\w]+)*)(?P<base64>;base64)?,(?P<data>.*)/";
$result = preg_match($pattern, (string) $input, $matches);
return new class ($matches, $result)
{
/**
* @param array<mixed> $matches
* @return void
*/
public function __construct(private array $matches, private int|false $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 isBase64Encoded(): bool
{
return isset($this->matches['base64']) && $this->matches['base64'] === ';base64';
}
public function data(): ?string
{
if (isset($this->matches['data']) && !empty($this->matches['data'])) {
return $this->matches['data'];
}
return null;
}
};
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Intervention\Image\Config;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\AnalyzerInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
use Intervention\Image\Interfaces\SpecializableInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use ReflectionClass;
abstract class AbstractDriver implements DriverInterface
{
/**
* Driver options
*/
protected Config $config;
/**
* @throws DriverException
* @return void
*/
public function __construct()
{
$this->config = new Config();
$this->checkHealth();
}
/**
* {@inheritdoc}
*
* @see DriverInterface::config()
*/
public function config(): Config
{
return $this->config;
}
/**
* {@inheritdoc}
*
* @see DriverInterface::handleInput()
*/
public function handleInput(mixed $input, array $decoders = []): ImageInterface|ColorInterface
{
return InputHandler::withDecoders($decoders, $this)->handle($input);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::specialize()
*/
public function specialize(
ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface $object
): ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface {
// return object directly if no specializing is possible
if (!($object instanceof SpecializableInterface)) {
return $object;
}
// return directly and only attach driver if object is already specialized
if ($object instanceof SpecializedInterface) {
$object->setDriver($this);
return $object;
}
// resolve classname for specializable object
$specialized_classname = implode("\\", [
(new ReflectionClass($this))->getNamespaceName(), // driver's namespace
match (true) {
$object instanceof ModifierInterface => 'Modifiers',
$object instanceof AnalyzerInterface => 'Analyzers',
$object instanceof EncoderInterface => 'Encoders',
$object instanceof DecoderInterface => 'Decoders',
},
$object_shortname = (new ReflectionClass($object))->getShortName(),
]);
// fail if driver specialized classname does not exists
if (!class_exists($specialized_classname)) {
throw new NotSupportedException(
"Class '" . $object_shortname . "' is not supported by " . $this->id() . " driver."
);
}
// create a driver specialized object with the specializable properties of generic object
$specialized = new $specialized_classname(...$object->specializable());
// attach driver
return $specialized->setDriver($this);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::specializeMultiple()
*
* @throws NotSupportedException
* @throws DriverException
*/
public function specializeMultiple(array $objects): array
{
return array_map(
function (string|object $object): ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface {
return $this->specialize(
match (true) {
is_string($object) => new $object(),
is_object($object) => $object,
}
);
},
$objects
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers;
use Intervention\Image\EncodedImage;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\EncodedImageInterface;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Traits\CanBuildFilePointer;
abstract class AbstractEncoder implements EncoderInterface
{
use CanBuildFilePointer;
public const DEFAULT_QUALITY = 75;
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImageInterface
{
return $image->encode($this);
}
/**
* Build new file pointer, run callback with it and return result as encoded image
*
* @throws RuntimeException
*/
protected function createEncodedImage(callable $callback, ?string $mediaType = null): EncodedImage
{
$pointer = $this->buildFilePointer();
$callback($pointer);
return is_string($mediaType) ? new EncodedImage($pointer, $mediaType) : new EncodedImage($pointer);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use GdImage;
use Intervention\Image\Analyzers\PixelColorAnalyzer as GenericPixelColorAnalyzer;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\GeometryException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class PixelColorAnalyzer extends GenericPixelColorAnalyzer implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*/
public function analyze(ImageInterface $image): mixed
{
return $this->colorAt(
$image->colorspace(),
$image->core()->frame($this->frame_key)->native()
);
}
/**
* @throws GeometryException
* @throws ColorException
*/
protected function colorAt(ColorspaceInterface $colorspace, GdImage $gd): ColorInterface
{
$index = @imagecolorat($gd, $this->x, $this->y);
if (!imageistruecolor($gd)) {
$index = imagecolorsforindex($gd, $index);
}
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,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Analyzers;
use Intervention\Image\Collection;
use Intervention\Image\Interfaces\ImageInterface;
class PixelColorsAnalyzer extends PixelColorAnalyzer
{
/**
* {@inheritdoc}
*
* @see AnalyzerInterface::analyze()
*/
public function analyze(ImageInterface $image): mixed
{
$colors = new Collection();
$colorspace = $image->colorspace();
foreach ($image as $frame) {
$colors->push(
parent::colorAt($colorspace, $frame->native())
);
}
return $colors;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Colors\Rgb\Channels\Alpha;
use Intervention\Image\Colors\Rgb\Channels\Blue;
use Intervention\Image\Colors\Rgb\Channels\Green;
use Intervention\Image\Colors\Rgb\Channels\Red;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Colors\Rgb\Colorspace;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class ColorProcessor implements ColorProcessorInterface
{
/**
* Create new color processor object
*
* @return void
*/
public function __construct(protected ColorspaceInterface $colorspace = new Colorspace())
{
//
}
/**
* {@inheritdoc}
*
* @see ColorProcessorInterface::colorToNative()
*/
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;
}
/**
* {@inheritdoc}
*
* @see ColorProcessorInterface::nativeToColor()
*/
public function nativeToColor(mixed $value): ColorInterface
{
if (!is_int($value) && !is_array($value)) {
throw new ColorException('GD driver can only decode colors in integer and array format.');
}
if (is_array($value)) {
// array conversion
if (!$this->isValidArrayColor($value)) {
throw new ColorException(
'GD driver can only decode array color format array{red: int, green: int, blue: int, alpha: int}.',
);
}
$r = $value['red'];
$g = $value['green'];
$b = $value['blue'];
$a = $value['alpha'];
} else {
// integer conversion
$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).
*/
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);
}
/**
* Check if given array is valid color format
* array{red: int, green: int, blue: int, alpha: int}
* i.e. result of imagecolorsforindex()
*
* @param array<mixed> $color
*/
private function isValidArrayColor(array $color): bool
{
if (!array_key_exists('red', $color)) {
return false;
}
if (!array_key_exists('green', $color)) {
return false;
}
if (!array_key_exists('blue', $color)) {
return false;
}
if (!array_key_exists('alpha', $color)) {
return false;
}
if (!is_int($color['red'])) {
return false;
}
if (!is_int($color['green'])) {
return false;
}
if (!is_int($color['blue'])) {
return false;
}
if (!is_int($color['alpha'])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Collection;
use Intervention\Image\Exceptions\AnimationException;
use Intervention\Image\Interfaces\CoreInterface;
use Intervention\Image\Interfaces\FrameInterface;
class Core extends Collection implements CoreInterface
{
protected int $loops = 0;
/**
* {@inheritdoc}
*
* @see CoreInterface::add()
*/
public function add(FrameInterface $frame): CoreInterface
{
$this->push($frame);
return $this;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::native()
*/
public function native(): mixed
{
return $this->first()->native();
}
/**
* {@inheritdoc}
*
* @see CoreInterface::setNative()
*/
public function setNative(mixed $native): self
{
$this->empty()->push(new Frame($native));
return $this;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::frame()
*/
public function frame(int $position): FrameInterface
{
$frame = $this->getAtPosition($position);
if (!($frame instanceof FrameInterface)) {
throw new AnimationException('Frame #' . $position . ' could not be found in the image.');
}
return $frame;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::loops()
*/
public function loops(): int
{
return $this->loops;
}
/**
* {@inheritdoc}
*
* @see CoreInterface::setLoops()
*/
public function setLoops(int $loops): self
{
$this->loops = $loops;
return $this;
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::first()
*/
public function first(): FrameInterface
{
return parent::first();
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::last()
*/
public function last(): FrameInterface
{
return parent::last();
}
/**
* Clone instance
*/
public function __clone(): void
{
foreach ($this->items as $key => $frame) {
$this->items[$key] = clone $frame;
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\SpecializableDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\MediaType;
use ValueError;
abstract class AbstractDecoder extends SpecializableDecoder implements SpecializedInterface
{
/**
* Return media (mime) type of the file at given file path
*
* @throws DecoderException
* @throws NotSupportedException
*/
protected function getMediaTypeByFilePath(string $filepath): MediaType
{
$info = @getimagesize($filepath);
if (!is_array($info)) {
throw new DecoderException('Unable to detect media (MIME) from data in file path.');
}
try {
return MediaType::from($info['mime']);
} catch (ValueError) {
throw new NotSupportedException('Unsupported media type (MIME) ' . $info['mime'] . '.');
}
}
/**
* Return media (mime) type of the given image data
*
* @throws DecoderException
* @throws NotSupportedException
*/
protected function getMediaTypeByBinary(string $data): MediaType
{
$info = @getimagesizefromstring($data);
if (!is_array($info)) {
throw new DecoderException('Unable to detect media (MIME) from binary data.');
}
try {
return MediaType::from($info['mime']);
} catch (ValueError) {
throw new NotSupportedException('Unsupported media type (MIME) ' . $info['mime'] . '.');
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
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
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!$this->isValidBase64($input)) {
throw new DecoderException('Unable to decode input');
}
return parent::decode(base64_decode((string) $input));
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Format;
use Intervention\Image\Modifiers\AlignRotationModifier;
class BinaryImageDecoder extends NativeObjectDecoder implements DecoderInterface
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
return match ($this->isGifFormat($input)) {
true => $this->decodeGif($input),
default => $this->decodeBinary($input),
};
}
/**
* Decode image from given binary data
*
* @throws RuntimeException
*/
private function decodeBinary(string $input): ImageInterface
{
$gd = @imagecreatefromstring($input);
if ($gd === false) {
throw new DecoderException('Unable to decode input');
}
// create image instance
$image = parent::decode($gd);
// get media type
$mediaType = $this->getMediaTypeByBinary($input);
// extract & set exif data for appropriate formats
if (in_array($mediaType->format(), [Format::JPEG, Format::TIFF])) {
$image->setExif($this->extractExifData($input));
}
// set mediaType on origin
$image->origin()->setMediaType($mediaType);
// adjust image orientation
if ($this->driver()->config()->autoOrientation) {
$image->modify(new AlignRotationModifier());
}
return $image;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
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
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $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,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\EncodedImage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ColorInterface;
class EncodedImageObjectDecoder extends BinaryImageDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_a($input, EncodedImage::class)) {
throw new DecoderException('Unable to decode input');
}
return parent::decode($input->toString());
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Format;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Modifiers\AlignRotationModifier;
class FilePathImageDecoder extends NativeObjectDecoder implements DecoderInterface
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!$this->isFile($input)) {
throw new DecoderException('Unable to decode input');
}
// detect media (mime) type
$mediaType = $this->getMediaTypeByFilePath($input);
$image = match ($mediaType->format()) {
// gif files might be animated and therefore cannot
// be handled by the standard GD decoder.
Format::GIF => $this->decodeGif($input),
default => parent::decode(match ($mediaType->format()) {
Format::JPEG => @imagecreatefromjpeg($input),
Format::WEBP => @imagecreatefromwebp($input),
Format::PNG => @imagecreatefrompng($input),
Format::AVIF => @imagecreatefromavif($input),
Format::BMP => @imagecreatefrombmp($input),
default => throw new DecoderException('Unable to decode input'),
}),
};
// set file path & mediaType on origin
$image->origin()->setFilePath($input);
$image->origin()->setMediaType($mediaType);
// extract exif for the appropriate formats
if ($mediaType->format() === Format::JPEG) {
$image->setExif($this->extractExifData($input));
}
// adjust image orientation
if ($this->driver()->config()->autoOrientation) {
$image->modify(new AlignRotationModifier());
}
return $image;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
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
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $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,109 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Exception;
use GdImage;
use Intervention\Gif\Decoder as GifDecoder;
use Intervention\Gif\Splitter as GifSplitter;
use Intervention\Image\Drivers\Gd\Core;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ImageInterface;
class NativeObjectDecoder extends AbstractDecoder
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_object($input)) {
throw new DecoderException('Unable to decode input');
}
if (!($input instanceof GdImage)) {
throw new DecoderException('Unable to decode input');
}
if (!imageistruecolor($input)) {
imagepalettetotruecolor($input);
}
imagesavealpha($input, true);
// build image instance
return new Image(
$this->driver(),
new Core([
new Frame($input)
])
);
}
/**
* Decode image from given GIF source which can be either a file path or binary data
*
* Depending on the configuration, this is taken over by the native GD function
* or, if animations are required, by our own extended decoder.
*
* @throws RuntimeException
*/
protected function decodeGif(mixed $input): ImageInterface
{
// create non-animated image depending on config
if (!$this->driver()->config()->decodeAnimation) {
$native = match (true) {
$this->isGifFormat($input) => @imagecreatefromstring($input),
default => @imagecreatefromgif($input),
};
if ($native === false) {
throw new DecoderException('Unable to decode input.');
}
$image = self::decode($native);
$image->origin()->setMediaType('image/gif');
return $image;
}
try {
// create empty core
$core = new Core();
$gif = GifDecoder::decode($input);
$splitter = GifSplitter::create($gif)->split();
$delays = $splitter->getDelays();
// set loops on core
if ($loops = $gif->getMainApplicationExtension()?->getLoops()) {
$core->setLoops($loops);
}
// add GDImage instances to core
foreach ($splitter->coalesceToResources() as $key => $native) {
$core->push(
new Frame($native, $delays[$key] / 100)
);
}
} catch (Exception $e) {
throw new DecoderException($e->getMessage(), $e->getCode(), $e);
}
// create (possibly) animated image
$image = new Image($this->driver(), $core);
// set media type
$image->origin()->setMediaType('image/gif');
return $image;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
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
{
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_a($input, SplFileInfo::class)) {
throw new DecoderException('Unable to decode input');
}
return parent::decode($input->getRealPath());
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Drivers\AbstractDriver;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Format;
use Intervention\Image\FileExtension;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\FontProcessorInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\MediaType;
class Driver extends AbstractDriver
{
/**
* {@inheritdoc}
*
* @see DriverInterface::id()
*/
public function id(): string
{
return 'GD';
}
/**
* {@inheritdoc}
*
* @see DriverInterface::checkHealth()
*
* @codeCoverageIgnore
*/
public function checkHealth(): void
{
if (!extension_loaded('gd') || !function_exists('gd_info')) {
throw new DriverException(
'GD PHP extension must be installed to use this driver.'
);
}
}
/**
* {@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, 255, 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()
*
* @throws RuntimeException
*/
public function createAnimation(callable $init): ImageInterface
{
$animation = new class ($this)
{
public function __construct(
protected DriverInterface $driver,
public Core $core = new Core()
) {
//
}
/**
* @throws RuntimeException
*/
public function add(mixed $source, float $delay = 1): self
{
$this->core->add(
$this->driver->handleInput($source)->core()->first()->setDelay($delay)
);
return $this;
}
/**
* @throws RuntimeException
*/
public function __invoke(): ImageInterface
{
return new Image(
$this->driver,
$this->core
);
}
};
$init($animation);
return call_user_func($animation);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::colorProcessor()
*/
public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface
{
return new ColorProcessor($colorspace);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::fontProcessor()
*/
public function fontProcessor(): FontProcessorInterface
{
return new FontProcessor();
}
/**
* {@inheritdoc}
*
* @see DriverInterface::supports()
*/
public function supports(string|Format|FileExtension|MediaType $identifier): bool
{
return match (Format::tryCreate($identifier)) {
Format::JPEG => boolval(imagetypes() & IMG_JPEG),
Format::WEBP => boolval(imagetypes() & IMG_WEBP),
Format::GIF => boolval(imagetypes() & IMG_GIF),
Format::PNG => boolval(imagetypes() & IMG_PNG),
Format::AVIF => boolval(imagetypes() & IMG_AVIF),
Format::BMP => boolval(imagetypes() & IMG_BMP),
default => false,
};
}
/**
* Return version of GD library
*/
public static function version(): string
{
return gd_info()['GD Version'];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\AvifEncoder as GenericAvifEncoder;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class AvifEncoder extends GenericAvifEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
return $this->createEncodedImage(function ($pointer) use ($image): void {
imageavif($image->core()->native(), $pointer, $this->quality);
}, 'image/avif');
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\BmpEncoder as GenericBmpEncoder;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class BmpEncoder extends GenericBmpEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
return $this->createEncodedImage(function ($pointer) use ($image): void {
imagebmp($image->core()->native(), $pointer, false);
}, 'image/bmp');
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Encoders;
use Exception;
use Intervention\Gif\Builder as GifBuilder;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\GifEncoder as GenericGifEncoder;
use Intervention\Image\Exceptions\EncoderException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class GifEncoder extends GenericGifEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
if ($image->isAnimated()) {
return $this->encodeAnimated($image);
}
$gd = Cloner::clone($image->core()->native());
return $this->createEncodedImage(function ($pointer) use ($gd): void {
imageinterlace($gd, $this->interlaced);
imagegif($gd, $pointer);
}, 'image/gif');
}
/**
* @throws RuntimeException
*/
protected function encodeAnimated(ImageInterface $image): EncodedImage
{
try {
$builder = GifBuilder::canvas(
$image->width(),
$image->height()
);
foreach ($image as $frame) {
$builder->addFrame(
source: $this->encode($frame->toImage($image->driver()))->toFilePointer(),
delay: $frame->delay(),
interlaced: $this->interlaced
);
}
$builder->setLoops($image->loops());
return new EncodedImage($builder->encode(), 'image/gif');
} catch (Exception $e) {
throw new EncoderException($e->getMessage(), $e->getCode(), $e);
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Encoders\JpegEncoder as GenericJpegEncoder;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class JpegEncoder extends GenericJpegEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
$blendingColor = $this->driver()->handleInput(
$this->driver()->config()->blendingColor
);
$output = Cloner::cloneBlended(
$image->core()->native(),
background: $blendingColor
);
return $this->createEncodedImage(function ($pointer) use ($output): void {
imageinterlace($output, $this->progressive);
imagejpeg($output, $pointer, $this->quality);
}, 'image/jpeg');
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Encoders;
use GdImage;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\PngEncoder as GenericPngEncoder;
use Intervention\Image\Exceptions\AnimationException;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class PngEncoder extends GenericPngEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
$output = $this->prepareOutput($image);
return $this->createEncodedImage(function ($pointer) use ($output): void {
imageinterlace($output, $this->interlaced);
imagepng($output, $pointer, -1);
}, 'image/png');
}
/**
* Prepare given image instance for PNG format output according to encoder settings
*
* @throws RuntimeException
* @throws ColorException
* @throws AnimationException
*/
private function prepareOutput(ImageInterface $image): GdImage
{
if ($this->indexed) {
$output = clone $image;
$output->reduceColors(255);
return $output->core()->native();
}
return Cloner::clone($image->core()->native());
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\WebpEncoder as GenericWebpEncoder;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class WebpEncoder extends GenericWebpEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
$quality = $this->quality === 100 && defined('IMG_WEBP_LOSSLESS') ? IMG_WEBP_LOSSLESS : $this->quality;
return $this->createEncodedImage(function ($pointer) use ($image, $quality): void {
imagewebp($image->core()->native(), $pointer, $quality);
}, 'image/webp');
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Drivers\AbstractFontProcessor;
use Intervention\Image\Exceptions\FontException;
use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\SizeInterface;
class FontProcessor extends AbstractFontProcessor
{
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::boxSize()
*/
public function boxSize(string $text, FontInterface $font): SizeInterface
{
// if the font has no ttf file the box size is calculated
// with gd's internal font system: integer values from 1-5
if (!$font->hasFilename()) {
// calculate box size from gd font
$box = new Rectangle(0, 0);
$chars = mb_strlen($text);
if ($chars > 0) {
$box->setWidth(
$chars * $this->gdCharacterWidth((int) $font->filename())
);
$box->setHeight(
$this->gdCharacterHeight((int) $font->filename())
);
}
return $box;
}
// build full path to font file to make sure to pass absolute path to imageftbbox()
// because of issues with different GD version behaving differently when passing
// relative paths to imageftbbox()
$fontPath = realpath($font->filename());
if ($fontPath === false) {
throw new FontException('Font file ' . $font->filename() . ' does not exist.');
}
// calculate box size from ttf font file with angle 0
$box = imageftbbox(
size: $this->nativeFontSize($font),
angle: 0,
font_filename: $fontPath,
string: $text,
);
if ($box === false) {
throw new FontException('Unable to calculate box size of font ' . $font->filename() . '.');
}
// build size from points
return new Rectangle(
width: intval(abs($box[6] - $box[4])), // difference of upper-left-x and upper-right-x
height: intval(abs($box[7] - $box[1])), // difference if upper-left-y and lower-left-y
pivot: new Point($box[6], $box[7]), // position of upper-left corner
);
}
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::nativeFontSize()
*/
public function nativeFontSize(FontInterface $font): float
{
return floatval(round($font->size() * .76, 6));
}
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::leading()
*/
public function leading(FontInterface $font): int
{
return (int) round(parent::leading($font) * .8);
}
/**
* Return width of a single character
*/
protected function gdCharacterWidth(int $gdfont): int
{
return $gdfont + 4;
}
/**
* Return height of a single character
*/
protected function gdCharacterHeight(int $gdfont): int
{
return match ($gdfont) {
2, 3 => 14,
4, 5 => 16,
default => 8,
};
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use GdImage;
use Intervention\Image\Drivers\AbstractFrame;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InputException;
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 extends AbstractFrame implements FrameInterface
{
/**
* Create new frame instance
*
* @return void
*/
public function __construct(
protected GdImage $native,
protected float $delay = 0,
protected int $dispose = 1,
protected int $offset_left = 0,
protected int $offset_top = 0
) {
//
}
/**
* {@inheritdoc}
*
* @see FrameInterface::toImage()
*/
public function toImage(DriverInterface $driver): ImageInterface
{
return new Image($driver, new Core([$this]));
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setNative()
*/
public function setNative(mixed $native): FrameInterface
{
$this->native = $native;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::native()
*/
public function native(): GdImage
{
return $this->native;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::size()
*/
public function size(): SizeInterface
{
return new Rectangle(imagesx($this->native), imagesy($this->native));
}
/**
* {@inheritdoc}
*
* @see FrameInterface::delay()
*/
public function delay(): float
{
return $this->delay;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setDelay()
*/
public function setDelay(float $delay): FrameInterface
{
$this->delay = $delay;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::dispose()
*/
public function dispose(): int
{
return $this->dispose;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setDispose()
*
* @throws InputException
*/
public function setDispose(int $dispose): FrameInterface
{
if (!in_array($dispose, [0, 1, 2, 3])) {
throw new InputException('Value for argument $dispose must be 0, 1, 2 or 3.');
}
$this->dispose = $dispose;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setOffset()
*/
public function setOffset(int $left, int $top): FrameInterface
{
$this->offset_left = $left;
$this->offset_top = $top;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::offsetLeft()
*/
public function offsetLeft(): int
{
return $this->offset_left;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setOffsetLeft()
*/
public function setOffsetLeft(int $offset): FrameInterface
{
$this->offset_left = $offset;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::offsetTop()
*/
public function offsetTop(): int
{
return $this->offset_top;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setOffsetTop()
*/
public function setOffsetTop(int $offset): FrameInterface
{
$this->offset_top = $offset;
return $this;
}
/**
* This workaround helps cloning GdImages which is currently not possible.
*
* @throws ColorException
*/
public function __clone(): void
{
$this->native = Cloner::clone($this->native);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\AlignRotationModifier as GenericAlignRotationModifier;
class AlignRotationModifier extends GenericAlignRotationModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
$image = match ($image->exif('IFD0.Orientation')) {
2 => $image->flop(),
3 => $image->rotate(180),
4 => $image->rotate(180)->flop(),
5 => $image->rotate(270)->flop(),
6 => $image->rotate(270),
7 => $image->rotate(90)->flop(),
8 => $image->rotate(90),
default => $image
};
return $this->markAligned($image);
}
/**
* Set exif data of image to top-left orientation, marking the image as
* aligned and making sure the rotation correction process is not
* performed again.
*/
private function markAligned(ImageInterface $image): ImageInterface
{
$exif = $image->exif()->map(function ($item) {
if (is_array($item) && array_key_exists('Orientation', $item)) {
$item['Orientation'] = 1;
return $item;
}
return $item;
});
return $image->setExif($exif);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\BlendTransparencyModifier as GenericBlendTransparencyModifier;
class BlendTransparencyModifier extends GenericBlendTransparencyModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
$blendingColor = $this->blendingColor($this->driver());
foreach ($image as $frame) {
// create new canvas with blending color as background
$modified = Cloner::cloneBlended(
$frame->native(),
background: $blendingColor
);
// set new gd image
$frame->setNative($modified);
}
return $image;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\BlurModifier as GenericBlurModifier;
class BlurModifier extends GenericBlurModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
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,26 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\BrightnessModifier as GenericBrightnessModifier;
class BrightnessModifier extends GenericBrightnessModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
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,31 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ColorizeModifier as GenericColorizeModifier;
class ColorizeModifier extends GenericColorizeModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
// normalize colorize levels
$red = (int) round($this->red * 2.55);
$green = (int) round($this->green * 2.55);
$blue = (int) 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,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ColorspaceModifier as GenericColorspaceModifier;
class ColorspaceModifier extends GenericColorspaceModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
if (!($this->targetColorspace() instanceof RgbColorspace)) {
throw new NotSupportedException(
'Only RGB colorspace is supported by GD driver.'
);
}
return $image;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
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\Gd\Cloner;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ContainModifier as GenericContainModifier;
class ContainModifier extends GenericContainModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
$crop = $this->getCropSize($image);
$resize = $this->getResizeSize($image);
$background = $this->driver()->handleInput($this->background);
$blendingColor = $this->driver()->handleInput(
$this->driver()->config()->blendingColor
);
foreach ($image as $frame) {
$this->modify($frame, $crop, $resize, $background, $blendingColor);
}
return $image;
}
/**
* @throws ColorException
*/
protected function modify(
FrameInterface $frame,
SizeInterface $crop,
SizeInterface $resize,
ColorInterface $background,
ColorInterface $blendingColor
): void {
// create new gd image
$modified = Cloner::cloneEmpty($frame->native(), $resize, $background);
// make image area transparent to keep transparency
// even if background-color is set
$transparent = imagecolorallocatealpha(
$modified,
$blendingColor->channel(Red::class)->value(),
$blendingColor->channel(Green::class)->value(),
$blendingColor->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 resource
$frame->setNative($modified);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ContrastModifier as GenericContrastModifier;
class ContrastModifier extends GenericContrastModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
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,19 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\GeometryException;
use Intervention\Image\Interfaces\SizeInterface;
class CoverDownModifier extends CoverModifier
{
/**
* @throws GeometryException
*/
public function getResizeSize(SizeInterface $size): SizeInterface
{
return $size->resizeDown($this->width, $this->height);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\CoverModifier as GenericCoverModifier;
class CoverModifier extends GenericCoverModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
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;
}
/**
* @throws ColorException
*/
protected function modifyFrame(FrameInterface $frame, SizeInterface $crop, SizeInterface $resize): void
{
// create new image
$modified = Cloner::cloneEmpty($frame->native(), $resize);
// copy content from resource
imagecopyresampled(
$modified,
$frame->native(),
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,78 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\CropModifier as GenericCropModifier;
class CropModifier extends GenericCropModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
$originalSize = $image->size();
$crop = $this->crop($image);
$background = $this->driver()->handleInput($this->background);
foreach ($image as $frame) {
$this->cropFrame($frame, $originalSize, $crop, $background);
}
return $image;
}
/**
* @throws ColorException
*/
protected function cropFrame(
FrameInterface $frame,
SizeInterface $originalSize,
SizeInterface $resizeTo,
ColorInterface $background
): void {
// create new image with transparent background
$modified = Cloner::cloneEmpty($frame->native(), $resizeTo, $background);
// define offset
$offset_x = $resizeTo->pivot()->x() + $this->offset_x;
$offset_y = $resizeTo->pivot()->y() + $this->offset_y;
// define target width & height
$targetWidth = min($resizeTo->width(), $originalSize->width());
$targetHeight = min($resizeTo->height(), $originalSize->height());
$targetWidth = $targetWidth < $originalSize->width() ? $targetWidth + $offset_x : $targetWidth;
$targetHeight = $targetHeight < $originalSize->height() ? $targetHeight + $offset_y : $targetHeight;
// don't alpha blend for copy operation to keep transparent areas of original image
imagealphablending($modified, false);
// copy content from resource
imagecopyresampled(
$modified,
$frame->native(),
$offset_x * -1,
$offset_y * -1,
0,
0,
$targetWidth,
$targetHeight,
$targetWidth,
$targetHeight
);
// set new content as resource
$frame->setNative($modified);
}
}

View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use RuntimeException;
use Intervention\Image\Exceptions\GeometryException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\DrawBezierModifier as GenericDrawBezierModifier;
class DrawBezierModifier extends GenericDrawBezierModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws RuntimeException
* @throws GeometryException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve');
}
[$polygon, $polygon_border_segments] = $this->calculateBezierPoints();
if ($this->drawable->hasBackgroundColor() || $this->drawable->hasBorder()) {
imagealphablending($frame->native(), true);
imageantialias($frame->native(), true);
}
if ($this->drawable->hasBackgroundColor()) {
$background_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
);
imagesetthickness($frame->native(), 0);
imagefilledpolygon(
$frame->native(),
$polygon,
$background_color
);
}
if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) {
$border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->borderColor()
);
if ($this->drawable->borderSize() === 1) {
imagesetthickness($frame->native(), $this->drawable->borderSize());
$count = count($polygon);
for ($i = 0; $i < $count; $i += 2) {
if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) {
imageline(
$frame->native(),
$polygon[$i],
$polygon[$i + 1],
$polygon[$i + 2],
$polygon[$i + 3],
$border_color
);
}
}
} else {
$polygon_border_segments_total = count($polygon_border_segments);
for ($i = 0; $i < $polygon_border_segments_total; $i += 1) {
imagefilledpolygon(
$frame->native(),
$polygon_border_segments[$i],
$border_color
);
}
}
}
}
return $image;
}
/**
* Calculate interpolation points for quadratic beziers using the Bernstein polynomial form
*
* @return array{'x': float, 'y': float}
*/
private function calculateQuadraticBezierInterpolationPoint(float $t = 0.05): array
{
$remainder = 1 - $t;
$control_point_1_multiplier = $remainder * $remainder;
$control_point_2_multiplier = $remainder * $t * 2;
$control_point_3_multiplier = $t * $t;
$x = (
$this->drawable->first()->x() * $control_point_1_multiplier +
$this->drawable->second()->x() * $control_point_2_multiplier +
$this->drawable->last()->x() * $control_point_3_multiplier
);
$y = (
$this->drawable->first()->y() * $control_point_1_multiplier +
$this->drawable->second()->y() * $control_point_2_multiplier +
$this->drawable->last()->y() * $control_point_3_multiplier
);
return ['x' => $x, 'y' => $y];
}
/**
* Calculate interpolation points for cubic beziers using the Bernstein polynomial form
*
* @return array{'x': float, 'y': float}
*/
private function calculateCubicBezierInterpolationPoint(float $t = 0.05): array
{
$remainder = 1 - $t;
$t_squared = $t * $t;
$remainder_squared = $remainder * $remainder;
$control_point_1_multiplier = $remainder_squared * $remainder;
$control_point_2_multiplier = $remainder_squared * $t * 3;
$control_point_3_multiplier = $t_squared * $remainder * 3;
$control_point_4_multiplier = $t_squared * $t;
$x = (
$this->drawable->first()->x() * $control_point_1_multiplier +
$this->drawable->second()->x() * $control_point_2_multiplier +
$this->drawable->third()->x() * $control_point_3_multiplier +
$this->drawable->last()->x() * $control_point_4_multiplier
);
$y = (
$this->drawable->first()->y() * $control_point_1_multiplier +
$this->drawable->second()->y() * $control_point_2_multiplier +
$this->drawable->third()->y() * $control_point_3_multiplier +
$this->drawable->last()->y() * $control_point_4_multiplier
);
return ['x' => $x, 'y' => $y];
}
/**
* Calculate the points needed to draw a quadratic or cubic bezier with optional border/stroke
*
* @throws GeometryException
* @return array{0: array<mixed>, 1: array<mixed>}
*/
private function calculateBezierPoints(): array
{
if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve');
}
$polygon = [];
$inner_polygon = [];
$outer_polygon = [];
$polygon_border_segments = [];
// define ratio t; equivalent to 5 percent distance along edge
$t = 0.05;
$polygon[] = $this->drawable->first()->x();
$polygon[] = $this->drawable->first()->y();
for ($i = $t; $i < 1; $i += $t) {
if ($this->drawable->count() === 3) {
$ip = $this->calculateQuadraticBezierInterpolationPoint($i);
} elseif ($this->drawable->count() === 4) {
$ip = $this->calculateCubicBezierInterpolationPoint($i);
}
$polygon[] = (int) $ip['x'];
$polygon[] = (int) $ip['y'];
}
$polygon[] = $this->drawable->last()->x();
$polygon[] = $this->drawable->last()->y();
if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 1) {
// create the border/stroke effect by calculating two new curves with offset positions
// from the main polygon and then connecting the inner/outer curves to create separate
// 4-point polygon segments
$polygon_total_points = count($polygon);
$offset = ($this->drawable->borderSize() / 2);
for ($i = 0; $i < $polygon_total_points; $i += 2) {
if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) {
$dx = $polygon[$i + 2] - $polygon[$i];
$dy = $polygon[$i + 3] - $polygon[$i + 1];
$dxy_sqrt = ($dx * $dx + $dy * $dy) ** 0.5;
// inner polygon
$scale = $offset / $dxy_sqrt;
$ox = -$dy * $scale;
$oy = $dx * $scale;
$inner_polygon[] = $ox + $polygon[$i];
$inner_polygon[] = $oy + $polygon[$i + 1];
$inner_polygon[] = $ox + $polygon[$i + 2];
$inner_polygon[] = $oy + $polygon[$i + 3];
// outer polygon
$scale = -$offset / $dxy_sqrt;
$ox = -$dy * $scale;
$oy = $dx * $scale;
$outer_polygon[] = $ox + $polygon[$i];
$outer_polygon[] = $oy + $polygon[$i + 1];
$outer_polygon[] = $ox + $polygon[$i + 2];
$outer_polygon[] = $oy + $polygon[$i + 3];
}
}
$inner_polygon_total_points = count($inner_polygon);
for ($i = 0; $i < $inner_polygon_total_points; $i += 2) {
if (array_key_exists($i + 2, $inner_polygon) && array_key_exists($i + 3, $inner_polygon)) {
$polygon_border_segments[] = [
$inner_polygon[$i],
$inner_polygon[$i + 1],
$outer_polygon[$i],
$outer_polygon[$i + 1],
$outer_polygon[$i + 2],
$outer_polygon[$i + 3],
$inner_polygon[$i + 2],
$inner_polygon[$i + 3],
];
}
}
}
return [$polygon, $polygon_border_segments];
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use RuntimeException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\DrawEllipseModifier as GenericDrawEllipseModifier;
class DrawEllipseModifier extends GenericDrawEllipseModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws RuntimeException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
if ($this->drawable->hasBorder()) {
imagealphablending($frame->native(), true);
// slightly smaller ellipse to keep 1px bordered edges clean
if ($this->drawable->hasBackgroundColor()) {
imagefilledellipse(
$frame->native(),
$this->drawable()->position()->x(),
$this->drawable->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->drawable()->position()->x(),
$this->drawable()->position()->y(),
$this->drawable->width(),
$this->drawable->height(),
0,
360,
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->borderColor()
)
);
} else {
imagealphablending($frame->native(), true);
imagesetthickness($frame->native(), 0);
imagefilledellipse(
$frame->native(),
$this->drawable()->position()->x(),
$this->drawable()->position()->y(),
$this->drawable->width(),
$this->drawable->height(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
)
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use RuntimeException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\DrawLineModifier as GenericDrawLineModifier;
class DrawLineModifier extends GenericDrawLineModifier implements SpecializedInterface
{
/**
* @throws RuntimeException
*/
public function apply(ImageInterface $image): ImageInterface
{
$color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
);
foreach ($image as $frame) {
imagealphablending($frame->native(), true);
imageantialias($frame->native(), true);
imagesetthickness($frame->native(), $this->drawable->width());
imageline(
$frame->native(),
$this->drawable->start()->x(),
$this->drawable->start()->y(),
$this->drawable->end()->x(),
$this->drawable->end()->y(),
$color
);
}
return $image;
}
}

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