Initial commit

This commit is contained in:
isUnknown 2025-10-03 07:46:23 +02:00
commit efa5624dab
687 changed files with 162710 additions and 0 deletions

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

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

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