Skip to content

Commit

Permalink
Optimize image decoding with Imagick driver
Browse files Browse the repository at this point in the history
File paths are now read directly via Imagick::readImage() instead of
reading everything with file_get_contents() and passing it to
BinaryImageDecoder.
  • Loading branch information
olivervogel committed Jan 22, 2024
1 parent d37e47b commit d27fdd7
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 67 deletions.
22 changes: 15 additions & 7 deletions src/Drivers/AbstractDecoder.php
Expand Up @@ -68,21 +68,29 @@ protected function isGifFormat(string $input): bool
}

/**
* Extract and return EXIF data from given image data string
* Extract and return EXIF data from given input which can be binary image
* data or a file path.
*
* @param string $image_data
* @param string $path_or_data
* @return CollectionInterface
*/
protected function extractExifData(string $image_data): CollectionInterface
protected function extractExifData(string $path_or_data): CollectionInterface
{
if (!function_exists('exif_read_data')) {
return new Collection();
}

try {
$pointer = $this->buildFilePointer($image_data);
$data = @exif_read_data($pointer, null, true);
fclose($pointer);
$input = 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);
}
} catch (Exception) {
$data = [];
}
Expand Down Expand Up @@ -118,7 +126,7 @@ protected function parseDataUri(mixed $input): object

$result = preg_match($pattern, $input, $matches);

return new class ($matches, $result)
return new class($matches, $result)
{
private $matches;
private $result;
Expand Down
63 changes: 5 additions & 58 deletions src/Drivers/Imagick/Decoders/BinaryImageDecoder.php
Expand Up @@ -6,17 +6,12 @@

use Imagick;
use ImagickException;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Drivers\Imagick\Core;
use Intervention\Image\Drivers\Imagick\Driver;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Origin;

class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
class BinaryImageDecoder extends ImagickImageDecoder implements DecoderInterface
{
public function decode(mixed $input): ImageInterface|ColorInterface
{
Expand All @@ -31,59 +26,11 @@ public function decode(mixed $input): ImageInterface|ColorInterface
throw new DecoderException('Unable to decode input');
}

// For some JPEG formats, the "coalesceImages()" call leads to an image
// completely filled with background color. The logic behind this is
// incomprehensible for me; could be an imagick bug.
if ($imagick->getImageFormat() != 'JPEG') {
$imagick = $imagick->coalesceImages();
}

// fix image orientation
switch ($imagick->getImageOrientation()) {
case Imagick::ORIENTATION_TOPRIGHT: // 2
$imagick->flopImage();
break;

case Imagick::ORIENTATION_BOTTOMRIGHT: // 3
$imagick->rotateimage("#000", 180);
break;

case Imagick::ORIENTATION_BOTTOMLEFT: // 4
$imagick->rotateimage("#000", 180);
$imagick->flopImage();
break;

case Imagick::ORIENTATION_LEFTTOP: // 5
$imagick->rotateimage("#000", -270);
$imagick->flopImage();
break;

case Imagick::ORIENTATION_RIGHTTOP: // 6
$imagick->rotateimage("#000", -270);
break;

case Imagick::ORIENTATION_RIGHTBOTTOM: // 7
$imagick->rotateimage("#000", -90);
$imagick->flopImage();
break;

case Imagick::ORIENTATION_LEFTBOTTOM: // 8
$imagick->rotateimage("#000", -90);
break;
}

// set new orientation in image
$imagick->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);

$image = new Image(
new Driver(),
new Core($imagick),
$this->extractExifData($input)
);
// decode image
$image = parent::decode($imagick);

$image->setOrigin(new Origin(
$imagick->getImageMimeType()
));
// extract exif data
$image->setExif($this->extractExifData($input));

return $image;
}
Expand Down
16 changes: 14 additions & 2 deletions src/Drivers/Imagick/Decoders/FilePathImageDecoder.php
Expand Up @@ -5,12 +5,14 @@
namespace Intervention\Image\Drivers\Imagick\Decoders;

use Exception;
use Imagick;
use ImagickException;
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 ImagickImageDecoder implements DecoderInterface
{
public function decode(mixed $input): ImageInterface|ColorInterface
{
Expand All @@ -30,12 +32,22 @@ public function decode(mixed $input): ImageInterface|ColorInterface
throw new DecoderException('Unable to decode input');
}

try {
$imagick = new Imagick();
$imagick->readImage($input);
} catch (ImagickException) {
throw new DecoderException('Unable to decode input');
}

// decode image
$image = parent::decode(file_get_contents($input));
$image = parent::decode($imagick);

// set file path on origin
$image->origin()->setFilePath($input);

// extract exif data
$image->setExif($this->extractExifData($input));

return $image;
}
}
85 changes: 85 additions & 0 deletions src/Drivers/Imagick/Decoders/ImagickImageDecoder.php
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Intervention\Image\Drivers\Imagick\Decoders;

use Imagick;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Drivers\Imagick\Core;
use Intervention\Image\Drivers\Imagick\Driver;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Origin;

class ImagickImageDecoder extends AbstractDecoder implements DecoderInterface
{
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_object($input)) {
throw new DecoderException('Unable to decode input');
}

if (!($input instanceof Imagick)) {
throw new DecoderException('Unable to decode input');
}

// For some JPEG formats, the "coalesceImages()" call leads to an image
// completely filled with background color. The logic behind this is
// incomprehensible for me; could be an imagick bug.
if ($input->getImageFormat() != 'JPEG') {
$input = $input->coalesceImages();
}

// fix image orientation
switch ($input->getImageOrientation()) {
case Imagick::ORIENTATION_TOPRIGHT: // 2
$input->flopImage();
break;

case Imagick::ORIENTATION_BOTTOMRIGHT: // 3
$input->rotateimage("#000", 180);
break;

case Imagick::ORIENTATION_BOTTOMLEFT: // 4
$input->rotateimage("#000", 180);
$input->flopImage();
break;

case Imagick::ORIENTATION_LEFTTOP: // 5
$input->rotateimage("#000", -270);
$input->flopImage();
break;

case Imagick::ORIENTATION_RIGHTTOP: // 6
$input->rotateimage("#000", -270);
break;

case Imagick::ORIENTATION_RIGHTBOTTOM: // 7
$input->rotateimage("#000", -90);
$input->flopImage();
break;

case Imagick::ORIENTATION_LEFTBOTTOM: // 8
$input->rotateimage("#000", -90);
break;
}

// set new orientation in image
$input->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);

$image = new Image(
new Driver(),
new Core($input)
);

$image->setOrigin(new Origin(
$input->getImageMimeType()
));

return $image;
}
}
12 changes: 12 additions & 0 deletions src/Image.php
Expand Up @@ -249,6 +249,18 @@ public function exif(?string $query = null): mixed
return is_null($query) ? $this->exif : $this->exif->get($query);
}

/**
* {@inheritdoc}
*
* @see ImgageInterface::setExif()
*/
public function setExif(CollectionInterface $exif): ImageInterface
{
$this->exif = $exif;

return $this;
}

/**
* {@inheritdoc}
*
Expand Down
8 changes: 8 additions & 0 deletions src/Interfaces/ImageInterface.php
Expand Up @@ -144,6 +144,14 @@ public function setLoops(int $loops): ImageInterface;
*/
public function exif(?string $query = null): mixed;

/**
* Set exif data for the image object
*
* @param CollectionInterface $exif
* @return ImageInterface
*/
public function setExif(CollectionInterface $exif): ImageInterface;

/**
* Return image resolution/density
*
Expand Down

0 comments on commit d27fdd7

Please sign in to comment.