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,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;
}
}

View File

@@ -0,0 +1,36 @@
<?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\DrawPixelModifier as GenericDrawPixelModifier;
class DrawPixelModifier extends GenericDrawPixelModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
$color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->driver()->handleInput($this->color)
);
foreach ($image as $frame) {
imagealphablending($frame->native(), true);
imagesetpixel(
$frame->native(),
$this->position->x(),
$this->position->y(),
$color
);
}
return $image;
}
}

View File

@@ -0,0 +1,51 @@
<?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\DrawPolygonModifier as ModifiersDrawPolygonModifier;
class DrawPolygonModifier extends ModifiersDrawPolygonModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws RuntimeException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
if ($this->drawable->hasBackgroundColor()) {
imagealphablending($frame->native(), true);
imagesetthickness($frame->native(), 0);
imagefilledpolygon(
$frame->native(),
$this->drawable->toArray(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
)
);
}
if ($this->drawable->hasBorder()) {
imagealphablending($frame->native(), true);
imagesetthickness($frame->native(), $this->drawable->borderSize());
imagepolygon(
$frame->native(),
$this->drawable->toArray(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->borderColor()
)
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,61 @@
<?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\DrawRectangleModifier as GenericDrawRectangleModifier;
class DrawRectangleModifier extends GenericDrawRectangleModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws RuntimeException
*/
public function apply(ImageInterface $image): ImageInterface
{
$position = $this->drawable->position();
foreach ($image as $frame) {
// draw background
if ($this->drawable->hasBackgroundColor()) {
imagealphablending($frame->native(), true);
imagesetthickness($frame->native(), 0);
imagefilledrectangle(
$frame->native(),
$position->x(),
$position->y(),
$position->x() + $this->drawable->width(),
$position->y() + $this->drawable->height(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->backgroundColor()
)
);
}
// draw border
if ($this->drawable->hasBorder()) {
imagealphablending($frame->native(), true);
imagesetthickness($frame->native(), $this->drawable->borderSize());
imagerectangle(
$frame->native(),
$position->x(),
$position->y(),
$position->x() + $this->drawable->width(),
$position->y() + $this->drawable->height(),
$this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->borderColor()
)
);
}
}
return $image;
}
}

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