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

Refactor image decoding process of GD driver

This commit is contained in:
Oliver Vogel 2024-01-27 10:20:56 +01:00
parent ef8af613fb
commit 03a59283c3
No known key found for this signature in database
GPG Key ID: 1B19D214C02D69BB
7 changed files with 218 additions and 68 deletions

View File

@ -81,15 +81,15 @@ abstract class AbstractDecoder extends DriverSpecialized implements DecoderInter
}
try {
$input = match (true) {
$source = match (true) {
(strlen($path_or_data) <= PHP_MAXPATHLEN && is_file($path_or_data)) => $path_or_data, // path
default => $this->buildFilePointer($path_or_data), // data
};
// extract exif data via file path
$data = @exif_read_data($input, null, true);
if (is_resource($input)) {
fclose($input);
$data = @exif_read_data($source, null, true);
if (is_resource($source)) {
fclose($source);
}
} catch (Exception) {
$data = [];
@ -98,6 +98,26 @@ abstract class AbstractDecoder extends DriverSpecialized implements DecoderInter
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
*
@ -126,7 +146,7 @@ abstract class AbstractDecoder extends DriverSpecialized implements DecoderInter
$result = preg_match($pattern, $input, $matches);
return new class($matches, $result)
return new class ($matches, $result)
{
private $matches;
private $result;

View File

@ -9,30 +9,43 @@ use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Gif\Decoder as GifDecoder;
use Intervention\Gif\Splitter as GifSplitter;
use Intervention\Image\Drivers\Gd\Core;
use Intervention\Image\Drivers\Gd\Decoders\Traits\CanDecodeGif;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Image;
use Intervention\Image\Origin;
class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
{
use CanDecodeGif;
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
if ($this->isGifFormat($input)) {
return $this->decodeGif($input); // decode (animated) gif
}
$image = match ($this->isGifFormat($input)) {
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);
@ -54,49 +67,12 @@ class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
$this->extractExifData($input)
);
// set mediaType on origin
if ($info = getimagesizefromstring($input)) {
$image->setOrigin(
new Origin($info['mime'])
);
$image->origin()->setMediaType($info['mime']);
}
// fix image orientation
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
};
}
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')
);
return $this->adjustImageRotation($image);
}
}

View File

@ -5,13 +5,16 @@ declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Exception;
use Intervention\Image\Drivers\Gd\Decoders\Traits\CanDecodeGif;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterface
class FilePathImageDecoder extends GdImageDecoder implements DecoderInterface
{
use CanDecodeGif;
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
@ -30,12 +33,58 @@ class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterfac
throw new DecoderException('Unable to decode input');
}
// decode image
$image = parent::decode(file_get_contents($input));
// detect media (mime) type
$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()->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'];
}
}

View 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)
])
);
}
}

View 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);
}
}

View File

@ -37,6 +37,19 @@ class Origin
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
*

View File

@ -8,16 +8,6 @@ use Intervention\Image\Origin;
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
{
$origin = new Origin('image/jpeg', __DIR__ . '/tests/images/example.jpg');
@ -32,4 +22,17 @@ class OriginTest extends TestCase
$origin = new Origin('image/jpeg');
$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());
}
}