Initial commit
This commit is contained in:
commit
efa5624dab
687 changed files with 162710 additions and 0 deletions
130
kirby/src/Image/Darkroom/GdLib.php
Normal file
130
kirby/src/Image/Darkroom/GdLib.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Image\Darkroom;
|
||||
|
||||
use claviska\SimpleImage;
|
||||
use Kirby\Filesystem\Mime;
|
||||
use Kirby\Image\Darkroom;
|
||||
use Kirby\Image\Focus;
|
||||
|
||||
/**
|
||||
* GdLib darkroom driver
|
||||
*
|
||||
* @package Kirby Image
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class GdLib extends Darkroom
|
||||
{
|
||||
/**
|
||||
* Processes the image with the SimpleImage library
|
||||
*/
|
||||
public function process(string $file, array $options = []): array
|
||||
{
|
||||
$options = $this->preprocess($file, $options);
|
||||
$mime = $this->mime($options);
|
||||
|
||||
$image = new SimpleImage();
|
||||
$image->fromFile($file);
|
||||
$image->autoOrient();
|
||||
|
||||
$image = $this->resize($image, $options);
|
||||
$image = $this->blur($image, $options);
|
||||
$image = $this->grayscale($image, $options);
|
||||
$image = $this->sharpen($image, $options);
|
||||
|
||||
$image->toFile($file, $mime, $options);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around SimpleImage's resize and crop methods
|
||||
*/
|
||||
protected function resize(SimpleImage $image, array $options): SimpleImage
|
||||
{
|
||||
// just resize, no crop
|
||||
if ($options['crop'] === false) {
|
||||
return $image->resize($options['width'], $options['height']);
|
||||
}
|
||||
|
||||
// crop based on focus point
|
||||
if (Focus::isFocalPoint($options['crop']) === true) {
|
||||
// get crop coords for focal point:
|
||||
// if image needs to be cropped, crop before resizing
|
||||
if ($focus = Focus::coords(
|
||||
$options['crop'],
|
||||
$options['sourceWidth'],
|
||||
$options['sourceHeight'],
|
||||
$options['width'],
|
||||
$options['height']
|
||||
)) {
|
||||
$image->crop(
|
||||
$focus['x1'],
|
||||
$focus['y1'],
|
||||
$focus['x2'],
|
||||
$focus['y2']
|
||||
);
|
||||
}
|
||||
|
||||
return $image->thumbnail($options['width'], $options['height']);
|
||||
}
|
||||
|
||||
// normal crop with crop anchor
|
||||
return $image->thumbnail(
|
||||
$options['width'],
|
||||
$options['height'] ?? $options['width'],
|
||||
$options['crop']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct blur settings for SimpleImage
|
||||
*/
|
||||
protected function blur(SimpleImage $image, array $options): SimpleImage
|
||||
{
|
||||
if ($options['blur'] === false) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
return $image->blur('gaussian', (int)$options['blur']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies grayscale conversion if activated in the options.
|
||||
*/
|
||||
protected function grayscale(SimpleImage $image, array $options): SimpleImage
|
||||
{
|
||||
if ($options['grayscale'] === false) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
return $image->desaturate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies sharpening if activated in the options.
|
||||
*/
|
||||
protected function sharpen(SimpleImage $image, array $options): SimpleImage
|
||||
{
|
||||
if (is_int($options['sharpen']) === false) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
return $image->sharpen($options['sharpen']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns mime type based on `format` option
|
||||
*/
|
||||
protected function mime(array $options): string|null
|
||||
{
|
||||
if ($options['format'] === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Mime::fromExtension($options['format']);
|
||||
}
|
||||
}
|
||||
236
kirby/src/Image/Darkroom/ImageMagick.php
Normal file
236
kirby/src/Image/Darkroom/ImageMagick.php
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Image\Darkroom;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Image\Darkroom;
|
||||
use Kirby\Image\Focus;
|
||||
|
||||
/**
|
||||
* Legacy ImageMagick driver using the convert CLI
|
||||
*
|
||||
* @package Kirby Image
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* @deprecated 5.1.0 Use `imagick` in the `thumbs.driver` config option instead
|
||||
* @todo Remove in 7.0.0
|
||||
*/
|
||||
class ImageMagick extends Darkroom
|
||||
{
|
||||
/**
|
||||
* Applies the blur settings
|
||||
*/
|
||||
protected function blur(string $file, array $options): string|null
|
||||
{
|
||||
if ($options['blur'] !== false) {
|
||||
return '-blur ' . escapeshellarg('0x' . $options['blur']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep animated gifs
|
||||
*/
|
||||
protected function coalesce(string $file, array $options): string|null
|
||||
{
|
||||
if (F::extension($file) === 'gif') {
|
||||
return '-coalesce';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the convert command with the right path to the binary file
|
||||
*/
|
||||
protected function convert(string $file, array $options): string
|
||||
{
|
||||
$command = escapeshellarg($options['bin']);
|
||||
|
||||
// default is limiting to single-threading to keep CPU usage sane
|
||||
$command .= ' -limit thread ' . escapeshellarg($options['threads']);
|
||||
|
||||
// append input file
|
||||
return $command . ' ' . escapeshellarg($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns additional default parameters for imagemagick
|
||||
*/
|
||||
protected function defaults(): array
|
||||
{
|
||||
return parent::defaults() + [
|
||||
'bin' => 'convert',
|
||||
'interlace' => false,
|
||||
'threads' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct settings for grayscale images
|
||||
*/
|
||||
protected function grayscale(string $file, array $options): string|null
|
||||
{
|
||||
if ($options['grayscale'] === true) {
|
||||
return '-colorspace gray';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies sharpening if activated in the options.
|
||||
*/
|
||||
protected function sharpen(string $file, array $options): string|null
|
||||
{
|
||||
if (is_int($options['sharpen']) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$amount = max(1, min(100, $options['sharpen'])) / 100;
|
||||
return '-sharpen ' . escapeshellarg('0x' . $amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct settings for interlaced JPEGs if
|
||||
* activated via options
|
||||
*/
|
||||
protected function interlace(string $file, array $options): string|null
|
||||
{
|
||||
if ($options['interlace'] === true) {
|
||||
return '-interlace line';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and runs the full imagemagick command
|
||||
* to process the image
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct JPEG compression quality settings
|
||||
*/
|
||||
protected function quality(string $file, array $options): string
|
||||
{
|
||||
return '-quality ' . escapeshellarg($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
|
||||
{
|
||||
// simple resize
|
||||
if ($options['crop'] === false) {
|
||||
return '-thumbnail ' . escapeshellarg(sprintf('%sx%s!', $options['width'], $options['height']));
|
||||
}
|
||||
|
||||
// crop based on focus point
|
||||
if (Focus::isFocalPoint($options['crop']) === true) {
|
||||
if ($focus = Focus::coords(
|
||||
$options['crop'],
|
||||
$options['sourceWidth'],
|
||||
$options['sourceHeight'],
|
||||
$options['width'],
|
||||
$options['height']
|
||||
)) {
|
||||
return sprintf(
|
||||
'-crop %sx%s+%s+%s -resize %sx%s^',
|
||||
$focus['width'],
|
||||
$focus['height'],
|
||||
$focus['x1'],
|
||||
$focus['y1'],
|
||||
$options['width'],
|
||||
$options['height']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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'
|
||||
};
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the option for the output file
|
||||
*/
|
||||
protected function save(string $file, array $options): string
|
||||
{
|
||||
if ($options['format'] !== null) {
|
||||
$file = pathinfo($file, PATHINFO_DIRNAME) . '/' . pathinfo($file, PATHINFO_FILENAME) . '.' . $options['format'];
|
||||
}
|
||||
|
||||
return escapeshellarg($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all metadata from the image
|
||||
*/
|
||||
protected function strip(string $file, array $options): string
|
||||
{
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
292
kirby/src/Image/Darkroom/Imagick.php
Normal file
292
kirby/src/Image/Darkroom/Imagick.php
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Image\Darkroom;
|
||||
|
||||
use Exception;
|
||||
use Imagick as Image;
|
||||
use Kirby\Image\Darkroom;
|
||||
use Kirby\Image\Focus;
|
||||
|
||||
/**
|
||||
* Imagick darkroom driver
|
||||
*
|
||||
* @package Kirby Image
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
* @since 5.1.0
|
||||
*/
|
||||
class Imagick extends Darkroom
|
||||
{
|
||||
protected function autoOrient(Image $image): Image
|
||||
{
|
||||
switch ($image->getImageOrientation()) {
|
||||
case Image::ORIENTATION_TOPLEFT:
|
||||
break;
|
||||
case Image::ORIENTATION_TOPRIGHT:
|
||||
$image->flopImage();
|
||||
break;
|
||||
case Image::ORIENTATION_BOTTOMRIGHT:
|
||||
$image->rotateImage('#000', 180);
|
||||
break;
|
||||
case Image::ORIENTATION_BOTTOMLEFT:
|
||||
$image->flopImage();
|
||||
$image->rotateImage('#000', 180);
|
||||
break;
|
||||
case Image::ORIENTATION_LEFTTOP:
|
||||
$image->flopImage();
|
||||
$image->rotateImage('#000', -90);
|
||||
break;
|
||||
case Image::ORIENTATION_RIGHTTOP:
|
||||
$image->rotateImage('#000', 90);
|
||||
break;
|
||||
case Image::ORIENTATION_RIGHTBOTTOM:
|
||||
$image->flopImage();
|
||||
$image->rotateImage('#000', 90);
|
||||
break;
|
||||
case Image::ORIENTATION_LEFTBOTTOM:
|
||||
$image->rotateImage('#000', -90);
|
||||
break;
|
||||
default: // Invalid orientation
|
||||
break;
|
||||
}
|
||||
|
||||
$image->setImageOrientation(Image::ORIENTATION_TOPLEFT);
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the blur settings
|
||||
*/
|
||||
protected function blur(Image $image, array $options): Image
|
||||
{
|
||||
if ($options['blur'] !== false) {
|
||||
$image->blurImage(0.0, $options['blur']);
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep animated gifs
|
||||
*/
|
||||
protected function coalesce(Image $image): Image
|
||||
{
|
||||
if ($image->getImageMimeType() === 'image/gif') {
|
||||
return $image->coalesceImages();
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns additional default parameters for imagemagick
|
||||
*/
|
||||
protected function defaults(): array
|
||||
{
|
||||
return parent::defaults() + [
|
||||
'interlace' => false,
|
||||
'profiles' => ['icc', 'icm'],
|
||||
'threads' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct settings for grayscale images
|
||||
*/
|
||||
protected function grayscale(Image $image, array $options): Image
|
||||
{
|
||||
if ($options['grayscale'] === true) {
|
||||
$image->setImageColorspace(Image::COLORSPACE_GRAY);
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct settings for interlaced JPEGs if
|
||||
* activated via options
|
||||
*/
|
||||
protected function interlace(Image $image, array $options): Image
|
||||
{
|
||||
if ($options['interlace'] === true) {
|
||||
$image->setInterlaceScheme(Image::INTERLACE_LINE);
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and runs the full imagemagick command
|
||||
* to process the image
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function process(string $file, array $options = []): array
|
||||
{
|
||||
$options = $this->preprocess($file, $options);
|
||||
|
||||
$image = new Image($file);
|
||||
$image = $this->threads($image, $options);
|
||||
$image = $this->interlace($image, $options);
|
||||
$image = $this->coalesce($image);
|
||||
$image = $this->grayscale($image, $options);
|
||||
$image = $this->autoOrient($image);
|
||||
$image = $this->resize($image, $options);
|
||||
$image = $this->quality($image, $options);
|
||||
$image = $this->blur($image, $options);
|
||||
$image = $this->sharpen($image, $options);
|
||||
$image = $this->strip($image, $options);
|
||||
|
||||
if ($this->save($image, $file, $options) === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new Exception(message: 'The imagemagick result could not be generated');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct JPEG compression quality settings
|
||||
*/
|
||||
protected function quality(Image $image, array $options): Image
|
||||
{
|
||||
$image->setImageCompressionQuality($options['quality']);
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the correct options to crop or resize the image
|
||||
* and translates the crop positions for imagemagick
|
||||
*/
|
||||
protected function resize(Image $image, array $options): Image
|
||||
{
|
||||
// simple resize
|
||||
if ($options['crop'] === false) {
|
||||
$image->thumbnailImage(
|
||||
$options['width'],
|
||||
$options['height'],
|
||||
true
|
||||
);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
// crop based on focus point
|
||||
if (Focus::isFocalPoint($options['crop']) === true) {
|
||||
if ($focus = Focus::coords(
|
||||
$options['crop'],
|
||||
$options['sourceWidth'],
|
||||
$options['sourceHeight'],
|
||||
$options['width'],
|
||||
$options['height']
|
||||
)) {
|
||||
$image->cropImage(
|
||||
$focus['width'],
|
||||
$focus['height'],
|
||||
$focus['x1'],
|
||||
$focus['y1']
|
||||
);
|
||||
|
||||
$image->thumbnailImage(
|
||||
$options['width'],
|
||||
$options['height'],
|
||||
true
|
||||
);
|
||||
|
||||
return $image;
|
||||
}
|
||||
}
|
||||
|
||||
// translate the gravity option into something imagemagick understands
|
||||
$gravity = match ($options['crop'] ?? null) {
|
||||
'top left' => Image::GRAVITY_NORTHWEST,
|
||||
'top' => Image::GRAVITY_NORTH,
|
||||
'top right' => Image::GRAVITY_NORTHEAST,
|
||||
'left' => Image::GRAVITY_WEST,
|
||||
'right' => Image::GRAVITY_EAST,
|
||||
'bottom left' => Image::GRAVITY_SOUTHWEST,
|
||||
'bottom' => Image::GRAVITY_SOUTH,
|
||||
'bottom right' => Image::GRAVITY_SOUTHEAST,
|
||||
default => Image::GRAVITY_CENTER
|
||||
};
|
||||
|
||||
$landscape = $options['width'] >= $options['height'];
|
||||
|
||||
$image->thumbnailImage(
|
||||
$landscape ? $options['width'] : $image->getImageWidth(),
|
||||
$landscape ? $image->getImageHeight() : $options['height'],
|
||||
true
|
||||
);
|
||||
|
||||
$image->setGravity($gravity);
|
||||
$image->cropImage($options['width'], $options['height'], 0, 0);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the option for the output file
|
||||
*/
|
||||
protected function save(Image $image, string $file, array $options): bool
|
||||
{
|
||||
if ($options['format'] !== null) {
|
||||
$file = pathinfo($file, PATHINFO_DIRNAME) . '/' . pathinfo($file, PATHINFO_FILENAME) . '.' . $options['format'];
|
||||
}
|
||||
|
||||
return $image->writeImages($file, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies sharpening if activated in the options.
|
||||
*/
|
||||
protected function sharpen(Image $image, array $options): Image
|
||||
{
|
||||
if (is_int($options['sharpen']) === false) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
$amount = max(1, min(100, $options['sharpen'])) / 100;
|
||||
$image->sharpenImage(0.0, $amount);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all metadata but ICC profiles from the image
|
||||
*/
|
||||
protected function strip(Image $image, array $options): Image
|
||||
{
|
||||
// strip all profiles but the ICC profile
|
||||
$profiles = $image->getImageProfiles('*', false);
|
||||
|
||||
foreach ($profiles as $profile) {
|
||||
if (in_array($profile, $options['profiles'] ?? [], true) === false) {
|
||||
$image->removeImageProfile($profile);
|
||||
}
|
||||
}
|
||||
|
||||
// strip all properties
|
||||
$properties = $image->getImageProperties('*', false);
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$image->deleteImageProperty($property);
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets thread limit
|
||||
*/
|
||||
protected function threads(Image $image, array $options): Image
|
||||
{
|
||||
$image->setResourceLimit(
|
||||
Image::RESOURCETYPE_THREAD,
|
||||
$options['threads']
|
||||
);
|
||||
return $image;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue