mirror of
https://github.com/Intervention/image.git
synced 2025-08-14 01:44:03 +02:00
Refactor image decoding process of GD driver
This commit is contained in:
@@ -81,15 +81,15 @@ abstract class AbstractDecoder extends DriverSpecialized implements DecoderInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$input = match (true) {
|
$source = match (true) {
|
||||||
(strlen($path_or_data) <= PHP_MAXPATHLEN && is_file($path_or_data)) => $path_or_data, // path
|
(strlen($path_or_data) <= PHP_MAXPATHLEN && is_file($path_or_data)) => $path_or_data, // path
|
||||||
default => $this->buildFilePointer($path_or_data), // data
|
default => $this->buildFilePointer($path_or_data), // data
|
||||||
};
|
};
|
||||||
|
|
||||||
// extract exif data via file path
|
// extract exif data via file path
|
||||||
$data = @exif_read_data($input, null, true);
|
$data = @exif_read_data($source, null, true);
|
||||||
if (is_resource($input)) {
|
if (is_resource($source)) {
|
||||||
fclose($input);
|
fclose($source);
|
||||||
}
|
}
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
$data = [];
|
$data = [];
|
||||||
@@ -98,6 +98,26 @@ abstract class AbstractDecoder extends DriverSpecialized implements DecoderInter
|
|||||||
return new Collection(is_array($data) ? $data : []);
|
return new Collection(is_array($data) ? $data : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust image rotation of given image according to the exif data
|
||||||
|
*
|
||||||
|
* @param ImageInterface $image
|
||||||
|
* @return ImageInterface
|
||||||
|
*/
|
||||||
|
protected function adjustImageRotation(ImageInterface $image): ImageInterface
|
||||||
|
{
|
||||||
|
return match ($image->exif('IFD0.Orientation')) {
|
||||||
|
2 => $image->flop(),
|
||||||
|
3 => $image->rotate(180),
|
||||||
|
4 => $image->rotate(180)->flop(),
|
||||||
|
5 => $image->rotate(270)->flop(),
|
||||||
|
6 => $image->rotate(270),
|
||||||
|
7 => $image->rotate(90)->flop(),
|
||||||
|
8 => $image->rotate(90),
|
||||||
|
default => $image
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if given input is base64 encoded data
|
* Determine if given input is base64 encoded data
|
||||||
*
|
*
|
||||||
@@ -126,7 +146,7 @@ abstract class AbstractDecoder extends DriverSpecialized implements DecoderInter
|
|||||||
|
|
||||||
$result = preg_match($pattern, $input, $matches);
|
$result = preg_match($pattern, $input, $matches);
|
||||||
|
|
||||||
return new class($matches, $result)
|
return new class ($matches, $result)
|
||||||
{
|
{
|
||||||
private $matches;
|
private $matches;
|
||||||
private $result;
|
private $result;
|
||||||
|
@@ -9,30 +9,43 @@ use Intervention\Image\Drivers\Gd\Frame;
|
|||||||
use Intervention\Image\Interfaces\ColorInterface;
|
use Intervention\Image\Interfaces\ColorInterface;
|
||||||
use Intervention\Image\Interfaces\DecoderInterface;
|
use Intervention\Image\Interfaces\DecoderInterface;
|
||||||
use Intervention\Image\Interfaces\ImageInterface;
|
use Intervention\Image\Interfaces\ImageInterface;
|
||||||
use Intervention\Gif\Decoder as GifDecoder;
|
|
||||||
use Intervention\Gif\Splitter as GifSplitter;
|
|
||||||
use Intervention\Image\Drivers\Gd\Core;
|
use Intervention\Image\Drivers\Gd\Core;
|
||||||
|
use Intervention\Image\Drivers\Gd\Decoders\Traits\CanDecodeGif;
|
||||||
use Intervention\Image\Drivers\Gd\Driver;
|
use Intervention\Image\Drivers\Gd\Driver;
|
||||||
use Intervention\Image\Exceptions\DecoderException;
|
use Intervention\Image\Exceptions\DecoderException;
|
||||||
use Intervention\Image\Image;
|
use Intervention\Image\Image;
|
||||||
use Intervention\Image\Origin;
|
|
||||||
|
|
||||||
class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
|
class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
|
||||||
{
|
{
|
||||||
|
use CanDecodeGif;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @see DecoderInterface::decode()
|
||||||
|
*/
|
||||||
public function decode(mixed $input): ImageInterface|ColorInterface
|
public function decode(mixed $input): ImageInterface|ColorInterface
|
||||||
{
|
{
|
||||||
if (!is_string($input)) {
|
if (!is_string($input)) {
|
||||||
throw new DecoderException('Unable to decode input');
|
throw new DecoderException('Unable to decode input');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGifFormat($input)) {
|
$image = match ($this->isGifFormat($input)) {
|
||||||
return $this->decodeGif($input); // decode (animated) gif
|
true => $this->decodeGif($input),
|
||||||
}
|
default => $this->decodeBinary($input),
|
||||||
|
};
|
||||||
|
|
||||||
return $this->decodeString($input);
|
return $image;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function decodeString(string $input): ImageInterface
|
/**
|
||||||
|
* Decode image from given binary data
|
||||||
|
*
|
||||||
|
* @param string $input
|
||||||
|
* @return ImageInterface
|
||||||
|
* @throws DecoderException
|
||||||
|
*/
|
||||||
|
private function decodeBinary(string $input): ImageInterface
|
||||||
{
|
{
|
||||||
$gd = @imagecreatefromstring($input);
|
$gd = @imagecreatefromstring($input);
|
||||||
|
|
||||||
@@ -54,49 +67,12 @@ class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
|
|||||||
$this->extractExifData($input)
|
$this->extractExifData($input)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// set mediaType on origin
|
||||||
if ($info = getimagesizefromstring($input)) {
|
if ($info = getimagesizefromstring($input)) {
|
||||||
$image->setOrigin(
|
$image->origin()->setMediaType($info['mime']);
|
||||||
new Origin($info['mime'])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix image orientation
|
// fix image orientation
|
||||||
return match ($image->exif('IFD0.Orientation')) {
|
return $this->adjustImageRotation($image);
|
||||||
2 => $image->flop(),
|
|
||||||
3 => $image->rotate(180),
|
|
||||||
4 => $image->rotate(180)->flop(),
|
|
||||||
5 => $image->rotate(270)->flop(),
|
|
||||||
6 => $image->rotate(270),
|
|
||||||
7 => $image->rotate(90)->flop(),
|
|
||||||
8 => $image->rotate(90),
|
|
||||||
default => $image
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function decodeGif(string $input): ImageInterface
|
|
||||||
{
|
|
||||||
$gif = GifDecoder::decode($input);
|
|
||||||
|
|
||||||
if (!$gif->isAnimated()) {
|
|
||||||
return $this->decodeString($input);
|
|
||||||
}
|
|
||||||
|
|
||||||
$splitter = GifSplitter::create($gif)->split();
|
|
||||||
$delays = $splitter->getDelays();
|
|
||||||
|
|
||||||
// build core
|
|
||||||
$core = new Core();
|
|
||||||
$core->setLoops($gif->getMainApplicationExtension()?->getLoops());
|
|
||||||
foreach ($splitter->coalesceToResources() as $key => $data) {
|
|
||||||
$core->push(
|
|
||||||
(new Frame($data))->setDelay($delays[$key] / 100)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$image = new Image(new Driver(), $core);
|
|
||||||
|
|
||||||
return $image->setOrigin(
|
|
||||||
new Origin('image/gif')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,13 +5,16 @@ declare(strict_types=1);
|
|||||||
namespace Intervention\Image\Drivers\Gd\Decoders;
|
namespace Intervention\Image\Drivers\Gd\Decoders;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Intervention\Image\Drivers\Gd\Decoders\Traits\CanDecodeGif;
|
||||||
use Intervention\Image\Exceptions\DecoderException;
|
use Intervention\Image\Exceptions\DecoderException;
|
||||||
use Intervention\Image\Interfaces\ColorInterface;
|
use Intervention\Image\Interfaces\ColorInterface;
|
||||||
use Intervention\Image\Interfaces\DecoderInterface;
|
use Intervention\Image\Interfaces\DecoderInterface;
|
||||||
use Intervention\Image\Interfaces\ImageInterface;
|
use Intervention\Image\Interfaces\ImageInterface;
|
||||||
|
|
||||||
class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterface
|
class FilePathImageDecoder extends GdImageDecoder implements DecoderInterface
|
||||||
{
|
{
|
||||||
|
use CanDecodeGif;
|
||||||
|
|
||||||
public function decode(mixed $input): ImageInterface|ColorInterface
|
public function decode(mixed $input): ImageInterface|ColorInterface
|
||||||
{
|
{
|
||||||
if (!is_string($input)) {
|
if (!is_string($input)) {
|
||||||
@@ -30,12 +33,58 @@ class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterfac
|
|||||||
throw new DecoderException('Unable to decode input');
|
throw new DecoderException('Unable to decode input');
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode image
|
// detect media (mime) type
|
||||||
$image = parent::decode(file_get_contents($input));
|
$mediaType = $this->getMediaTypeByFilePath($input);
|
||||||
|
|
||||||
// set file path on origin
|
// gif files might be animated and therefore cannot be handled by the standard GD decoder.
|
||||||
|
$image = match ($mediaType) {
|
||||||
|
'image/gif' => $this->decodeGif($input),
|
||||||
|
default => parent::decode(match ($mediaType) {
|
||||||
|
'image/jpeg', 'image/jpg', 'image/pjpeg' => imagecreatefromjpeg($input),
|
||||||
|
'image/webp', 'image/x-webp' => imagecreatefromwebp($input),
|
||||||
|
'image/png', 'image/x-png' => imagecreatefrompng($input),
|
||||||
|
'image/avif', 'image/x-avif' => imagecreatefromavif($input),
|
||||||
|
'image/bmp',
|
||||||
|
'image/ms-bmp',
|
||||||
|
'image/x-bitmap',
|
||||||
|
'image/x-bmp',
|
||||||
|
'image/x-ms-bmp',
|
||||||
|
'image/x-win-bitmap',
|
||||||
|
'image/x-windows-bmp',
|
||||||
|
'image/x-xbitmap' => imagecreatefrombmp($input),
|
||||||
|
default => throw new DecoderException('Unable to decode input'),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// set file path & mediaType on origin
|
||||||
$image->origin()->setFilePath($input);
|
$image->origin()->setFilePath($input);
|
||||||
|
$image->origin()->setMediaType($mediaType);
|
||||||
|
|
||||||
return $image;
|
// extract exif
|
||||||
|
$image->setExif($this->extractExifData($input));
|
||||||
|
|
||||||
|
// fix image orientation
|
||||||
|
return $this->adjustImageRotation($image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return media (mime) type of the file at given file path
|
||||||
|
*
|
||||||
|
* @param string $filepath
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getMediaTypeByFilePath(string $filepath): string
|
||||||
|
{
|
||||||
|
$info = getimagesize($filepath);
|
||||||
|
|
||||||
|
if (!is_array($info)) {
|
||||||
|
throw new DecoderException('Unable to decode input');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('mime', $info)) {
|
||||||
|
throw new DecoderException('Unable to decode input');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $info['mime'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
43
src/Drivers/Gd/Decoders/GdImageDecoder.php
Normal file
43
src/Drivers/Gd/Decoders/GdImageDecoder.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Drivers\Gd\Decoders;
|
||||||
|
|
||||||
|
use GdImage;
|
||||||
|
use Intervention\Image\Drivers\AbstractDecoder;
|
||||||
|
use Intervention\Image\Drivers\Gd\Core;
|
||||||
|
use Intervention\Image\Drivers\Gd\Driver;
|
||||||
|
use Intervention\Image\Drivers\Gd\Frame;
|
||||||
|
use Intervention\Image\Exceptions\DecoderException;
|
||||||
|
use Intervention\Image\Image;
|
||||||
|
use Intervention\Image\Interfaces\ImageInterface;
|
||||||
|
use Intervention\Image\Interfaces\ColorInterface;
|
||||||
|
|
||||||
|
class GdImageDecoder extends AbstractDecoder
|
||||||
|
{
|
||||||
|
public function decode(mixed $input): ImageInterface|ColorInterface
|
||||||
|
{
|
||||||
|
if (!is_object($input)) {
|
||||||
|
throw new DecoderException('Unable to decode input');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($input instanceof GdImage)) {
|
||||||
|
throw new DecoderException('Unable to decode input');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageistruecolor($input)) {
|
||||||
|
imagepalettetotruecolor($input);
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesavealpha($input, true);
|
||||||
|
|
||||||
|
// build image instance
|
||||||
|
return new Image(
|
||||||
|
new Driver(),
|
||||||
|
new Core([
|
||||||
|
new Frame($input)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
src/Drivers/Gd/Decoders/Traits/CanDecodeGif.php
Normal file
46
src/Drivers/Gd/Decoders/Traits/CanDecodeGif.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Drivers\Gd\Decoders\Traits;
|
||||||
|
|
||||||
|
use Intervention\Gif\Decoder as GifDecoder;
|
||||||
|
use Intervention\Gif\Splitter as GifSplitter;
|
||||||
|
use Intervention\Image\Drivers\Gd\Core;
|
||||||
|
use Intervention\Image\Drivers\Gd\Driver;
|
||||||
|
use Intervention\Image\Drivers\Gd\Frame;
|
||||||
|
use Intervention\Image\Image;
|
||||||
|
use Intervention\Image\Interfaces\ImageInterface;
|
||||||
|
|
||||||
|
trait CanDecodeGif
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Decode image from given GIF source which can be either a file path or binary data
|
||||||
|
*
|
||||||
|
* @param mixed $input
|
||||||
|
* @return ImageInterface
|
||||||
|
*/
|
||||||
|
protected function decodeGif(mixed $input): ImageInterface
|
||||||
|
{
|
||||||
|
$gif = GifDecoder::decode($input);
|
||||||
|
$splitter = GifSplitter::create($gif)->split();
|
||||||
|
$delays = $splitter->getDelays();
|
||||||
|
|
||||||
|
// build core
|
||||||
|
$core = new Core();
|
||||||
|
|
||||||
|
// set loops on core
|
||||||
|
if ($loops = $gif->getMainApplicationExtension()?->getLoops()) {
|
||||||
|
$core->setLoops($loops);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add GDImage instances to core
|
||||||
|
foreach ($splitter->coalesceToResources() as $key => $native) {
|
||||||
|
$core->push(
|
||||||
|
(new Frame($native))->setDelay($delays[$key] / 100)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Image(new Driver(), $core);
|
||||||
|
}
|
||||||
|
}
|
@@ -37,6 +37,19 @@ class Origin
|
|||||||
return $this->mediaType();
|
return $this->mediaType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set media type of current instance
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return Origin
|
||||||
|
*/
|
||||||
|
public function setMediaType(string $type): self
|
||||||
|
{
|
||||||
|
$this->mediaType = $type;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return file path of origin
|
* Return file path of origin
|
||||||
*
|
*
|
||||||
|
@@ -8,16 +8,6 @@ use Intervention\Image\Origin;
|
|||||||
|
|
||||||
class OriginTest extends TestCase
|
class OriginTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testMediaType(): void
|
|
||||||
{
|
|
||||||
$origin = new Origin();
|
|
||||||
$this->assertEquals('application/octet-stream', $origin->mediaType());
|
|
||||||
|
|
||||||
$origin = new Origin('image/gif');
|
|
||||||
$this->assertEquals('image/gif', $origin->mediaType());
|
|
||||||
$this->assertEquals('image/gif', $origin->mimetype());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFilePath(): void
|
public function testFilePath(): void
|
||||||
{
|
{
|
||||||
$origin = new Origin('image/jpeg', __DIR__ . '/tests/images/example.jpg');
|
$origin = new Origin('image/jpeg', __DIR__ . '/tests/images/example.jpg');
|
||||||
@@ -32,4 +22,17 @@ class OriginTest extends TestCase
|
|||||||
$origin = new Origin('image/jpeg');
|
$origin = new Origin('image/jpeg');
|
||||||
$this->assertEquals('', $origin->fileExtension());
|
$this->assertEquals('', $origin->fileExtension());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSetGetMediaType(): void
|
||||||
|
{
|
||||||
|
$origin = new Origin();
|
||||||
|
$this->assertEquals('application/octet-stream', $origin->mediaType());
|
||||||
|
|
||||||
|
$origin = new Origin('image/gif');
|
||||||
|
$this->assertEquals('image/gif', $origin->mediaType());
|
||||||
|
$this->assertEquals('image/gif', $origin->mimetype());
|
||||||
|
$result = $origin->setMediaType('image/jpeg');
|
||||||
|
$this->assertEquals('image/jpeg', $origin->mediaType());
|
||||||
|
$this->assertEquals('image/jpeg', $result->mediaType());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user