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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use GdImage;
use Intervention\Image\Drivers\AbstractDriver;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\MissingDependencyException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Format;
use Intervention\Image\FileExtension;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\CoreInterface;
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 MissingDependencyException(
'GD PHP extension must be installed to use this driver'
);
}
}
/**
* {@inheritdoc}
*
* @see DriverInterface::createImage()
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public function createImage(int $width, int $height): ImageInterface
{
if ($width < 1 || $height < 1) {
throw new InvalidArgumentException('Invalid image size. Only use int<1, max>');
}
// build new transparent GDImage
$data = imagecreatetruecolor($width, $height);
if (!$data instanceof GDImage) {
throw new DriverException('Failed to create new image');
}
imagesavealpha($data, true);
$background = imagecolorallocatealpha($data, 255, 255, 255, 127);
imagealphablending($data, false);
imagefill($data, 0, 0, $background);
imagecolortransparent($data, $background);
imageresolution($data, 72, 72);
return new Image($this, new Core([new Frame($data)]));
}
/**
* {@inheritdoc}
*
* @see DriverInterface::createCore()
*/
public function createCore(array $frames): CoreInterface
{
return new Core($frames);
}
/**
* {@inheritdoc}
*
* @see DriverInterface::colorProcessor()
*/
public function colorProcessor(ImageInterface $image): ColorProcessorInterface
{
return new ColorProcessor();
}
/**
* {@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 function version(): string
{
return gd_info()['GD Version'];
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Encoders\JpegEncoder as GenericJpegEncoder;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\StreamException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\EncodedImageInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
class JpegEncoder extends GenericJpegEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*
* @throws InvalidArgumentException
* @throws StateException
* @throws ModifierException
* @throws DriverException
* @throws StreamException
*/
public function encode(ImageInterface $image): EncodedImageInterface
{
$backgroundColor = $this->driver()->decodeColor(
$this->driver()->config()->backgroundColor
)->toColorspace(Rgb::class);
if (!$backgroundColor instanceof RgbColor) {
throw new ModifierException('Failed to normalize background color to rgb color space');
}
$output = Cloner::cloneBlended(
$image->core()->native(),
background: $backgroundColor
);
return $this->createEncodedImage(function ($stream) use ($output): void {
imageinterlace($output, $this->progressive);
imagejpeg($output, $stream, $this->quality);
}, 'image/jpeg');
}
}

View File

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

View File

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

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use Intervention\Image\Drivers\AbstractFontProcessor;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Geometry\Point;
use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Size;
class FontProcessor extends AbstractFontProcessor
{
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::boxSize()
*
* @throws DriverException
* @throws InvalidArgumentException
*/
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->hasFile()) {
// font size to gd's internal fonts (1-5)
$gdFont = (int) $font->size();
// calculate box size from gd font
$box = new Size(0, 0);
$chars = mb_strlen($text);
if ($chars > 0) {
$box->setWidth(
$chars * $this->gdCharacterWidth($gdFont)
);
$box->setHeight(
$this->gdCharacterHeight($gdFont)
);
}
return $box;
}
// calculate box size from ttf font file with angle 0
$box = imageftbbox(
size: $this->nativeFontSize($font),
angle: 0,
font_filename: $font->filepath(),
string: $text,
);
if ($box === false) {
throw new DriverException('Unable to calculate box size of font');
}
// build size from points
return new Size(
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,202 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd;
use GdImage;
use Intervention\Image\Drivers\AbstractFrame;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Size;
class Frame extends AbstractFrame implements FrameInterface
{
/**
* Create new frame instance
*/
public function __construct(
protected GdImage $native,
protected float $delay = 0,
protected int $disposalMethod = 1,
protected int $offsetLeft = 0,
protected int $offsetTop = 0
) {
//
}
/**
* {@inheritdoc}
*
* @see FrameInterface::toImage()
*/
public function toImage(DriverInterface $driver): ImageInterface
{
return new Image($driver, new Core([$this]));
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setNative()
*
* @throws InvalidArgumentException
*/
public function setNative(mixed $native): FrameInterface
{
if (!$native instanceof GdImage) {
throw new InvalidArgumentException(
'Value for argument setNative() "$native" must be instanceof of ' . GdImage::class,
);
}
$this->native = $native;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::native()
*/
public function native(): GdImage
{
return $this->native;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::size()
*
* @throws InvalidArgumentException
*/
public function size(): SizeInterface
{
return new Size(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::disposalMethod()
*/
public function disposalMethod(): int
{
return $this->disposalMethod;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setDisposalMethod()
*
* @throws InvalidArgumentException
*/
public function setDisposalMethod(int $method): FrameInterface
{
if (!in_array($method, [0, 1, 2, 3])) {
throw new InvalidArgumentException('Value for disposal method "$method" must be 0, 1, 2 or 3');
}
$this->disposalMethod = $method;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setOffset()
*/
public function setOffset(int $left, int $top): FrameInterface
{
$this->offsetLeft = $left;
$this->offsetTop = $top;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::offsetLeft()
*/
public function offsetLeft(): int
{
return $this->offsetLeft;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setOffsetLeft()
*/
public function setOffsetLeft(int $offset): FrameInterface
{
$this->offsetLeft = $offset;
return $this;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::offsetTop()
*/
public function offsetTop(): int
{
return $this->offsetTop;
}
/**
* {@inheritdoc}
*
* @see FrameInterface::setOffsetTop()
*/
public function setOffsetTop(int $offset): FrameInterface
{
$this->offsetTop = $offset;
return $this;
}
/**
* This workaround helps cloning GdImages which is currently not possible.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public function __clone(): void
{
$this->native = Cloner::clone($this->native);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
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()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
for ($i = 0; $i < $this->level; $i++) {
$result = imagefilter($frame->native(), IMG_FILTER_GAUSSIAN_BLUR);
if ($result === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to process blur effect',
);
}
}
}
return $image;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
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()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
$result = imagefilter($frame->native(), IMG_FILTER_BRIGHTNESS, intval($this->level * 2.55));
if ($result === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to set image brightness',
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
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()
*
* @throws ModifierException
*/
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) {
$result = imagefilter($frame->native(), IMG_FILTER_COLORIZE, $red, $green, $blue);
if ($result === false) {
throw new ModifierException('Failed to apply colorize effect');
}
}
return $image;
}
}

View File

@@ -0,0 +1,32 @@
<?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()
*
* @throws NotSupportedException
*/
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,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
class ContainDownModifier extends ContainModifier
{
/**
* Calculate crop size of the contain down resizing process.
*/
protected function cropSize(ImageInterface $image): SizeInterface
{
return $image->size()
->containDown(
$this->width,
$this->height
)
->alignPivotTo(
$this->resizeSize($image),
$this->alignment
);
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\ModifierException;
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;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\StateException;
class ContainModifier extends GenericContainModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
* @throws ModifierException
* @throws StateException
* @throws DriverException
*/
public function apply(ImageInterface $image): ImageInterface
{
$crop = $this->cropSize($image);
$resize = $this->resizeSize($image);
$backgroundColor = $this->backgroundColor()->toColorspace(Rgb::class);
if (!$backgroundColor instanceof RgbColor) {
throw new ModifierException('Failed to normalize background color to RGB color space');
}
foreach ($image as $frame) {
$this->modify($frame, $crop, $resize, $backgroundColor);
}
return $image;
}
/**
* @throws InvalidArgumentException
* @throws ModifierException
* @throws DriverException
*/
private function modify(
FrameInterface $frame,
SizeInterface $crop,
SizeInterface $resize,
RgbColor $backgroundColor
): void {
// create new gd image
$modified = Cloner::cloneEmpty($frame->native(), $resize, $backgroundColor);
// make image area transparent to keep transparency
// even if background-color is set
$transparent = imagecolorallocatealpha(
$modified,
$backgroundColor->red()->value(),
$backgroundColor->green()->value(),
$backgroundColor->blue()->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 background 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,32 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
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()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
$result = imagefilter($frame->native(), IMG_FILTER_CONTRAST, ($this->level * -1));
if ($result === false) {
throw new ModifierException('Failed to set image contrast');
}
}
return $image;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\SizeInterface;
class CoverDownModifier extends CoverModifier
{
/**
* Calculate resizing size of the cover down process
*/
protected function resizeSize(SizeInterface $size): SizeInterface
{
return $size->resizeDown($this->width, $this->height);
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
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()
*
* @throws InvalidArgumentException
* @throws ModifierException
* @throws DriverException
*/
public function apply(ImageInterface $image): ImageInterface
{
$crop = $this->cropSize($image);
$resize = $this->resizeSize($crop);
foreach ($image as $frame) {
$this->modifyFrame($frame, $crop, $resize);
}
return $image;
}
/**
* @throws InvalidArgumentException
* @throws ModifierException
* @throws DriverException
*/
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,93 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
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;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Exceptions\DriverException;
class CropModifier extends GenericCropModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
* @throws ModifierException
* @throws StateException
* @throws DriverException
*/
public function apply(ImageInterface $image): ImageInterface
{
$originalSize = $image->size();
$crop = $this->crop($image);
$background = $this->backgroundColor()->toColorspace(RgbColorspace::class);
if (!$background instanceof RgbColor) {
throw new ModifierException('Failed to normalize background color to RGB color space');
}
foreach ($image as $frame) {
$this->cropFrame($frame, $originalSize, $crop, $background);
}
return $image;
}
/**
* @throws InvalidArgumentException
* @throws ModifierException
* @throws DriverException
*/
private function cropFrame(
FrameInterface $frame,
SizeInterface $originalSize,
SizeInterface $resizeTo,
RgbColor $background
): void {
// create new image with transparent background
$modified = Cloner::cloneEmpty($frame->native(), $resizeTo, $background);
// define offset
$offsetX = $resizeTo->pivot()->x() + $this->x;
$offsetY = $resizeTo->pivot()->y() + $this->y;
// define target width & height
$targetWidth = min($resizeTo->width(), $originalSize->width());
$targetHeight = min($resizeTo->height(), $originalSize->height());
$targetWidth = $targetWidth < $originalSize->width() ? $targetWidth + $offsetX : $targetWidth;
$targetHeight = $targetHeight < $originalSize->height() ? $targetHeight + $offsetY : $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(),
$offsetX * -1,
$offsetY * -1,
0,
0,
$targetWidth,
$targetHeight,
$targetWidth,
$targetHeight
);
// set new content as resource
$frame->setNative($modified);
}
}

View File

@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
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 InvalidArgumentException
* @throws ModifierException
* @throws StateException
* @throws ColorDecoderException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
throw new InvalidArgumentException('You must specify either 3 or 4 points to create a bezier curve');
}
[$polygon, $polygonBorderSegments] = $this->calculateBezierPoints();
if ($this->drawable->hasBackgroundColor() || $this->drawable->hasBorder()) {
$result = imagealphablending($frame->native(), true);
$this->abortUnless($result, 'Unable to set alpha blending');
$result = imageantialias($frame->native(), true);
$this->abortUnless($result, 'Unable to set image antialias option');
}
if ($this->drawable->hasBackgroundColor()) {
$backgroundColor = $this->driver()->colorProcessor($image)->export(
$this->backgroundColor()
);
$result = imagesetthickness($frame->native(), 0);
$this->abortUnless($result, 'Unable to set line thickness');
$result = imagefilledpolygon(
$frame->native(),
$polygon,
$backgroundColor
);
$this->abortUnless($result, 'Unable to draw line on image');
}
if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) {
$borderColor = $this->driver()->colorProcessor($image)->export(
$this->borderColor()
);
if ($this->drawable->borderSize() === 1) {
$result = imagesetthickness($frame->native(), $this->drawable->borderSize());
$this->abortUnless($result, 'Unable to set line thickness');
$count = count($polygon);
for ($i = 0; $i < $count; $i += 2) {
if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) {
$result = imageline(
$frame->native(),
$polygon[$i],
$polygon[$i + 1],
$polygon[$i + 2],
$polygon[$i + 3],
$borderColor
);
$this->abortUnless($result, 'Unable to draw line on image');
}
}
} else {
$polygonBorderSegmentsTotal = count($polygonBorderSegments);
for ($i = 0; $i < $polygonBorderSegmentsTotal; $i += 1) {
$result = imagefilledpolygon(
$frame->native(),
$polygonBorderSegments[$i],
$borderColor
);
$this->abortUnless($result, 'Unable to draw line on image');
}
}
}
}
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;
$controlPoint1Multiplier = $remainder * $remainder;
$controlPoint2Multiplier = $remainder * $t * 2;
$controlPoint3Multiplier = $t * $t;
$x = (
$this->drawable->first()->x() * $controlPoint1Multiplier +
$this->drawable->second()->x() * $controlPoint2Multiplier +
$this->drawable->last()->x() * $controlPoint3Multiplier
);
$y = (
$this->drawable->first()->y() * $controlPoint1Multiplier +
$this->drawable->second()->y() * $controlPoint2Multiplier +
$this->drawable->last()->y() * $controlPoint3Multiplier
);
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;
$tSquared = $t * $t;
$remainderSquared = $remainder * $remainder;
$controlPoint1Multiplier = $remainderSquared * $remainder;
$controlPoint2Multiplier = $remainderSquared * $t * 3;
$controlPoint3Multiplier = $tSquared * $remainder * 3;
$controlPoint4Multiplier = $tSquared * $t;
$x = (
$this->drawable->first()->x() * $controlPoint1Multiplier +
$this->drawable->second()->x() * $controlPoint2Multiplier +
$this->drawable->third()->x() * $controlPoint3Multiplier +
$this->drawable->last()->x() * $controlPoint4Multiplier
);
$y = (
$this->drawable->first()->y() * $controlPoint1Multiplier +
$this->drawable->second()->y() * $controlPoint2Multiplier +
$this->drawable->third()->y() * $controlPoint3Multiplier +
$this->drawable->last()->y() * $controlPoint4Multiplier
);
return ['x' => $x, 'y' => $y];
}
/**
* Calculate the points needed to draw a quadratic or cubic bezier with optional border/stroke
*
* @throws InvalidArgumentException
* @throws ModifierException
* @return array{0: array<mixed>, 1: array<mixed>}
*/
private function calculateBezierPoints(): array
{
if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
throw new InvalidArgumentException('You must specify either 3 or 4 points to create a bezier curve');
}
$polygon = [];
$innerPolygon = [];
$outerPolygon = [];
$polygonBorderSegments = [];
// 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
$polygonTotalPoints = count($polygon);
$offset = ($this->drawable->borderSize() / 2);
for ($i = 0; $i < $polygonTotalPoints; $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];
$dxySqrt = sqrt($dx * $dx + $dy * $dy);
// prevent division by zero
if ($dxySqrt === 0.0) {
throw new ModifierException('Failed to apply ' . self::class . ', division by zero');
}
// inner polygon
$scale = $offset / $dxySqrt;
$ox = -$dy * $scale;
$oy = $dx * $scale;
$innerPolygon[] = $ox + $polygon[$i];
$innerPolygon[] = $oy + $polygon[$i + 1];
$innerPolygon[] = $ox + $polygon[$i + 2];
$innerPolygon[] = $oy + $polygon[$i + 3];
// outer polygon
$scale = -$offset / $dxySqrt;
$ox = -$dy * $scale;
$oy = $dx * $scale;
$outerPolygon[] = $ox + $polygon[$i];
$outerPolygon[] = $oy + $polygon[$i + 1];
$outerPolygon[] = $ox + $polygon[$i + 2];
$outerPolygon[] = $oy + $polygon[$i + 3];
}
}
$innerPolygonTotalPoints = count($innerPolygon);
for ($i = 0; $i < $innerPolygonTotalPoints; $i += 2) {
if (array_key_exists($i + 2, $innerPolygon) && array_key_exists($i + 3, $innerPolygon)) {
$polygonBorderSegments[] = [
$innerPolygon[$i],
$innerPolygon[$i + 1],
$outerPolygon[$i],
$outerPolygon[$i + 1],
$outerPolygon[$i + 2],
$outerPolygon[$i + 3],
$innerPolygon[$i + 2],
$innerPolygon[$i + 3],
];
}
}
}
return [$polygon, $polygonBorderSegments];
}
/**
* Throw ModifierException with given message if result is 'false'
*
* @throws ModifierException
*/
private function abortUnless(mixed $result, string $message): void
{
if ($result === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', ' . $message
);
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
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 ModifierException
* @throws StateException
* @throws ColorDecoderException
*/
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)->export(
$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)->export(
$this->borderColor()
)
);
} elseif ($this->drawable->hasBackgroundColor()) {
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)->export(
$this->backgroundColor()
)
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\DrawLineModifier as GenericDrawLineModifier;
class DrawLineModifier extends GenericDrawLineModifier implements SpecializedInterface
{
/**
* @throws ModifierException
* @throws StateException
* @throws ColorDecoderException
*/
public function apply(ImageInterface $image): ImageInterface
{
if (!$this->drawable->hasBackgroundColor()) {
return $image;
}
$color = $this->driver()->colorProcessor($image)->export(
$this->backgroundColor()
);
foreach ($image as $frame) {
$this->modifyFrame($frame, $color);
}
return $image;
}
/**
* Draw current line on given frame
*
* @throws ModifierException
*/
private function modifyFrame(FrameInterface $frame, int $color): void
{
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
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
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()
*
* @throws ModifierException
* @throws StateException
* @throws ColorDecoderException
*/
public function apply(ImageInterface $image): ImageInterface
{
$color = $this->driver()->colorProcessor($image)->export($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,55 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\DrawPolygonModifier as GenericDrawPolygonModifier;
class DrawPolygonModifier extends GenericDrawPolygonModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
* @throws StateException
* @throws ColorDecoderException
*/
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)->export(
$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)->export(
$this->borderColor()
)
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
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 ModifierException
* @throws StateException
* @throws ColorDecoderException
*/
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)->export(
$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)->export(
$this->borderColor()
)
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ColorDecoderException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\FillModifier as GenericFillModifier;
class FillModifier extends GenericFillModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
* @throws StateException
* @throws ColorDecoderException
*/
public function apply(ImageInterface $image): ImageInterface
{
$color = $this->driver()->colorProcessor($image)->export(
$this->color()
);
foreach ($image as $frame) {
if ($this->hasPosition()) {
$this->floodFillWithColor($frame, $color);
} else {
$this->fillAllWithColor($frame, $color);
}
}
return $image;
}
/**
* @throws ModifierException
*/
private function floodFillWithColor(FrameInterface $frame, int $color): void
{
imagefill(
$frame->native(),
$this->position->x(),
$this->position->y(),
$color
);
}
/**
* @throws ModifierException
*/
private function fillAllWithColor(FrameInterface $frame, int $color): void
{
imagealphablending($frame->native(), true);
imagefilledrectangle(
$frame->native(),
0,
0,
$frame->size()->width() - 1,
$frame->size()->height() - 1,
$color
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\FillTransparentAreasModifier as GenericFillTransparentAreasModifier;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
class FillTransparentAreasModifier extends GenericFillTransparentAreasModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
* @throws ModifierException
* @throws StateException
* @throws DriverException
*/
public function apply(ImageInterface $image): ImageInterface
{
$backgroundColor = $this->backgroundColor($this->driver())->toColorspace(RgbColorspace::class);
if (!$backgroundColor instanceof RgbColor) {
throw new ModifierException('Failed to normalize background color to RGB color space');
}
foreach ($image as $frame) {
// create new canvas with background color as background
$modified = Cloner::cloneBlended(
$frame->native(),
background: $backgroundColor
);
// set new gd image
$frame->setNative($modified);
}
return $image;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Direction;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\FlipModifier as GenericFlipModifier;
class FlipModifier extends GenericFlipModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
$direction = $this->direction === Direction::HORIZONTAL ? IMG_FLIP_HORIZONTAL : IMG_FLIP_VERTICAL;
foreach ($image as $frame) {
imageflip($frame->native(), $direction);
}
return $image;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\GammaModifier as GenericGammaModifier;
class GammaModifier extends GenericGammaModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
imagegammacorrect($frame->native(), 1, $this->gamma);
}
return $image;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\GrayscaleModifier as GenericGrayscaleModifier;
class GrayscaleModifier extends GenericGrayscaleModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
$result = imagefilter($frame->native(), IMG_FILTER_GRAYSCALE);
if ($result === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to transform image to grayscale',
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\PointInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\InsertModifier as GenericInsertModifier;
use Intervention\Image\Traits\CanConvertRange;
class InsertModifier extends GenericInsertModifier implements SpecializedInterface
{
use CanConvertRange;
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
* @throws StateException
*/
public function apply(ImageInterface $image): ImageInterface
{
$watermark = $this->driver()->decodeImage($this->image);
$position = $this->position($image, $watermark);
foreach ($image as $frame) {
imagealphablending($frame->native(), true);
if ($this->transparency === 1.0) {
$this->insertOpaque($frame, $watermark, $position);
} else {
$this->insertTransparent($frame, $watermark, $position);
}
}
return $image;
}
/**
* Insert watermark with 100% opacity
*
* @throws ModifierException
*/
private function insertOpaque(FrameInterface $frame, ImageInterface $watermark, PointInterface $position): void
{
imagecopy(
$frame->native(),
$watermark->core()->native(),
$position->x(),
$position->y(),
0,
0,
$watermark->width(),
$watermark->height()
);
}
/**
* Insert watermark transparent with current transparency
*
* Unfortunately, the original PHP function imagecopymerge does not work reliably.
* For example, any transparency of the image to be inserted is not applied correctly.
* For this reason, a new GDImage is created into which the original image is inserted
* in the first step and the watermark is inserted with 100% opacity in the second
* step. This combination is then transferred to the original image again with the
* respective opacity.
*
* Please note: Unfortunately, there is still an edge case, when a transparent image
* is inserted on a transparent background, the "double" transparent areas appear opaque!
*
* @throws ModifierException
*/
private function insertTransparent(FrameInterface $frame, ImageInterface $watermark, PointInterface $position): void
{
$cut = imagecreatetruecolor($watermark->width(), $watermark->height());
if ($cut === false) {
throw new ModifierException('Failed to insert image');
}
imagecopy(
$cut,
$frame->native(),
0,
0,
$position->x(),
$position->y(),
imagesx($cut),
imagesy($cut)
);
imagecopy(
$cut,
$watermark->core()->native(),
0,
0,
0,
0,
imagesx($cut),
imagesy($cut)
);
try {
$transparency = (int) round(self::convertRange($this->transparency, 0, 1, 0, 100));
} catch (RuntimeException $e) {
throw new ModifierException('Failed to convert transparency', previous: $e);
}
imagecopymerge(
$frame->native(),
$cut,
$position->x(),
$position->y(),
0,
0,
$watermark->width(),
$watermark->height(),
$transparency,
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\InvertModifier as GenericInvertModifier;
class InvertModifier extends GenericInvertModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
$result = imagefilter($frame->native(), IMG_FILTER_NEGATE);
if ($result === false) {
throw new ModifierException('Failed to invert image colors');
}
}
return $image;
}
}

View File

@@ -0,0 +1,62 @@
<?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\OrientModifier as GenericOrientModifier;
class OrientModifier extends GenericOrientModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
$image = match ($this->orientation($image)) {
2 => $image->flip(),
3 => $image->rotate(180),
4 => $image->rotate(180)->flip(),
5 => $image->rotate(90)->flip(),
6 => $image->rotate(90),
7 => $image->rotate(270)->flip(),
8 => $image->rotate(270),
default => $image
};
return $this->markAligned($image);
}
/**
* Return exif information about image orientation.
*/
private function orientation(ImageInterface $image): int
{
$orientation = $image->exif('IFD0.Orientation');
return is_numeric($orientation) ? (int) $orientation : 0;
}
/**
* 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,34 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\PixelateModifier as GenericPixelateModifier;
class PixelateModifier extends GenericPixelateModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
foreach ($image as $frame) {
$result = imagefilter($frame->native(), IMG_FILTER_PIXELATE, $this->size, true);
if ($result === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to process pixelation effect',
);
}
}
return $image;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ProfileModifier as GenericProfileModifier;
class ProfileModifier extends GenericProfileModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws NotSupportedException
*/
public function apply(ImageInterface $image): ImageInterface
{
throw new NotSupportedException(
'Color profiles are not supported by GD driver'
);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ReduceColorsModifier as GenericReduceColorsModifier;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Exceptions\DriverException;
class ReduceColorsModifier extends GenericReduceColorsModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
* @throws StateException
* @throws ModifierException
* @throws DriverException
*/
public function apply(ImageInterface $image): ImageInterface
{
if ($this->limit <= 0) {
throw new InvalidArgumentException('Quantization limit must be greater than 0');
}
// no color reduction if the limit is higher than the colors in the img
$colorCount = imagecolorstotal($image->core()->native());
if ($colorCount > 0 && $this->limit > $colorCount) {
return $image;
}
$width = $image->width();
$height = $image->height();
$backgroundColor = $this->backgroundColor($image);
if (!$backgroundColor instanceof RgbColor) {
throw new ModifierException('Failed to convert background color to RGB color space');
}
$nativeBackgroundColor = $this->driver()
->colorProcessor($image)
->export($backgroundColor);
foreach ($image as $frame) {
// create new image for color quantization
$reduced = Cloner::cloneEmpty($frame->native(), background: $backgroundColor);
// fill with background
imagefill($reduced, 0, 0, $nativeBackgroundColor);
// set transparency
imagecolortransparent($reduced, $nativeBackgroundColor);
// copy original image (colors are limited automatically in the copy process)
imagecopy($reduced, $frame->native(), 0, 0, 0, 0, $width, $height);
// gd library does not support color quantization directly therefore the
// colors are decrease by transforming the image to a palette version
imagetruecolortopalette($reduced, true, $this->limit);
$frame->setNative($reduced);
}
return $image;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\RemoveAnimationModifier as GenericRemoveAnimationModifier;
class RemoveAnimationModifier extends GenericRemoveAnimationModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
*/
public function apply(ImageInterface $image): ImageInterface
{
$image->core()->setNative(
$this->selectedFrame($image)->native()
);
return $image;
}
}

View File

@@ -0,0 +1,24 @@
<?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\RemoveProfileModifier as GenericRemoveProfileModifier;
class RemoveProfileModifier extends GenericRemoveProfileModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*/
public function apply(ImageInterface $image): ImageInterface
{
// Color profiles are not supported by GD, so the decoded
// image is already free of profiles anyway.
return $image;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ResizeCanvasModifier as GenericResizeCanvasModifier;
class ResizeCanvasModifier extends GenericResizeCanvasModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
* @throws StateException
*/
public function apply(ImageInterface $image): ImageInterface
{
$cropSize = $this->cropSize($image);
$image->modify(new CropModifier(
$cropSize->width(),
$cropSize->height(),
$cropSize->pivot()->x(),
$cropSize->pivot()->y(),
$this->backgroundColor(),
));
return $image;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
class ResizeCanvasRelativeModifier extends ResizeCanvasModifier
{
protected function cropSize(ImageInterface $image, bool $relative = false): SizeInterface
{
return parent::cropSize($image, true);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
class ResizeDownModifier extends ResizeModifier
{
protected function adjustedSize(ImageInterface $image): SizeInterface
{
return $image->size()->resizeDown($this->width, $this->height);
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ResizeModifier as GenericResizeModifier;
class ResizeModifier extends GenericResizeModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
* @throws ModifierException
* @throws DriverException
*/
public function apply(ImageInterface $image): ImageInterface
{
$resizeTo = $this->adjustedSize($image);
foreach ($image as $frame) {
$this->resizeFrame($frame, $resizeTo);
}
return $image;
}
/**
* @throws InvalidArgumentException
* @throws ModifierException
* @throws DriverException
*/
private function resizeFrame(FrameInterface $frame, SizeInterface $resizeTo): void
{
// create empty canvas in target size
$modified = Cloner::cloneEmpty($frame->native(), $resizeTo);
// copy content from resource
imagecopyresampled(
$modified,
$frame->native(),
$resizeTo->pivot()->x(),
$resizeTo->pivot()->y(),
0,
0,
$resizeTo->width(),
$resizeTo->height(),
$frame->size()->width(),
$frame->size()->height()
);
// set new content as resource
$frame->setNative($modified);
}
/**
* Return the size the modifier will resize to
*/
protected function adjustedSize(ImageInterface $image): SizeInterface
{
return $image->size()->resize($this->width, $this->height);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\ResolutionModifier as GenericResolutionModifier;
class ResolutionModifier extends GenericResolutionModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
$x = intval(round($this->x));
$y = intval(round($this->y));
foreach ($image as $frame) {
imageresolution($frame->native(), $x, $y);
}
// GD returns 96x96 as resolution by default even if the image has no resolution.
// This is problematic because it is impossible to tell whether the image
// really has this resolution or whether it just corresponds to the default value.
//
// If the resolution was change to 96x96 (default resolution of GD) we mark
// the resolution as changed to be able to distinguish it
if ($x === 96 && $y === 96) {
$image->core()->meta()->set('resolutionChanged', true);
}
return $image;
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Alignment;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Rgb\Colorspace as Rgb;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\RotateModifier as GenericRotateModifier;
use Intervention\Image\Size;
class RotateModifier extends GenericRotateModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
* @throws ModifierException
* @throws StateException
* @throws DriverException
*/
public function apply(ImageInterface $image): ImageInterface
{
$background = $this->backgroundColor();
foreach ($image as $frame) {
$this->modifyFrame($frame, $background);
}
return $image;
}
/**
* Apply rotation modification on given frame, given background
* color is used for newly create image areas
*
* @throws InvalidArgumentException
* @throws ModifierException
* @throws DriverException
*/
protected function modifyFrame(FrameInterface $frame, ColorInterface $background): void
{
// normalize color to rgb colorspace
$background = $background->toColorspace(Rgb::class);
if (!$background instanceof RgbColor) {
throw new ModifierException('Failed to normalize background color to RGB color space');
}
// get transparent color from frame core
$transparent = match ($transparent = imagecolortransparent($frame->native())) {
-1 => imagecolorallocatealpha(
$frame->native(),
$background->red()->value(),
$background->green()->value(),
$background->blue()->value(),
127
),
default => $transparent,
};
// rotate original image against transparent background
$rotated = imagerotate(
$frame->native(),
$this->rotationAngle() * -1,
$transparent
);
// create size from original after rotation
$container = (new Size(
imagesx($rotated),
imagesy($rotated),
))->movePivot(Alignment::CENTER);
// create size from original and rotate points
$cutout = Polygon::fromSize(new Size(
imagesx($frame->native()),
imagesy($frame->native()),
$container->pivot()
))->alignHorizontally(Alignment::CENTER)
->alignVertically(Alignment::CENTER)
->rotate($this->rotationAngle());
// create new gd image
$modified = Cloner::cloneEmpty($frame->native(), $container, $background);
// draw the cutout on new gd image to have a transparent
// background where the rotated image will be placed
imagealphablending($modified, false);
imagefilledpolygon(
$modified,
$cutout->toArray(),
imagecolortransparent($modified)
);
// place rotated image on new gd image
imagealphablending($modified, true);
imagecopy(
$modified,
$rotated,
0,
0,
0,
0,
imagesx($rotated),
imagesy($rotated)
);
$frame->setNative($modified);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
class ScaleDownModifier extends ResizeModifier
{
/**
* {@inheritdoc}
*
* @see ResizeModifier::adjustedSize()
*/
protected function adjustedSize(ImageInterface $image): SizeInterface
{
return $image->size()->scaleDown($this->width, $this->height);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SizeInterface;
class ScaleModifier extends ResizeModifier
{
protected function adjustedSize(ImageInterface $image): SizeInterface
{
return $image->size()->scale($this->width, $this->height);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\SharpenModifier as GenericSharpenModifier;
class SharpenModifier extends GenericSharpenModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
$matrix = $this->matrix();
foreach ($image as $frame) {
$result = imageconvolution($frame->native(), $matrix, 1, 0);
if ($result === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to set convolution matrix',
);
}
}
return $image;
}
/**
* Create matrix to be used by imageconvolution()
*
* @return array<array<float>>
*/
private function matrix(): array
{
$min = $this->level >= 10 ? $this->level * -0.01 : 0;
$max = $this->level * -0.025;
$abs = ((4 * $min + 4 * $max) * -1) + 1;
return [
[$min, $max, $min],
[$max, $abs, $max],
[$min, $max, $min]
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\SliceAnimationModifier as GenericSliceAnimationModifier;
class SliceAnimationModifier extends GenericSliceAnimationModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws InvalidArgumentException
*/
public function apply(ImageInterface $image): ImageInterface
{
if ($this->offset >= $image->count()) {
throw new InvalidArgumentException('Offset is not in the range of frames');
}
$image->core()->slice($this->offset, $this->length);
return $image;
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\TextModifier as GenericTextModifier;
class TextModifier extends GenericTextModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws ModifierException
* @throws StateException
*/
public function apply(ImageInterface $image): ImageInterface
{
$fontProcessor = $this->driver()->fontProcessor();
$lines = $fontProcessor->textBlock($this->text, $this->font, $this->position);
// decode text colors
$textColor = $this->gdTextColor($image);
$strokeColor = $this->gdStrokeColor($image);
foreach ($image as $frame) {
imagealphablending($frame->native(), true);
if ($this->font->hasFile()) {
foreach ($lines as $line) {
foreach ($this->strokeOffsets($this->font) as $offset) {
imagettftext(
image: $frame->native(),
size: $fontProcessor->nativeFontSize($this->font),
angle: $this->font->angle() * -1,
x: $line->position()->x() + $offset->x(),
y: $line->position()->y() + $offset->y(),
color: $strokeColor,
font_filename: $this->font->filepath(),
text: (string) $line
);
}
imagettftext(
image: $frame->native(),
size: $fontProcessor->nativeFontSize($this->font),
angle: $this->font->angle() * -1,
x: $line->position()->x(),
y: $line->position()->y(),
color: $textColor,
font_filename: $this->font->filepath(),
text: (string) $line
);
}
} else {
foreach ($lines as $line) {
foreach ($this->strokeOffsets($this->font) as $offset) {
imagestring(
image: $frame->native(),
font: $this->gdFont(),
x: $line->position()->x() + $offset->x(),
y: $line->position()->y() + $offset->y(),
string: (string) $line,
color: $strokeColor
);
}
imagestring(
image: $frame->native(),
font: $this->gdFont(),
x: $line->position()->x(),
y: $line->position()->y(),
string: (string) $line,
color: $textColor
);
}
}
}
return $image;
}
/**
* Decode text color in GD compatible format
*
* @throws StateException
*/
protected function gdTextColor(ImageInterface $image): int
{
return $this
->driver()
->colorProcessor($image)
->export(parent::textColor());
}
/**
* Decode color for stroke (outline) effect in GD compatible format
*
* @throws StateException
*/
protected function gdStrokeColor(ImageInterface $image): int
{
if (!$this->font->hasStrokeEffect()) {
return 0;
}
$color = parent::strokeColor();
if ($color->isTransparent()) {
throw new StateException('The stroke color must be fully opaque');
}
return $this
->driver()
->colorProcessor($image)
->export($color);
}
/**
* Return GD's internal font size
*/
private function gdFont(): int
{
if (!in_array($this->font->size(), range(1, 5))) {
return 1;
}
return (int) $this->font->size();
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Exceptions\ModifierException;
use Intervention\Image\Exceptions\NotSupportedException;
use Intervention\Image\Exceptions\StateException;
use Intervention\Image\Geometry\Point;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Modifiers\TrimModifier as GenericTrimModifier;
use ValueError;
class TrimModifier extends GenericTrimModifier implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see ModifierInterface::apply()
*
* @throws NotSupportedException
* @throws StateException
* @throws ModifierException
*/
public function apply(ImageInterface $image): ImageInterface
{
if ($image->isAnimated()) {
throw new NotSupportedException('Trim modifier cannot be applied to animated images');
}
// apply tolerance with a min. value of .5 because the default tolerance of '0' should
// already trim away similar colors which is not the case with imagecropauto.
$trimmed = imagecropauto(
$image->core()->native(),
IMG_CROP_THRESHOLD,
max([.5, $this->tolerance / 10]),
$this->trimColor($image)
);
// if the tolerance is very high, it is possible that no image is left.
// imagick returns a 1x1 pixel image in this case. this does the same.
if ($trimmed === false) {
$trimmed = $this->driver()->createImage(1, 1)->core()->native();
}
$image->core()->setNative($trimmed);
return $image;
}
/**
* Create an average color from the colors of the four corner points of the given image
*
* @throws ModifierException
*/
private function trimColor(ImageInterface $image): int
{
// trim color base
$red = 0;
$green = 0;
$blue = 0;
// corner coordinates
$size = $image->size();
$cornerPoints = [
new Point(0, 0),
new Point($size->width() - 1, 0),
new Point(0, $size->height() - 1),
new Point($size->width() - 1, $size->height() - 1),
];
// create an average color to be used in trim operation
foreach ($cornerPoints as $pos) {
$cornerColor = imagecolorat($image->core()->native(), $pos->x(), $pos->y());
if ($cornerColor === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to determine average color for process',
);
}
try {
$rgb = imagecolorsforindex($image->core()->native(), $cornerColor);
} catch (ValueError) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to read trim color from index',
);
}
$red += round(round($rgb['red'] / 51) * 51);
$green += round(round($rgb['green'] / 51) * 51);
$blue += round(round($rgb['blue'] / 51) * 51);
}
$red = (int) round($red / 4);
$green = (int) round($green / 4);
$blue = (int) round($blue / 4);
$color = imagecolorallocate($image->core()->native(), $red, $green, $blue);
if ($color === false) {
throw new ModifierException(
'Failed to apply ' . self::class . ', unable to allocate trim color',
);
}
return $color;
}
}