Permalink
Browse files

Merge pull request #130 from flyimg/im-geometry-refactor

Image Processor geometry refactor, added more tests.
  • Loading branch information...
baamenabar committed Sep 18, 2017
2 parents 481192b + 9e4c19f commit b1b9dfd35c4d53745ec9e4a34b2f90388db9fc7a
@@ -4,7 +4,7 @@
use Core\Entity\OptionsBag;
use Core\Exception\ReadFileException;
use Core\Entity\Image\ImageMetaInfo;
use Core\Entity\ImageMetaInfo;
class InputImage
{
@@ -137,4 +137,9 @@ public function getSourceImageMimeType(): string
$this->sourceImageMimeType = $this->sourceImageInfo->getMimeType();
return $this->sourceImageMimeType;
}
public function getSourceImageInfo()
{
return $this->sourceImageInfo;
}
}
@@ -1,6 +1,6 @@
<?php
namespace Core\Entity\Image;
namespace Core\Entity;
use Core\Exception\ExecFailedException;
@@ -11,6 +11,12 @@ class OptionsBag
/** @var array */
protected $parsedOptions;
/**
* @var array (associative)
* This options list will keep a copy of the parsed options even if an option get's removed by remove.
*/
protected $optionsCollection;
/**
* OptionsBag constructor.
*
@@ -21,6 +27,7 @@ public function __construct(AppParameters $appParameters, string $options)
{
$this->appParameters = $appParameters;
$this->parsedOptions = $this->parseOptions($options);
$this->optionsCollection = $this->parsedOptions;
}
/**
@@ -90,4 +97,31 @@ public function asArray(): array
{
return $this->parsedOptions;
}
/**
* Returns a parameter by name.
* These options will not be removed by the extract method.
*
* @param string $key The key
*
* @return mixed
*/
public function getOption($key)
{
return array_key_exists($key, $this->optionsCollection) ? $this->optionsCollection[$key] : '';
}
/**
* Update a parameter by name.
* These options will not update the main options list.
*
* @param string $key
* @param string $value
* @return OptionsBag
*/
public function setOption(string $key, string $value)
{
$this->optionsCollection[$key] = $value;
return $this;
}
}
@@ -3,24 +3,40 @@
namespace Core\Processor;
use Core\Entity\Image\OutputImage;
use Core\Entity\ImageMetaInfo;
/**
* Class ImageProcessor
* @package Core\Service
* Is the above line still valid??
*
* In this class we separate requests in 3 types
* - Simple resize geometry, resolved with -thumbnail
* -- Only width or height
* -- Only width and height
* - Cropping
*
* - Advanced requests
*/
class ImageProcessor extends Processor
{
/**
* This holds the width and height dimensions in pixels of the source image
* @var array
* Basic source image info parsed from IM identify command
* @var ImageMetaInfo
*/
protected $sourceDimensions = [];
protected $sourceImageInfo;
/**
* Basic source image info parsed from IM identify command
* OptionsBag from the request
* @var Core\Entity\OptionsBag
*/
protected $options;
/**
* stores/caches image related data like dimensions
* @var array
*/
protected $sourceInfo = [];
protected $geometry;
/**
* Save new FileName based on source file and list of options
@@ -32,6 +48,8 @@ class ImageProcessor extends Processor
*/
public function processNewImage(OutputImage $outputImage): OutputImage
{
$this->sourceImageInfo = $outputImage->getInputImage()->getSourceImageInfo();
$this->options = $outputImage->getInputImage()->getOptionsBag();
$this->generateCmdString($outputImage);
$this->execute($outputImage->getCommandString());
@@ -45,53 +63,99 @@ public function processNewImage(OutputImage $outputImage): OutputImage
*/
public function generateCmdString(OutputImage $outputImage)
{
$strip = $outputImage->extract('strip');
$thread = $outputImage->extract('thread');
$resize = $outputImage->extract('resize');
$frame = $outputImage->extract('gif-frame');
list($size, $extent, $gravity) = $this->generateSize($outputImage);
// we will categorize the operation in this method and call the adecuate functions depending on the parameters
// first we get the data we need
$width = $this->options->getOption('width');
$height = $this->options->getOption('height');
$crop = $this->options->getOption('crop');
// we default to thumbnail
$resizeOperator = $resize ? 'resize' : 'thumbnail';
$command = [];
$command[] = self::IM_CONVERT_COMMAND;
$tmpFileName = $outputImage->getInputImage()->getSourceImagePath();
//Check the source image is gif
// if width AND height AND crop are defined we need check further to define the type of operation we will do
if ($width && $height && $crop) {
$size = $this->generateCropSize();
} elseif ($width || $height) {
$size = $this->generateSimpleSize();
}
if ($outputImage->isInputGif()) {
$command[] = '-coalesce';
if ($outputImage->getOutputImageExtension() != OutputImage::EXT_GIF) {
$tmpFileName .= '['.escapeshellarg($frame).']';
}
}
$command[] = " " . $tmpFileName;
$command[] = ' -' . $resizeOperator . ' ' .
$size . $gravity . $extent .
' -colorspace sRGB';
foreach ($outputImage->getInputImage()->getOptionsBag() as $key => $value) {
if (!empty($value) && !in_array($key, self::EXCLUDED_IM_OPTIONS)) {
$command[] = "-{$key} ".escapeshellarg($value);
}
}
$command[] = $this->getSourceImagePath($outputImage);
$command[] = $size;
$command[] = ' -colorspace sRGB';
// strip is added internally by ImageMagick when using -thumbnail
$strip = $outputImage->extract('strip');
if (!empty($strip)) {
$command[] = "-strip ";
$command[] = "-strip";
}
$thread = $outputImage->extract('thread');
if (!empty($thread)) {
$command[] = "-limit thread ".escapeshellarg($thread);
}
$command = $this->applyQuality($outputImage, $command);
$command[] = $this->calculateQuality($outputImage);
$commandStr = implode(' ', $command);
$outputImage->setCommandString($commandStr);
}
/**
* IF we crop we need to know if the source image is bigger or smaller than the target size.
* @return string command section for the resizing.
*
* note: The shorthand version of resize to fill space will always fill the space even if image is bigger
*/
protected function generateCropSize(): string
{
$this->updateTargetDimensions();
$command = [];
$command[] = $this->getResizeOperator();
$command[] = $this->getDimensions() . '^';
$command[] = '-gravity ' . $this->options->getOption('gravity');
$command[] = '-extent ' . $this->getDimensions();
return implode(' ', $command);
}
/**
* IF we simply resize we let IM deal with the calculations
* @return string command section for the resizing.
*/
protected function generateSimpleSize(): string
{
$command = [];
$command[] = $this->getResizeOperator();
$command[] = $this->getDimensions() .
($this->options->getOption('preserve-natural-size') ? escapeshellarg('>') : '');
return implode(' ', $command);
}
/**
* Gets the source image path and adds any extra modifiers to the string
* @param OutputImage $outputImage
* @return string Path of the source file to be used in the conversion command
*/
protected function getSourceImagePath(OutputImage $outputImage): string
{
$tmpFileName = $this->sourceImageInfo->getPath();
//Check the source image is gif
if ($outputImage->isInputGif()) {
$frame = $this->options->getOption('gif-frame');
// set the frame if the output image is not gif (to get ony one frame)
if ($outputImage->getOutputImageExtension() !== OutputImage::EXT_GIF) {
$tmpFileName .= '['.escapeshellarg($frame).']';
}
}
return $tmpFileName;
}
/**
* Apply the Quality processor based on options
*
@@ -100,27 +164,89 @@ public function generateCmdString(OutputImage $outputImage)
*
* @return array
*/
protected function applyQuality(OutputImage $outputImage, array $command): array
protected function calculateQuality(OutputImage $outputImage): string
{
$quality = $outputImage->extract('quality');
$parameter = '';
/** WebP format */
if (is_executable(self::CWEBP_COMMAND) && $outputImage->isOutputWebP()) {
$lossLess = $outputImage->extract('webp-lossless') ? 'true' : 'false';
$command[] = "-quality ".escapeshellarg($quality).
$parameter = "-quality ".escapeshellarg($quality).
" -define webp:lossless=".$lossLess." ".escapeshellarg($outputImage->getOutputImagePath());
} /** MozJpeg compression */
elseif (is_executable(self::MOZJPEG_COMMAND) && $outputImage->isOutputMozJpeg()) {
$command[] = "TGA:- | ".escapeshellarg(self::MOZJPEG_COMMAND)
$parameter = "TGA:- | ".escapeshellarg(self::MOZJPEG_COMMAND)
." -quality ".escapeshellarg($quality)
." -outfile ".escapeshellarg($outputImage->getOutputImagePath())
." -targa";
} /** default ImageMagick compression */
else {
$command[] = "-quality ".escapeshellarg($quality).
$parameter = "-quality ".escapeshellarg($quality).
" ".escapeshellarg($outputImage->getOutputImagePath());
}
return $command;
return $parameter;
}
/**
* This works as a cache for calculations
* @param string $key the key with wich we store a calculated value
* @param func $calculate function that returns a calculated value
* @return string|mixed
*/
protected function getGeometry($key, $calculate):string
{
if (isset($this->geometry[$key])) {
return $this->geometry[$key];
}
$this->geometry[$key] = call_user_func($calculate);
return $this->geometry[$key];
}
protected function getDimensions():string
{
return $this->getGeometry('dimensions', function () {
$targetWidth = $this->options->getOption('width');
$targetHeight = $this->options->getOption('height');
$dimensions = '';
if ($targetWidth) {
$dimensions .= (string)escapeshellarg($targetWidth);
}
if ($targetHeight) {
$dimensions .= (string)'x'.escapeshellarg($targetHeight);
}
return $dimensions;
});
}
protected function getResizeOperator():string
{
return $this->getGeometry('resizeOperator', function () {
return $this->options->getOption('resize') ? '-resize' : '-thumbnail';
});
}
protected function updateTargetDimensions()
{
if (!$this->options->getOption('preserve-natural-size')) {
return;
}
$targetWidth = $this->options->getOption('width');
$targetHeight = $this->options->getOption('height');
$originalWidth = $this->sourceImageInfo->getDimensions()['width'];
$originalHeight = $this->sourceImageInfo->getDimensions()['height'];
if ($originalWidth < $targetWidth) {
$this->options->setOption('width', $originalWidth);
}
if ($originalHeight < $targetHeight) {
$this->options->setOption('height', $originalHeight);
}
}
/**
@@ -1,8 +1,8 @@
<?php
namespace Tests\Core\Entity\Image;
namespace Tests\Core\Entity;
use Core\Entity\Image\ImageMetaInfo;
use Core\Entity\ImageMetaInfo;
use Core\Entity\Image\OutputImage;
use Core\Exception\ExecFailedException;
use Tests\Core\BaseTest;
@@ -31,7 +31,6 @@ public function mimeTypeDataProvider(): array
];
}
public function testGetMimeTypeCached()
{
$testImagePath = self::JPG_TEST_IMAGE;
Oops, something went wrong.

0 comments on commit b1b9dfd

Please sign in to comment.