diff --git a/src/Drivers/AbstractDecoder.php b/src/Drivers/AbstractDecoder.php index 52188bbe..5d8a69ce 100644 --- a/src/Drivers/AbstractDecoder.php +++ b/src/Drivers/AbstractDecoder.php @@ -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; diff --git a/src/Drivers/Gd/Decoders/BinaryImageDecoder.php b/src/Drivers/Gd/Decoders/BinaryImageDecoder.php index 8afb16ac..92c45b44 100644 --- a/src/Drivers/Gd/Decoders/BinaryImageDecoder.php +++ b/src/Drivers/Gd/Decoders/BinaryImageDecoder.php @@ -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); } } diff --git a/src/Drivers/Gd/Decoders/FilePathImageDecoder.php b/src/Drivers/Gd/Decoders/FilePathImageDecoder.php index be0cbff0..329d52d1 100644 --- a/src/Drivers/Gd/Decoders/FilePathImageDecoder.php +++ b/src/Drivers/Gd/Decoders/FilePathImageDecoder.php @@ -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']; } } diff --git a/src/Drivers/Gd/Decoders/GdImageDecoder.php b/src/Drivers/Gd/Decoders/GdImageDecoder.php new file mode 100644 index 00000000..c812b579 --- /dev/null +++ b/src/Drivers/Gd/Decoders/GdImageDecoder.php @@ -0,0 +1,43 @@ +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); + } +} diff --git a/src/Origin.php b/src/Origin.php index 9aa4780f..9e5413a5 100644 --- a/src/Origin.php +++ b/src/Origin.php @@ -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 * diff --git a/tests/OriginTest.php b/tests/OriginTest.php index 0ee86e1d..88ec9673 100644 --- a/tests/OriginTest.php +++ b/tests/OriginTest.php @@ -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()); + } }