Skip to content

Commit

Permalink
Use Imagick class
Browse files Browse the repository at this point in the history
  • Loading branch information
distantnative committed Oct 16, 2024
1 parent 2a78f6d commit 9047372
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 151 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"ext-apcu": "Support for the Apcu cache driver",
"ext-exif": "Support for exif information from images",
"ext-fileinfo": "Improved mime type detection for files",
"ext-imagick": "Improved thumbnail generation",
"ext-intl": "Improved i18n number formatting",
"ext-memcached": "Support for the Memcached cache driver",
"ext-sodium": "Support for the crypto class and more robust session handling",
Expand Down
6 changes: 4 additions & 2 deletions src/Image/Darkroom.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Exception;
use Kirby\Image\Darkroom\GdLib;
use Kirby\Image\Darkroom\ImageMagick;
use Kirby\Image\Darkroom\LegacyImageMagick;

/**
* A wrapper around resizing and cropping
Expand All @@ -19,8 +20,9 @@
class Darkroom
{
public static array $types = [
'gd' => GdLib::class,
'im' => ImageMagick::class
'gd' => GdLib::class,
'im' => ImageMagick::class,
'im-legacy' => LegacyImageMagick::class
];

public function __construct(
Expand Down
214 changes: 114 additions & 100 deletions src/Image/Darkroom/ImageMagick.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,79 @@
namespace Kirby\Image\Darkroom;

use Exception;
use Kirby\Filesystem\F;
use Imagick;
use Kirby\Image\Darkroom;
use Kirby\Image\Focus;

/**
* ImageMagick
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
* ImageMagick
*
* @package Kirby Image
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class ImageMagick extends Darkroom
{
/**
* Applies the blur settings
*/
protected function blur(string $file, array $options): string|null
protected function autoOrient(Imagick $image): void
{
if ($options['blur'] !== false) {
return '-blur ' . escapeshellarg('0x' . $options['blur']);
switch ($image->getImageOrientation()) {
case Imagick::ORIENTATION_TOPLEFT:
break;
case Imagick::ORIENTATION_TOPRIGHT:
$image->flopImage();
break;
case Imagick::ORIENTATION_BOTTOMRIGHT:
$image->rotateImage("#000", 180);
break;
case Imagick::ORIENTATION_BOTTOMLEFT:
$image->flopImage();
$image->rotateImage("#000", 180);
break;
case Imagick::ORIENTATION_LEFTTOP:
$image->flopImage();
$image->rotateImage("#000", -90);
break;
case Imagick::ORIENTATION_RIGHTTOP:
$image->rotateImage("#000", 90);
break;
case Imagick::ORIENTATION_RIGHTBOTTOM:
$image->flopImage();
$image->rotateImage("#000", 90);
break;
case Imagick::ORIENTATION_LEFTBOTTOM:
$image->rotateImage("#000", -90);
break;
default: // Invalid orientation
break;
}

return null;
$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
}

/**
* Keep animated gifs
* Applies the blur settings
*/
protected function coalesce(string $file, array $options): string|null
protected function blur(Imagick $image, array $options): Imagick
{
if (F::extension($file) === 'gif') {
return '-coalesce';
if ($options['blur'] !== false) {
return $image->blurImage(0.0, $options['blur']);
}

return null;
return $image;
}

/**
* Creates the convert command with the right path to the binary file
* Keep animated gifs
*/
protected function convert(string $file, array $options): string
protected function coalesce(Imagick $image): Imagick
{
$command = escapeshellarg($options['bin']);

// default is limiting to single-threading to keep CPU usage sane
$command .= ' -limit thread ' . escapeshellarg($options['threads']);
if ($image->getImageMimeType() === 'image/gif') {
return $image->coalesceImages();
}

// append input file
return $command . ' ' . escapeshellarg($file);
return $image;
}

/**
Expand All @@ -62,7 +84,6 @@ protected function convert(string $file, array $options): string
protected function defaults(): array
{
return parent::defaults() + [
'bin' => 'convert',
'interlace' => false,
'threads' => 1,
];
Expand All @@ -71,39 +92,35 @@ protected function defaults(): array
/**
* Applies the correct settings for grayscale images
*/
protected function grayscale(string $file, array $options): string|null
protected function grayscale(Imagick $image, array $options): void
{
if ($options['grayscale'] === true) {
return '-colorspace gray';
$image->setColorspace(Imagick::COLORSPACE_GRAY);
}

return null;
}

/**
* Applies sharpening if activated in the options.
*/
protected function sharpen(string $file, array $options): string|null
protected function sharpen(Imagick $image, array $options): Imagick
{
if (is_int($options['sharpen']) === false) {
return null;
return $image;
}

$amount = max(1, min(100, $options['sharpen'])) / 100;
return '-sharpen ' . escapeshellarg('0x' . $amount);
return $image->sharepenImage(0.0, $amount);
}

/**
* Applies the correct settings for interlaced JPEGs if
* activated via options
*/
protected function interlace(string $file, array $options): string|null
protected function interlace(Imagick $image, array $options): void
{
if ($options['interlace'] === true) {
return '-interlace line';
$image->setInterlaceScheme(Imagick::INTERLACE_LINE);
}

return null;
}

/**
Expand All @@ -115,29 +132,28 @@ protected function interlace(string $file, array $options): string|null
public function process(string $file, array $options = []): array
{
$options = $this->preprocess($file, $options);
$command = [];

$command[] = $this->convert($file, $options);
$command[] = $this->strip($file, $options);
$command[] = $this->interlace($file, $options);
$command[] = $this->coalesce($file, $options);
$command[] = $this->grayscale($file, $options);
$command[] = '-auto-orient';
$command[] = $this->resize($file, $options);
$command[] = $this->quality($file, $options);
$command[] = $this->blur($file, $options);
$command[] = $this->sharpen($file, $options);
$command[] = $this->save($file, $options);

// remove all null values and join the parts
$command = implode(' ', array_filter($command));

// try to execute the command
exec($command, $output, $return);

// log broken commands
if ($return !== 0) {
throw new Exception(message: 'The imagemagick convert command could not be executed: ' . $command);
$image = new Imagick($file);

$profiles = $image->getImageProfiles('icc', true);

$this->threads($image, $options);
$image->stripImage();
$this->interlace($image, $options);

$image = $this->coalesce($image);
$this->grayscale($image, $options);
$this->autoOrient($image);
$this->resize($image, $options);
$this->quality($image, $options);
$image = $this->blur($image, $options);
$image = $this->sharpen($image, $options);

if ($profiles !== []) {
$image->profileImage('icc', $profiles['icc']);
}

if ($this->save($image, $file, $options) === false) {
throw new Exception(message: 'The imagemagick result could not be generated');
}

return $options;
Expand All @@ -146,20 +162,23 @@ public function process(string $file, array $options = []): array
/**
* Applies the correct JPEG compression quality settings
*/
protected function quality(string $file, array $options): string
protected function quality(Imagick $image, array $options): void
{
return '-quality ' . escapeshellarg($options['quality']);
$image->setImageCompressionQuality($options['quality']);
}

/**
* Creates the correct options to crop or resize the image
* and translates the crop positions for imagemagick
*/
protected function resize(string $file, array $options): string
protected function resize(Imagick $image, array $options): void
{
// simple resize
if ($options['crop'] === false) {
return '-thumbnail ' . escapeshellarg(sprintf('%sx%s!', $options['width'], $options['height']));
$image->thumbnailImage(
$options['width'],
$options['height']
);
}

// crop based on focus point
Expand All @@ -171,12 +190,14 @@ protected function resize(string $file, array $options): string
$options['width'],
$options['height']
)) {
return sprintf(
'-crop %sx%s+%s+%s -resize %sx%s^',
$focus['width'],
$focus['height'],
$image->cropImage(
$options['width'],
$options['height'],
$focus['x1'],
$focus['y1'],
$focus['y1']
);

$image->thumbnailImage(
$options['width'],
$options['height']
);
Expand All @@ -185,49 +206,42 @@ protected function resize(string $file, array $options): string

// translate the gravity option into something imagemagick understands
$gravity = match ($options['crop'] ?? null) {
'top left' => 'NorthWest',
'top' => 'North',
'top right' => 'NorthEast',
'left' => 'West',
'right' => 'East',
'bottom left' => 'SouthWest',
'bottom' => 'South',
'bottom right' => 'SouthEast',
default => 'Center'
'top left' => Imagick::GRAVITY_NORTHWEST,
'top' => Imagick::GRAVITY_NORTH,
'top right' => Imagick::GRAVITY_NORTHEAST,
'left' => Imagick::GRAVITY_WEST,
'right' => Imagick::GRAVITY_EAST,
'bottom left' => Imagick::GRAVITY_SOUTHWEST,
'bottom' => Imagick::GRAVITY_SOUTH,
'bottom right' => Imagick::GRAVITY_SOUTHEAST,
default => Imagick::GRAVITY_CENTER
};

$command = '-thumbnail ' . escapeshellarg(sprintf('%sx%s^', $options['width'], $options['height']));
$command .= ' -gravity ' . escapeshellarg($gravity);
$command .= ' -crop ' . escapeshellarg(sprintf('%sx%s+0+0', $options['width'], $options['height']));

return $command;
$image->thumbnailImage($options['width'], $options['height']);
$image->setGravity($gravity);
$image->cropImage($options['width'], $options['height'], 0, 0);
}

/**
* Creates the option for the output file
*/
protected function save(string $file, array $options): string
protected function save(Imagick $image, string $file, array $options): bool
{
if ($options['format'] !== null) {
$file = pathinfo($file, PATHINFO_DIRNAME) . '/' . pathinfo($file, PATHINFO_FILENAME) . '.' . $options['format'];
}

return escapeshellarg($file);
return $image->writeImage($file);
}

/**
* Removes all metadata from the image
* Sets thread limit
*/
protected function strip(string $file, array $options): string
protected function threads(Imagick $image, array $options): void
{
if (F::extension($file) === 'png') {
// ImageMagick does not support keeping ICC profiles while
// stripping other privacy- and security-related information,
// such as GPS data; so discard all color profiles for PNG files
// (tested with ImageMagick 7.0.11-14 Q16 x86_64 2021-05-31)
return '-strip';
}

return '';
$image->setResourceLimit(
Imagick::RESOURCETYPE_THREAD,
$options['threads']
);
}
}
Loading

0 comments on commit 9047372

Please sign in to comment.