From 7b89554c0cac8326d8bbf45e9e61d79db6efccfe Mon Sep 17 00:00:00 2001 From: Rob Frawley 2nd Date: Wed, 6 Sep 2017 20:04:32 -0400 Subject: [PATCH] major post-processors rewrite - refactor post-processors for easier testing and to implement in a more sane manner - add complete post-processor tests - enhance post-processor docs - deprecate all post-processor setter methods - deprecate/add/change some post-processor options - enable passing configuration to process builder on a per-post-processor basis - fix stdin tests and static method calls in closures - fix short array syntax to long array syntax for php 5.3 support - drop hhvm --- .travis.yml | 1 + .../PostProcessor/InvalidOptionException.php | 65 +++++ Imagine/Filter/FilterManager.php | 14 +- .../PostProcessor/AbstractPostProcessor.php | 253 ++++++++++++++++++ .../ConfigurablePostProcessorInterface.php | 8 +- .../PostProcessor/JpegOptimPostProcessor.php | 154 +++++------ .../PostProcessor/MozJpegPostProcessor.php | 88 +++--- .../PostProcessor/OptiPngPostProcessor.php | 131 +++++---- .../PostProcessor/PngquantPostProcessor.php | 118 +++++--- .../PostProcessor/PostProcessorInterface.php | 12 +- Resources/doc/post-processors/png-opti.rst | 33 ++- Resources/doc/post-processors/png-quant.rst | 16 +- .../InvalidOptionExceptionTest.php | 50 ++++ .../bin/post-process-as-file-error.bash | 5 + Tests/Fixtures/bin/post-process-as-file.bash | 17 ++ .../bin/post-process-as-stdin-error.bash | 5 + Tests/Fixtures/bin/post-process-as-stdin.bash | 21 ++ Tests/Fixtures/bin/post-process-common.bash | 51 ++++ Tests/Imagine/Filter/FilterManagerTest.php | 2 +- .../AbstractPostProcessorTest.php | 218 +++++++++++++++ .../AbstractPostProcessorTestCase.php | 140 ++++++++++ .../JpegOptimPostProcessorTest.php | 192 +++++++++++++ .../MozJpegPostProcessorTest.php | 141 ++++++++++ .../OptiPngPostProcessorTest.php | 192 +++++++++++++ .../PngquantPostProcessorTest.php | 205 ++++++++++++++ 25 files changed, 1890 insertions(+), 242 deletions(-) create mode 100644 Exception/Imagine/Filter/PostProcessor/InvalidOptionException.php create mode 100644 Imagine/Filter/PostProcessor/AbstractPostProcessor.php create mode 100644 Tests/Exception/Imagine/Filter/PostProcessor/InvalidOptionExceptionTest.php create mode 100755 Tests/Fixtures/bin/post-process-as-file-error.bash create mode 100755 Tests/Fixtures/bin/post-process-as-file.bash create mode 100755 Tests/Fixtures/bin/post-process-as-stdin-error.bash create mode 100755 Tests/Fixtures/bin/post-process-as-stdin.bash create mode 100755 Tests/Fixtures/bin/post-process-common.bash create mode 100644 Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTest.php create mode 100644 Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTestCase.php create mode 100644 Tests/Imagine/Filter/PostProcessor/JpegOptimPostProcessorTest.php create mode 100644 Tests/Imagine/Filter/PostProcessor/MozJpegPostProcessorTest.php create mode 100644 Tests/Imagine/Filter/PostProcessor/OptiPngPostProcessorTest.php create mode 100644 Tests/Imagine/Filter/PostProcessor/PngquantPostProcessorTest.php diff --git a/.travis.yml b/.travis.yml index f52ea50ba..dc0b07509 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,7 @@ matrix: allow_failures: - php: 7.1 env: SYMFONY_VERSION=dev-master + - php: hhvm before_install: - if [ "${TRAVIS_PHP_VERSION}" != "hhvm" ]; then echo "memory_limit = -1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi; diff --git a/Exception/Imagine/Filter/PostProcessor/InvalidOptionException.php b/Exception/Imagine/Filter/PostProcessor/InvalidOptionException.php new file mode 100644 index 000000000..7201ab1d2 --- /dev/null +++ b/Exception/Imagine/Filter/PostProcessor/InvalidOptionException.php @@ -0,0 +1,65 @@ +stringifyOptions($options))); + } + + /** + * @param array $options + * + * @return string + */ + private function stringifyOptions(array $options = array()) + { + if (count($options) === 0) { + return '[]'; + } + + $options = array_map(array($this, 'stringifyOptionValue'), $options); + + array_walk($options, function (&$o, $name) { + $o = sprintf('%s="%s"', $name, $o); + }); + + return sprintf('[%s]', implode(', ', $options)); + } + + /** + * @param mixed $value + * + * @return string + */ + private function stringifyOptionValue($value) + { + if (is_array($value)) { + return json_encode($value); + } + + if (is_scalar($value)) { + return $value; + } + + return str_replace("\n", '', var_export($value, true)); + } +} diff --git a/Imagine/Filter/FilterManager.php b/Imagine/Filter/FilterManager.php index a7767c88e..2d4482739 100644 --- a/Imagine/Filter/FilterManager.php +++ b/Imagine/Filter/FilterManager.php @@ -98,7 +98,7 @@ public function getFilterConfiguration() * * @throws \InvalidArgumentException * - * @return Binary + * @return BinaryInterface */ public function apply(BinaryInterface $binary, array $config) { @@ -176,17 +176,13 @@ public function apply(BinaryInterface $binary, array $config) public function applyPostProcessors(BinaryInterface $binary, $config) { $config += array('post_processors' => array()); + foreach ($config['post_processors'] as $postProcessorName => $postProcessorOptions) { if (!isset($this->postProcessors[$postProcessorName])) { - throw new \InvalidArgumentException(sprintf( - 'Could not find post processor "%s"', $postProcessorName - )); - } - if ($this->postProcessors[$postProcessorName] instanceof ConfigurablePostProcessorInterface) { - $binary = $this->postProcessors[$postProcessorName]->processWithConfiguration($binary, $postProcessorOptions); - } else { - $binary = $this->postProcessors[$postProcessorName]->process($binary); + throw new \InvalidArgumentException(sprintf('Post-processor "%s" could not be found', $postProcessorName)); } + + $binary = $this->postProcessors[$postProcessorName]->process($binary, $postProcessorOptions); } return $binary; diff --git a/Imagine/Filter/PostProcessor/AbstractPostProcessor.php b/Imagine/Filter/PostProcessor/AbstractPostProcessor.php new file mode 100644 index 000000000..92dec18bc --- /dev/null +++ b/Imagine/Filter/PostProcessor/AbstractPostProcessor.php @@ -0,0 +1,253 @@ +executablePath = $executablePath; + $this->temporaryRootPath = $temporaryRootPath; + $this->filesystem = new Filesystem(); + } + + /** + * Performs post-process operation on passed binary and returns the resulting binary. + * + * @param BinaryInterface $binary + * @param array $options + * + * @throws ProcessFailedException + * + * @return BinaryInterface + */ + public function process(BinaryInterface $binary /* , array $options = array() */) + { + if (func_num_args() < 2) { + @trigger_error(sprintf( + 'Calling the %s::%s() method without a second parameter of options was deprecated in 1.10.0 and '. + 'will be removed in 2.0.', get_called_class(), __FUNCTION__ + ), E_USER_DEPRECATED); + } + + return $this->doProcess($binary, func_num_args() >= 2 ? func_get_arg(1) : array()); + } + + /** + * Performs post-process operation on passed binary and returns the resulting binary. + * + * @deprecated This method was deprecated in 1.10.0 and will be removed in 2.0. Use PostProcessorInterface::process() + * instead. + * + * @param BinaryInterface $binary + * @param array $options + * + * @throws ProcessFailedException + * + * @return BinaryInterface + */ + public function processWithConfiguration(BinaryInterface $binary, array $options) + { + @trigger_error(sprintf( + 'The %s::%s() method was deprecated in 1.10.0 and will be removed in 2.0. Use the %s::process() '. + 'method instead.', get_called_class(), __FUNCTION__, get_called_class() + ), E_USER_DEPRECATED); + + return $this->doProcess($binary, $options); + } + + /** + * @param BinaryInterface $binary + * @param array $options + * + * @throws ProcessFailedException + * + * @return BinaryInterface + */ + abstract protected function doProcess(BinaryInterface $binary, array $options); + + /** + * @param array $arguments + * @param array $options + * + * @return ProcessBuilder + */ + protected function createProcessBuilder(array $arguments = array(), array $options = array()) + { + $builder = new ProcessBuilder($arguments); + + if (!isset($options['process'])) { + return $builder; + } + + if (isset($options['process']['timeout'])) { + $builder->setTimeout($options['process']['timeout']); + } + + if (isset($options['process']['prefix'])) { + $builder->setPrefix($options['process']['prefix']); + } + + if (isset($options['process']['working_directory'])) { + $builder->setWorkingDirectory($options['process']['working_directory']); + } + + if (isset($options['process']['environment_variables']) && is_array($options['process']['environment_variables'])) { + foreach ($options['process']['environment_variables'] as $n => $v) { + $builder->setEnv($n, $v); + } + } + + if (isset($options['process']['options']) && is_array($options['process']['options'])) { + foreach ($options['process']['options'] as $n => $v) { + $builder->setOption($n, $v); + } + } + + return $builder; + } + + /** + * @param BinaryInterface $binary + * + * @return bool + */ + protected function isBinaryTypeJpgImage(BinaryInterface $binary) + { + return $this->isBinaryTypeMatch($binary, array('image/jpeg', 'image/jpg')); + } + + /** + * @param BinaryInterface $binary + * + * @return bool + */ + protected function isBinaryTypePngImage(BinaryInterface $binary) + { + return $this->isBinaryTypeMatch($binary, array('image/png')); + } + + /** + * @param BinaryInterface $binary + * @param string[] $types + * + * @return bool + */ + protected function isBinaryTypeMatch(BinaryInterface $binary, array $types) + { + return in_array($binary->getMimeType(), $types); + } + + /** + * @param BinaryInterface $binary + * @param array $options + * @param null $prefix + * + * @return string + */ + protected function writeTemporaryFile(BinaryInterface $binary, array $options = array(), $prefix = null) + { + $temporary = $this->acquireTemporaryFilePath($options, $prefix); + + if ($binary instanceof FileBinaryInterface) { + $this->filesystem->copy($binary->getPath(), $temporary, true); + } else { + $this->filesystem->dumpFile($temporary, $binary->getContent()); + } + + return $temporary; + } + + /** + * @param array $options + * @param string $prefix + * + * @return string + */ + protected function acquireTemporaryFilePath(array $options, $prefix = null) + { + $root = isset($options['temp_dir']) ? $options['temp_dir'] : ($this->temporaryRootPath ?: sys_get_temp_dir()); + + if (!is_dir($root)) { + try { + $this->filesystem->mkdir($root); + } catch (IOException $exception) { + // ignore failure as "tempnam" function will revert back to system default tmp path as last resort + } + } + + if (false === $file = @tempnam($root, $prefix ?: 'post-processor')) { + throw new \RuntimeException(sprintf('Temporary file cannot be created in "%s"', $root)); + } + + return $file; + } + + /** + * @param Process $process + * @param array $validReturns + * @param array $errorStrings + * + * @return bool + */ + protected function isSuccessfulProcess(Process $process, array $validReturns = array(0), array $errorStrings = array('ERROR')) + { + if (count($validReturns) > 0 && !in_array($process->getExitCode(), $validReturns)) { + return false; + } + + foreach ($errorStrings as $string) { + if (false !== strpos($process->getOutput(), $string)) { + return false; + } + } + + return true; + } + + /** + * @param string $method + */ + protected function triggerSetterMethodDeprecation($method) + { + @trigger_error(sprintf('The %s() method was deprecated in 1.10.0 and will be removed in 2.0. You must ' + .'setup the class state via its __construct() method. You can still pass filter-specific options to the '. + 'process() method to overwrite behavior.', $method), E_USER_DEPRECATED); + } +} diff --git a/Imagine/Filter/PostProcessor/ConfigurablePostProcessorInterface.php b/Imagine/Filter/PostProcessor/ConfigurablePostProcessorInterface.php index 3214ec4b8..e667c40e1 100644 --- a/Imagine/Filter/PostProcessor/ConfigurablePostProcessorInterface.php +++ b/Imagine/Filter/PostProcessor/ConfigurablePostProcessorInterface.php @@ -14,16 +14,16 @@ use Liip\ImagineBundle\Binary\BinaryInterface; /** - * Interface to make PostProcessors configurable without breaking BC. - * - * @see PostProcessorInterface for the original interface + * @deprecated This interface was deprecated in 1.10.0 and will be removed in 2.0. Use PostProcessorInterface::process(). * * @author Alex Wilson */ interface ConfigurablePostProcessorInterface { /** - * Allows processing a BinaryInterface, with run-time options, so PostProcessors remain stateless. + * Performs post-process operation on passed binary and returns the resulting binary. + * + * @deprecated This interface was deprecated in 1.10.0 and will be removed in 2.0. Use PostProcessorInterface::process(). * * @param BinaryInterface $binary * @param array $options Operation-specific options diff --git a/Imagine/Filter/PostProcessor/JpegOptimPostProcessor.php b/Imagine/Filter/PostProcessor/JpegOptimPostProcessor.php index fd97213d4..a1752953a 100644 --- a/Imagine/Filter/PostProcessor/JpegOptimPostProcessor.php +++ b/Imagine/Filter/PostProcessor/JpegOptimPostProcessor.php @@ -13,28 +13,26 @@ use Liip\ImagineBundle\Binary\BinaryInterface; use Liip\ImagineBundle\Binary\FileBinaryInterface; +use Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException; use Liip\ImagineBundle\Model\Binary; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\ProcessBuilder; -class JpegOptimPostProcessor implements PostProcessorInterface, ConfigurablePostProcessorInterface +class JpegOptimPostProcessor extends AbstractPostProcessor { - /** @var string Path to jpegoptim binary */ - protected $jpegoptimBin; - /** * If set --strip-all will be passed to jpegoptim. * * @var bool */ - protected $stripAll; + protected $strip; /** * If set, --max=$value will be passed to jpegoptim. * * @var int */ - protected $max; + protected $quality; /** * If set to true --all-progressive will be passed to jpegoptim, otherwise --all-normal will be passed. @@ -48,137 +46,141 @@ class JpegOptimPostProcessor implements PostProcessorInterface, ConfigurablePost * * @var string */ - protected $tempDir; + protected $temporaryRootPath; /** - * Constructor. - * - * @param string $jpegoptimBin Path to the jpegoptim binary - * @param bool $stripAll Strip all markers from output - * @param int $max Set maximum image quality factor - * @param bool $progressive Force output to be progressive - * @param string $tempDir Directory where temporary file will be written + * @param string $executablePath Path to the jpegoptim binary + * @param bool $strip Strip all markers from output + * @param int $quality Set maximum image quality factor + * @param bool $progressive Force output to be progressive + * @param string $temporaryRootPath Directory where temporary file will be written */ - public function __construct( - $jpegoptimBin = '/usr/bin/jpegoptim', - $stripAll = true, - $max = null, - $progressive = true, - $tempDir = '' - ) { - $this->jpegoptimBin = $jpegoptimBin; - $this->stripAll = $stripAll; - $this->max = $max; + public function __construct($executablePath = '/usr/bin/jpegoptim', $strip = true, $quality = null, $progressive = true, $temporaryRootPath = null) + { + parent::__construct($executablePath, $temporaryRootPath); + + $this->strip = $strip; + $this->quality = $quality; $this->progressive = $progressive; - $this->tempDir = $tempDir ?: sys_get_temp_dir(); } /** - * @param int $max + * @deprecated All post-processor setters have been deprecated in 1.10.0 for removal in 2.0. You must only use the + * class's constructor to set the property state. + * + * @param int $maxQuality * * @return JpegOptimPostProcessor */ - public function setMax($max) + public function setMax($maxQuality) { - $this->max = $max; + $this->triggerSetterMethodDeprecation(__METHOD__); + $this->quality = $maxQuality; return $this; } /** + * @deprecated All post-processor setters have been deprecated in 1.10.0 for removal in 2.0. You must only use the + * class's constructor to set the property state. + * * @param bool $progressive * * @return JpegOptimPostProcessor */ public function setProgressive($progressive) { + $this->triggerSetterMethodDeprecation(__METHOD__); $this->progressive = $progressive; return $this; } /** - * @param bool $stripAll + * @deprecated All post-processor setters have been deprecated in 1.10.0 for removal in 2.0. You must only use the + * class's constructor to set the property state. + * + * @param bool $strip * * @return JpegOptimPostProcessor */ - public function setStripAll($stripAll) + public function setStripAll($strip) { - $this->stripAll = $stripAll; + $this->triggerSetterMethodDeprecation(__METHOD__); + $this->strip = $strip; return $this; } /** * @param BinaryInterface $binary + * @param array $options * * @throws ProcessFailedException * * @return BinaryInterface */ - public function process(BinaryInterface $binary) + protected function doProcess(BinaryInterface $binary, array $options = array()) { - return $this->processWithConfiguration($binary, array()); + if (!$this->isBinaryTypeJpgImage($binary)) { + return $binary; + } + + $file = $this->writeTemporaryFile($binary, $options, 'imagine-post-processor-optipng'); + + $process = $this->setupProcessBuilder($options)->add($file)->getProcess(); + $process->run(); + + if (!$this->isSuccessfulProcess($process)) { + unlink($file); + throw new ProcessFailedException($process); + } + + $result = new Binary(file_get_contents($file), $binary->getMimeType(), $binary->getFormat()); + + unlink($file); + + return $result; } /** - * @param BinaryInterface $binary - * @param array $options - * - * @throws ProcessFailedException + * @param array $options * - * @return BinaryInterface + * @return ProcessBuilder */ - public function processWithConfiguration(BinaryInterface $binary, array $options) + private function setupProcessBuilder(array $options = array()) { - $type = strtolower($binary->getMimeType()); - if (!in_array($type, array('image/jpeg', 'image/jpg'))) { - return $binary; - } + $builder = $this->createProcessBuilder(array($this->executablePath), $options); - $tempDir = array_key_exists('temp_dir', $options) ? $options['temp_dir'] : $this->tempDir; - if (false === $input = tempnam($tempDir, 'imagine_jpegoptim')) { - throw new \RuntimeException(sprintf('Temp file can not be created in "%s".', $tempDir)); + if (isset($options['strip_all']) ? $options['strip_all'] : $this->strip) { + $builder->add('--strip-all'); } - $pb = new ProcessBuilder(array($this->jpegoptimBin)); + if (isset($options['max'])) { + @trigger_error(sprintf('the "max" option was deprecated in 1.10.0 and will be removed in 2.0. '. + 'Instead, use the "quality" option.'), E_USER_DEPRECATED); - $stripAll = array_key_exists('strip_all', $options) ? $options['strip_all'] : $this->stripAll; - if ($stripAll) { - $pb->add('--strip-all'); - } + if (isset($options['quality'])) { + throw new InvalidOptionException('the "max" and "quality" options cannot both be set', $options); + } - $max = array_key_exists('max', $options) ? $options['max'] : $this->max; - if ($max) { - $pb->add('--max='.$max); + $options['quality'] = $options['max']; } - $progressive = array_key_exists('progressive', $options) ? $options['progressive'] : $this->progressive; - if ($progressive) { - $pb->add('--all-progressive'); - } else { - $pb->add('--all-normal'); - } + if ($quality = isset($options['quality']) ? $options['quality'] : $this->quality) { + if (!in_array($options['quality'], range(0, 100))) { + throw new InvalidOptionException('the "quality" option must be an int between 0 and 100', $options); + } - $pb->add($input); - if ($binary instanceof FileBinaryInterface) { - copy($binary->getPath(), $input); - } else { - file_put_contents($input, $binary->getContent()); + $builder->add(sprintf('--max=%d', $quality)); } - $proc = $pb->getProcess(); - $proc->run(); - - if (false !== strpos($proc->getOutput(), 'ERROR') || 0 !== $proc->getExitCode()) { - unlink($input); - throw new ProcessFailedException($proc); + if (isset($options['progressive']) ? $options['progressive'] : $this->progressive) { + $builder->add('--all-progressive'); + } else { + $builder->add('--all-normal'); } - $result = new Binary(file_get_contents($input), $binary->getMimeType(), $binary->getFormat()); - - unlink($input); - - return $result; + return $builder; } } diff --git a/Imagine/Filter/PostProcessor/MozJpegPostProcessor.php b/Imagine/Filter/PostProcessor/MozJpegPostProcessor.php index 3926f8b88..017246af1 100644 --- a/Imagine/Filter/PostProcessor/MozJpegPostProcessor.php +++ b/Imagine/Filter/PostProcessor/MozJpegPostProcessor.php @@ -24,52 +24,40 @@ * * @author Alex Wilson */ -class MozJpegPostProcessor implements PostProcessorInterface, ConfigurablePostProcessorInterface +class MozJpegPostProcessor extends AbstractPostProcessor { - /** @var string Path to the mozjpeg cjpeg binary */ - protected $mozjpegBin; - - /** @var null|int Quality factor */ + /** + * @var null|int Quality factor + */ protected $quality; /** - * Constructor. - * - * @param string $mozjpegBin Path to the mozjpeg cjpeg binary - * @param int|null $quality Quality factor + * @param string $executablePath Path to the mozjpeg cjpeg binary + * @param int|null $quality Quality factor */ - public function __construct( - $mozjpegBin = '/opt/mozjpeg/bin/cjpeg', - $quality = null - ) { - $this->mozjpegBin = $mozjpegBin; - $this->setQuality($quality); + public function __construct($executablePath = '/opt/mozjpeg/bin/cjpeg', $quality = null) + { + parent::__construct($executablePath); + + $this->quality = $quality; } /** + * @deprecated All post-processor setters have been deprecated in 1.10.0 for removal in 2.0. You must only use the + * class's constructor to set the property state. + * * @param int $quality * * @return MozJpegPostProcessor */ public function setQuality($quality) { + $this->triggerSetterMethodDeprecation(__METHOD__); $this->quality = $quality; return $this; } - /** - * @param BinaryInterface $binary - * - * @throws ProcessFailedException - * - * @return BinaryInterface - */ - public function process(BinaryInterface $binary) - { - return $this->processWithConfiguration($binary, array()); - } - /** * @param BinaryInterface $binary * @param array $options @@ -78,39 +66,43 @@ public function process(BinaryInterface $binary) * * @return BinaryInterface */ - public function processWithConfiguration(BinaryInterface $binary, array $options) + protected function doProcess(BinaryInterface $binary, array $options = array()) { - $type = strtolower($binary->getMimeType()); - if (!in_array($type, array('image/jpeg', 'image/jpg'))) { + if (!$this->isBinaryTypeJpgImage($binary)) { return $binary; } - $pb = new ProcessBuilder(array($this->mozjpegBin)); - - // Places emphasis on DC - $pb->add('-quant-table'); - $pb->add(2); + $process = $this->setupProcessBuilder($options, $binary)->setInput($binary->getContent())->getProcess(); + $process->run(); - $transformQuality = array_key_exists('quality', $options) ? $options['quality'] : $this->quality; - if ($transformQuality !== null) { - $pb->add('-quality'); - $pb->add($transformQuality); + if (!$this->isSuccessfulProcess($process)) { + throw new ProcessFailedException($process); } - $pb->add('-optimise'); + return new Binary($process->getOutput(), $binary->getMimeType(), $binary->getFormat()); + } - // Favor stdin/stdout so we don't waste time creating a new file. - $pb->setInput($binary->getContent()); + /** + * @param array $options + * + * @return ProcessBuilder + */ + private function setupProcessBuilder(array $options = array()) + { + $builder = $this->createProcessBuilder(array($this->executablePath), $options); - $proc = $pb->getProcess(); - $proc->run(); + if ($quantTable = isset($options['quant_table']) ? $options['quant_table'] : 2) { + $builder->add('-quant-table')->add($quantTable); + } - if (false !== strpos($proc->getOutput(), 'ERROR') || 0 !== $proc->getExitCode()) { - throw new ProcessFailedException($proc); + if (isset($options['optimise']) ? $options['optimise'] : true) { + $builder->add('-optimise'); } - $result = new Binary($proc->getOutput(), $binary->getMimeType(), $binary->getFormat()); + if (null !== $quality = isset($options['quality']) ? $options['quality'] : $this->quality) { + $builder->add('-quality')->add($quality); + } - return $result; + return $builder; } } diff --git a/Imagine/Filter/PostProcessor/OptiPngPostProcessor.php b/Imagine/Filter/PostProcessor/OptiPngPostProcessor.php index 80ee5187b..561e9a249 100644 --- a/Imagine/Filter/PostProcessor/OptiPngPostProcessor.php +++ b/Imagine/Filter/PostProcessor/OptiPngPostProcessor.php @@ -13,17 +13,13 @@ use Liip\ImagineBundle\Binary\BinaryInterface; use Liip\ImagineBundle\Binary\FileBinaryInterface; +use Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException; use Liip\ImagineBundle\Model\Binary; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\ProcessBuilder; -class OptiPngPostProcessor implements PostProcessorInterface, ConfigurablePostProcessorInterface +class OptiPngPostProcessor extends AbstractPostProcessor { - /** - * @var string Path to optipng binary - */ - protected $optipngBin; - /** * If set --oN will be passed to optipng. * @@ -36,95 +32,124 @@ class OptiPngPostProcessor implements PostProcessorInterface, ConfigurablePostPr * * @var bool */ - protected $stripAll; + protected $strip; /** * Directory where temporary file will be written. * * @var string */ - protected $tempDir; + protected $temporaryRootPath; /** - * Constructor. - * - * @param string $optipngBin Path to the optipng binary - * @param int $level Optimization level - * @param bool $stripAll Strip metadata objects - * @param string $tempDir Directory where temporary file will be written + * @param string $executablePath Path to the optipng binary + * @param int $level Optimization level + * @param bool $strip Strip metadata objects + * @param string $temporaryRootPath Directory where temporary file will be written */ - public function __construct($optipngBin = '/usr/bin/optipng', $level = 7, $stripAll = true, $tempDir = '') + public function __construct($executablePath = '/usr/bin/optipng', $level = 7, $strip = true, $temporaryRootPath = null) { - $this->optipngBin = $optipngBin; + parent::__construct($executablePath, $temporaryRootPath); + $this->level = $level; - $this->stripAll = $stripAll; - $this->tempDir = $tempDir ?: sys_get_temp_dir(); + $this->strip = $strip; } /** * @param BinaryInterface $binary + * @param array $options * * @throws ProcessFailedException * - * @return BinaryInterface + * @return BinaryInterface|Binary */ - public function process(BinaryInterface $binary) + protected function doProcess(BinaryInterface $binary, array $options = array()) { - return $this->processWithConfiguration($binary, array()); + if (!$this->isBinaryTypePngImage($binary)) { + return $binary; + } + + $file = $this->writeTemporaryFile($binary, $options, 'imagine-post-processor-optipng'); + + $process = $this->setupProcessBuilder($options)->add($file)->getProcess(); + $process->run(); + + if (!$this->isSuccessfulProcess($process)) { + unlink($file); + throw new ProcessFailedException($process); + } + + $result = new Binary(file_get_contents($file), $binary->getMimeType(), $binary->getFormat()); + + unlink($file); + + return $result; } /** - * @param BinaryInterface $binary - * @param array $options - * - * @throws ProcessFailedException + * @param array $options * - * @return BinaryInterface|Binary + * @return ProcessBuilder */ - public function processWithConfiguration(BinaryInterface $binary, array $options) + private function setupProcessBuilder(array $options = array()) { - $type = strtolower($binary->getMimeType()); - if (!in_array($type, array('image/png'))) { - return $binary; + $builder = $this->createProcessBuilder(array($this->executablePath), $options); + + if (null !== $level = isset($options['level']) ? $options['level'] : $this->level) { + if (!in_array($level, range(0, 7))) { + throw new InvalidOptionException('the "level" option must be an int between 0 and 7', $options); + } + + $builder->add(sprintf('-o%d', $level)); } - $tempDir = array_key_exists('temp_dir', $options) ? $options['temp_dir'] : $this->tempDir; - if (false === $input = tempnam($tempDir, 'imagine_optipng')) { - throw new \RuntimeException(sprintf('Temp file can not be created in "%s".', $tempDir)); + if (isset($options['strip_all'])) { + @trigger_error(sprintf('The "strip_all" option was deprecated in 1.10.0 and will be removed in 2.0. '. + 'Instead, use the "strip" option.'), E_USER_DEPRECATED); + + if (isset($options['strip'])) { + throw new InvalidOptionException('the "strip" and "strip_all" options cannot both be set', $options); + } + + $options['strip'] = $options['strip_all']; } - $pb = new ProcessBuilder(array($this->optipngBin)); + if ($strip = isset($options['strip']) ? $options['strip'] : $this->strip) { + $builder->add('-strip')->add(true === $strip ? 'all' : $strip); + } - $level = array_key_exists('level', $options) ? $options['level'] : $this->level; - if ($level !== null) { - $pb->add(sprintf('--o%d', $level)); + if (isset($options['snip']) && true === $options['snip']) { + $builder->add('-snip'); } - $stripAll = array_key_exists('strip_all', $options) ? $options['strip_all'] : $this->stripAll; - if ($stripAll) { - $pb->add('--strip=all'); + if (isset($options['preserve_attributes']) && true === $options['preserve_attributes']) { + $builder->add('-preserve'); } - $pb->add($input); + if (isset($options['interlace_type'])) { + if (!in_array($options['interlace_type'], range(0, 1))) { + throw new InvalidOptionException('the "interlace_type" option must be either 0 or 1', $options); + } - if ($binary instanceof FileBinaryInterface) { - copy($binary->getPath(), $input); - } else { - file_put_contents($input, $binary->getContent()); + $builder->add('-i')->add($options['interlace_type']); } - $proc = $pb->getProcess(); - $proc->run(); + if (isset($options['no_bit_depth_reductions']) && true === $options['no_bit_depth_reductions']) { + $builder->add('-nb'); + } - if (false !== strpos($proc->getOutput(), 'ERROR') || 0 !== $proc->getExitCode()) { - unlink($input); - throw new ProcessFailedException($proc); + if (isset($options['no_color_type_reductions']) && true === $options['no_color_type_reductions']) { + $builder->add('-nc'); } - $result = new Binary(file_get_contents($input), $binary->getMimeType(), $binary->getFormat()); + if (isset($options['no_palette_reductions']) && true === $options['no_palette_reductions']) { + $builder->add('-np'); + } - unlink($input); + if (isset($options['no_reductions']) && true === $options['no_reductions']) { + $builder->add('-nx'); + } - return $result; + return $builder; } } diff --git a/Imagine/Filter/PostProcessor/PngquantPostProcessor.php b/Imagine/Filter/PostProcessor/PngquantPostProcessor.php index 806c81475..f0d4e7728 100644 --- a/Imagine/Filter/PostProcessor/PngquantPostProcessor.php +++ b/Imagine/Filter/PostProcessor/PngquantPostProcessor.php @@ -12,6 +12,7 @@ namespace Liip\ImagineBundle\Imagine\Filter\PostProcessor; use Liip\ImagineBundle\Binary\BinaryInterface; +use Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException; use Liip\ImagineBundle\Model\Binary; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\ProcessBuilder; @@ -25,32 +26,40 @@ * * @author Alex Wilson */ -class PngquantPostProcessor implements PostProcessorInterface, ConfigurablePostProcessorInterface +class PngquantPostProcessor extends AbstractPostProcessor { - /** @var string Path to pngquant binary */ - protected $pngquantBin; + /** + * @var string Path to pngquant binary + */ + protected $executablePath; - /** @var string Quality to pass to pngquant */ + /** + * @var string Quality to pass to pngquant + */ protected $quality; /** - * Constructor. - * - * @param string $pngquantBin Path to the pngquant binary + * @param string $executablePath + * @param array $quality */ - public function __construct($pngquantBin = '/usr/bin/pngquant', $quality = '80-100') + public function __construct($executablePath = '/usr/bin/pngquant', $quality = array(80, 100)) { - $this->pngquantBin = $pngquantBin; - $this->setQuality($quality); + parent::__construct($executablePath); + + $this->quality = $quality; } /** + * @deprecated All post-processor setters have been deprecated in 1.10.0 for removal in 2.0. You must only use the + * class's constructor to set the property state. + * * @param string $quality * * @return PngquantPostProcessor */ public function setQuality($quality) { + $this->triggerSetterMethodDeprecation(__METHOD__); $this->quality = $quality; return $this; @@ -58,52 +67,83 @@ public function setQuality($quality) /** * @param BinaryInterface $binary + * @param array $options * * @throws ProcessFailedException * * @return BinaryInterface */ - public function process(BinaryInterface $binary) + protected function doProcess(BinaryInterface $binary, array $options = array()) { - return $this->processWithConfiguration($binary, array()); + if (!$this->isBinaryTypePngImage($binary)) { + return $binary; + } + + $process = $this->setupProcessBuilder($options, $binary)->add('-')->setInput($binary->getContent())->getProcess(); + $process->run(); + + if (!$this->isSuccessfulProcess($process, array(0, 98, 99), array())) { + throw new ProcessFailedException($process); + } + + return new Binary($process->getOutput(), $binary->getMimeType(), $binary->getFormat()); } /** - * @param BinaryInterface $binary - * @param array $options - * - * @throws ProcessFailedException + * @param array $options * - * @return BinaryInterface + * @return ProcessBuilder */ - public function processWithConfiguration(BinaryInterface $binary, array $options) + private function setupProcessBuilder(array $options = array()) { - $type = strtolower($binary->getMimeType()); - if (!in_array($type, array('image/png'))) { - return $binary; + $builder = $this->createProcessBuilder(array($this->executablePath), $options); + + if ($quality = isset($options['quality']) ? $options['quality'] : $this->quality) { + if (is_string($quality) && false !== strpos($quality, '-')) { + @trigger_error(sprintf('Passing the "quality" option as a string was deprecated in 1.10.0 and ' . + 'will be removed in 2.0. Instead, pass wither an integer representing the max value or an array ' . + 'representing the minimum and maximum values.'), E_USER_DEPRECATED); + + $quality = array_map(function ($q) { + return (int) $q; + }, explode('-', $quality)); + } + + if (!is_array($quality)) { + $quality = array(0, (int) $quality); + } + + if (1 === count($quality)) { + array_unshift($quality, 0); + } + + if ($quality[0] > $quality[1]) { + throw new InvalidOptionException('the "quality" option cannot have a greater minimum value value than maximum quality value', $options); + } elseif (!in_array($quality[0], range(0, 100)) || !in_array($quality[1], range(0, 100))) { + throw new InvalidOptionException('the "quality" option value(s) must be an int between 0 and 100', $options); + } + + $builder->add('--quality')->add(sprintf('%d-%d', $quality[0], $quality[1])); } - $pb = new ProcessBuilder(array($this->pngquantBin)); - - // Specify quality. - $tranformQuality = array_key_exists('quality', $options) ? $options['quality'] : $this->quality; - $pb->add('--quality'); - $pb->add($tranformQuality); + if (isset($options['speed'])) { + if (!in_array($options['speed'], range(1, 11))) { + throw new InvalidOptionException('the "speed" option must be an int between 1 and 11', $options); + } - // Read to/from stdout to save resources. - $pb->add('-'); - $pb->setInput($binary->getContent()); - - $proc = $pb->getProcess(); - $proc->run(); - - // 98 and 99 are "quality too low" to compress current current image which, while isn't ideal, is not a failure - if (!in_array($proc->getExitCode(), array(0, 98, 99))) { - throw new ProcessFailedException($proc); + $builder->add('--speed')->add($options['speed']); } - $result = new Binary($proc->getOutput(), $binary->getMimeType(), $binary->getFormat()); + if (isset($options['dithering'])) { + if (false === $options['dithering']) { + $builder->add('--nofs'); + } elseif ($options['dithering'] >= 0 && $options['dithering'] <= 1) { + $builder->add('--floyd')->add($options['dithering']); + } elseif (true !== $options['dithering']) { + throw new InvalidOptionException('the "dithering" option must be a float between 0 and 1 or a bool', $options); + } + } - return $result; + return $builder; } } diff --git a/Imagine/Filter/PostProcessor/PostProcessorInterface.php b/Imagine/Filter/PostProcessor/PostProcessorInterface.php index 4271ac457..ccfc94aaa 100644 --- a/Imagine/Filter/PostProcessor/PostProcessorInterface.php +++ b/Imagine/Filter/PostProcessor/PostProcessorInterface.php @@ -12,20 +12,22 @@ namespace Liip\ImagineBundle\Imagine\Filter\PostProcessor; use Liip\ImagineBundle\Binary\BinaryInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; /** - * Interface for PostProcessors - handlers which can operate on binaries prepared in FilterManager. - * - * @see ConfigurablePostProcessorInterface For a means to configure these at run-time - * * @author Konstantin Tjuterev */ interface PostProcessorInterface { /** + * Performs post-process operation on passed binary and returns the resulting binary. + * * @param BinaryInterface $binary + * @param array $options + * + * @throws ProcessFailedException * * @return BinaryInterface */ - public function process(BinaryInterface $binary); + public function process(BinaryInterface $binary /* array $options = array() */); } diff --git a/Resources/doc/post-processors/png-opti.rst b/Resources/doc/post-processors/png-opti.rst index 0e884e22f..17216b8f6 100644 --- a/Resources/doc/post-processors/png-opti.rst +++ b/Resources/doc/post-processors/png-opti.rst @@ -42,12 +42,37 @@ for the resulting image binary. Options ------- -:strong:`strip_all:` ``bool`` - Removes all comments, EXIF markers, and other image metadata. - :strong:`level:` ``int`` - Sets the image optimization factor. + Sets the image optimization level. Valid values are integers between ``0`` and ``7``. + +:strong:`snip:` ``bool`` + When multi-images are encountered (for example, an animated image), this causes one of the images to be kept and drops + the other ones. Depending on the input format, this may be either the first or the most relevant (e.g. the largest) image. + +:strong:`strip:` ``bool|string`` + When set to ``true``, all extra image headers, such as its comments, EXIF markers, and other metadata, will be removed. + Equivalently, the string value ``all`` also removes all extra metadata. + +:strong:`preserve_attributes:` ``bool`` + Preserve file attributes (time stamps, file access rights, etc.) where applicable/possible. + +:strong:`interlace_type:` ``int`` + Sets the interlace type used for the output file. When set to ``0``, the output image will be non-interlaced. When + set to ``1``, the output image will be interlaced using the Adam7 method. When not set, the output will have the + same interlace type as the original input. + +:strong:`no_bit_depth_reductions:` ``bool`` + Disables any bit depth reduction optimizations. + +:strong:`no_color_type_reductions:` ``bool`` + Disables any color type reduction optimizations. + +:strong:`no_palette_reductions:` ``bool`` + Disables any color palette reduction optimizations. +:strong:`no_reductions:` ``bool`` + Disables any lossless reduction optimizations, enabling ``no_bit_depth_reductions``, ``no_color_type_reductions``, + and ``no_palette_reductions``. Parameters ---------- diff --git a/Resources/doc/post-processors/png-quant.rst b/Resources/doc/post-processors/png-quant.rst index 61c3431f5..4f1c87809 100644 --- a/Resources/doc/post-processors/png-quant.rst +++ b/Resources/doc/post-processors/png-quant.rst @@ -41,9 +41,19 @@ This configuration sets a quality factor range of 75 to 80 for the resulting ima Options ------- -:strong:`quality:` ``int`` - Sets the image optimization factor. - +:strong:`quality:` ``int|int[]`` + When set to an ``int`` this sets the maximum image quality level. When set to an ``int[]`` (such as ``[60,80]``) the + first array ``int`` is used to define the lowest acceptable quality level and the second to define the maximum quality + level (in this mode, the executable will use the least amount of colors required to meet or exceed the maximum quality, + but if the conversion results in a quality below the minimum quality the converted file will be discarded and the + original one used instead). + +:strong:`speed:` ``int`` + The speed/quality trade-off value to use. Valid values: ``1`` (slowest/best) through ``11`` (fastest/worst). + +:string:`dithering:` ``bool|float`` + When set to ``false`` the Floyd-Steinberg dithering algorithm is completely disabled. Otherwise, when a ``float``, + the dithering level is set. Parameters ---------- diff --git a/Tests/Exception/Imagine/Filter/PostProcessor/InvalidOptionExceptionTest.php b/Tests/Exception/Imagine/Filter/PostProcessor/InvalidOptionExceptionTest.php new file mode 100644 index 000000000..60ae30666 --- /dev/null +++ b/Tests/Exception/Imagine/Filter/PostProcessor/InvalidOptionExceptionTest.php @@ -0,0 +1,50 @@ + 'bar'), 'foo="bar"'), + array('a foobar message', array('baz' => new \stdClass()), 'baz="stdClass::__set_state(array())"'), + array('a foobar message', array('foo' => 'bar', 'baz' => new \stdClass()), 'foo="bar", baz="stdClass::__set_state(array())"'), + array('a foobar message', array('foo' => 'bar', 'baz' => new \stdClass(), 'int' => 100, 'array' => array('this', 'that')), 'foo="bar", baz="stdClass::__set_state(array())", int="100", array="["this","that"]"'), + array('a foobar message', array('foo' => 'bar', 'baz' => new \stdClass(), 'int' => 100, 'array' => array('this' => 'that')), 'foo="bar", baz="stdClass::__set_state(array())", int="100", array="{"this":"that"}"'), + ); + } + + /** + * @dataProvider provideExceptionMessageData + * + * @param string $message + * @param array $options + * @param string $optionsText + */ + public function testExceptionMessage($message, array $options, $optionsText) + { + $exception = new InvalidOptionException($message, $options); + + $this->assertContains(sprintf('(%s)', $message), $exception->getMessage()); + $this->assertContains(sprintf('[%s]', $optionsText), $exception->getMessage()); + } +} diff --git a/Tests/Fixtures/bin/post-process-as-file-error.bash b/Tests/Fixtures/bin/post-process-as-file-error.bash new file mode 100755 index 000000000..139c54b31 --- /dev/null +++ b/Tests/Fixtures/bin/post-process-as-file-error.bash @@ -0,0 +1,5 @@ +#!/bin/bash + +source "`cd $(dirname ${BASH_SOURCE[0]}) && pwd`/post-process-as-file.bash" + +exitAsFailed diff --git a/Tests/Fixtures/bin/post-process-as-file.bash b/Tests/Fixtures/bin/post-process-as-file.bash new file mode 100755 index 000000000..612e87b3c --- /dev/null +++ b/Tests/Fixtures/bin/post-process-as-file.bash @@ -0,0 +1,17 @@ +#!/bin/bash + +source "`cd $(dirname ${BASH_SOURCE[0]}) && pwd`/post-process-common.bash" + +function main() +{ + local arguments=("${@}") + local inputFile="" + + for a in "${arguments[@]}"; do + inputFile="${a}" + done + + writeScriptDebugFile <<< $(writeScriptInformation "${inputFile}" "${arguments[@]}") +} + +main "${@}" diff --git a/Tests/Fixtures/bin/post-process-as-stdin-error.bash b/Tests/Fixtures/bin/post-process-as-stdin-error.bash new file mode 100755 index 000000000..7cfd5be91 --- /dev/null +++ b/Tests/Fixtures/bin/post-process-as-stdin-error.bash @@ -0,0 +1,5 @@ +#!/bin/bash + +source "`cd $(dirname ${BASH_SOURCE[0]}) && pwd`/post-process-as-stdin.bash" + +exitAsFailed diff --git a/Tests/Fixtures/bin/post-process-as-stdin.bash b/Tests/Fixtures/bin/post-process-as-stdin.bash new file mode 100755 index 000000000..856304ed6 --- /dev/null +++ b/Tests/Fixtures/bin/post-process-as-stdin.bash @@ -0,0 +1,21 @@ +#!/bin/bash + +source "`cd $(dirname ${BASH_SOURCE[0]}) && pwd`/post-process-common.bash" + +function main() +{ + local arguments=("${@}") + local inputFile="" + + for a in "${arguments[@]}"; do + inputFile="${a}" + done + + if [[ ! -f "${inputFile}" ]]; then + inputFile="stdin" + fi + + writeScriptDebugFile <<< $(writeScriptInformation "stdin" "${arguments[@]}") +} + +main "${@}" diff --git a/Tests/Fixtures/bin/post-process-common.bash b/Tests/Fixtures/bin/post-process-common.bash new file mode 100755 index 000000000..2c870b78f --- /dev/null +++ b/Tests/Fixtures/bin/post-process-common.bash @@ -0,0 +1,51 @@ +#!/bin/bash + +function writeScriptInformation() +{ + local inputFile="${1}" + shift + local arguments=("${@}") + local iteration=1 + + printf 'argument-size:%d\nargument-list:' ${#arguments[@]} + printf '%s ' "${arguments[@]}" + printf '\n' + + for a in "${arguments[@]}"; do + printf 'argument-%04d:%s\n' ${iteration} "${a}" + iteration=$((${iteration}+1)) + done + + if [[ ! -f "${inputFile}" ]] && [[ "${inputFile}" != "stdin" ]]; then + printf 'input-type:file\ninput-file:\ncmd-status:error\n' + elif [[ "${inputFile}" == "stdin" ]]; then + printf 'input-type:stdin\ninput-file:%s\ncmd-status:success\nfiledumped:\n' "${inputFile}" + + if [[ "${inputFile}" == "stdin" ]]; then + if read -t 0; then + cat + else + echo "$*" + fi + fi + else + printf 'input-type:file\ninput-file:%s\ncmd-status:success\nfiledumped:\n' "${inputFile}" + cat "${inputFile}" + fi +} + +function writeScriptDebugFile() +{ + local debugFile="/tmp/post-process-fixture-bin.log" + + if read -t 0; then + cat | tee "${debugFile}" + else + echo "$*" | tee "${debugFile}" + fi +} + +function exitAsFailed() +{ + exit 255 +} diff --git a/Tests/Imagine/Filter/FilterManagerTest.php b/Tests/Imagine/Filter/FilterManagerTest.php index b850626a4..ec1e31595 100644 --- a/Tests/Imagine/Filter/FilterManagerTest.php +++ b/Tests/Imagine/Filter/FilterManagerTest.php @@ -984,7 +984,7 @@ public function testApplyPostProcessor() } /** * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Could not find post processor "foo" + * @expectedExceptionMessage Post-processor "foo" could not be found */ public function testThrowsIfNoPostProcessorAddedForFilterOnApplyFilter() { diff --git a/Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTest.php b/Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTest.php new file mode 100644 index 000000000..9d3ecefa9 --- /dev/null +++ b/Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTest.php @@ -0,0 +1,218 @@ +getProtectedReflectionMethodVisible($processor = $this->getPostProcessorInstance(), 'process') + ->invoke($processor, $this->getBinaryInterfaceMock()); + } + + /** + * @group legacy + * + * @expectedDeprecation The %s::processWithConfiguration() method was deprecated in 1.10.0 and will be removed in 2.0. Use the %s::process() method instead. + */ + public function testProcessWithConfigurationDeprecation() + { + $this + ->getProtectedReflectionMethodVisible($processor = $this->getPostProcessorInstance(), 'processWithConfiguration') + ->invoke($processor, $this->getBinaryInterfaceMock(), array()); + } + + public function testIsBinaryOfType() + { + $binary = $this->getBinaryInterfaceMock(); + + $binary + ->expects($this->atLeastOnce()) + ->method('getMimeType') + ->willReturnOnConsecutiveCalls( + 'image/jpg', 'image/jpeg', 'text/plain', 'image/png', 'image/jpg', 'image/jpeg', 'text/plain', 'image/png' + ); + + $processor = $this->getPostProcessorInstance(); + + $m = $this->getProtectedReflectionMethodVisible($processor, 'isBinaryTypeJpgImage'); + $this->assertTrue($m->invoke($processor, $binary)); + $this->assertTrue($m->invoke($processor, $binary)); + $this->assertFalse($m->invoke($processor, $binary)); + $this->assertFalse($m->invoke($processor, $binary)); + + $m = $this->getProtectedReflectionMethodVisible($processor, 'isBinaryTypePngImage'); + $this->assertFalse($m->invoke($processor, $binary)); + $this->assertFalse($m->invoke($processor, $binary)); + $this->assertFalse($m->invoke($processor, $binary)); + $this->assertTrue($m->invoke($processor, $binary)); + } + + public function testCreateProcessBuilder() + { + $optionTimeout = 120.0; + $optionPrefix = array('a-custom-prefix'); + $optionWorkDir = getcwd(); + $optionEnvVars = array('FOO' => 'BAR'); + $optionOptions = array('bypass_shell' => true); + + $m = $this->getProtectedReflectionMethodVisible($processor = $this->getPostProcessorInstance(), 'createProcessBuilder'); + $b = $m->invokeArgs($processor, array(array('/path/to/bin'), array( + 'process' => array( + 'timeout' => $optionTimeout, + 'prefix' => $optionPrefix, + 'working_directory' => $optionWorkDir, + 'environment_variables' => $optionEnvVars, + 'options' => $optionOptions, + ), + ))); + + $this->assertSame($optionTimeout, $this->getProtectedReflectionPropertyVisible($b, 'timeout')->getValue($b)); + $this->assertSame($optionPrefix, $this->getProtectedReflectionPropertyVisible($b, 'prefix')->getValue($b)); + $this->assertSame($optionWorkDir, $this->getProtectedReflectionPropertyVisible($b, 'cwd')->getValue($b)); + $this->assertSame($optionEnvVars, $this->getProtectedReflectionPropertyVisible($b, 'env')->getValue($b)); + $this->assertSame($optionOptions, $this->getProtectedReflectionPropertyVisible($b, 'options')->getValue($b)); + } + + /** + * @return array[] + */ + public static function provideWriteTemporaryFileData() + { + $find = new Finder(); + $data = array(); + + foreach ($find->in(__DIR__)->name('*.php')->files() as $f) { + $data[] = array(file_get_contents($f), 'application/x-php', 'php', 'foo-context', array()); + $data[] = array(file_get_contents($f), 'application/x-php', 'php', 'bar-context', array('temp_dir' => null)); + $data[] = array(file_get_contents($f), 'application/x-php', 'php', 'bar-context', array('temp_dir' => sys_get_temp_dir())); + $data[] = array(file_get_contents($f), 'application/x-php', 'php', 'baz-context', array('temp_dir' => sprintf('%s/foo/bar/baz', sys_get_temp_dir()))); + } + + return $data; + } + + /** + * @dataProvider provideWriteTemporaryFileData + * + * @param string $content + * @param string $mimeType + * @param string $format + * @param string $prefix + * @param array $options + */ + public function testWriteTemporaryFile($content, $mimeType, $format, $prefix, array $options) + { + $writer = $this->getProtectedReflectionMethodVisible($processor = $this->getPostProcessorInstance(), 'writeTemporaryFile'); + + $baseBinary = new Binary($content, $mimeType, $format); + $this->assertTemporaryFile($content, $base = $writer->invoke($processor, $baseBinary, $options, $prefix), $prefix, $options); + + $fileBinary = new FileBinary($base, $mimeType, $format); + $this->assertTemporaryFile($content, $file = $writer->invoke($processor, $fileBinary, $options, $prefix), $prefix, $options); + + @unlink($base); + @unlink($file); + + if (is_dir($dir = sprintf('%s/foo/bar/baz', sys_get_temp_dir()))) { + @rmdir($dir); + } + + if (is_dir($dir = sprintf('%s/foo/bar', sys_get_temp_dir()))) { + @rmdir($dir); + } + + if (is_dir($dir = sprintf('%s/foo', sys_get_temp_dir()))) { + @rmdir($dir); + } + } + + /** + * @return array[] + */ + public static function provideIsValidReturnData() + { + return array( + array(array(), array(), true), + array(array(0), array(), true), + array(array(100, 200, 0), array(), true), + array(array(100), array(), false), + array(array(100, 200), array(), false), + array(array(), array('ERROR'), true), + array(array(0), array('foo'), false), + array(array(0), array('foo-bar', 'baz'), false), + array(array(0), array('foo-bar', 'ERROR'), true), + ); + } + + /** + * @dataProvider provideIsValidReturnData + * + * @param array $validReturns + * @param array $errorString + * @param bool $expected + */ + public function testIsValidReturn(array $validReturns, array $errorString, $expected) + { + $process = $this + ->getMockBuilder('\Symfony\Component\Process\Process') + ->disableOriginalConstructor() + ->getMock(); + + $process + ->expects($this->any()) + ->method('getExitCode') + ->willReturn(0); + + $process + ->expects($this->any()) + ->method('getOutput') + ->willReturn('foo bar baz'); + + $result = $this + ->getProtectedReflectionMethodVisible($processor = $this->getPostProcessorInstance(), 'isSuccessfulProcess') + ->invoke($processor, $process, $validReturns, $errorString); + + $this->assertSame($expected, $result); + } + + /** + * @param array $parameters + * + * @return \PHPUnit_Framework_MockObject_MockObject|AbstractPostProcessor + */ + protected function getPostProcessorInstance(array $parameters = array()) + { + if (count($parameters) === 0) { + $parameters = array(static::getPostProcessAsStdInExecutable()); + } + + return $this + ->getMockBuilder('\Liip\ImagineBundle\Imagine\Filter\PostProcessor\AbstractPostProcessor') + ->setConstructorArgs($parameters) + ->getMockForAbstractClass(); + } +} diff --git a/Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTestCase.php b/Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTestCase.php new file mode 100644 index 000000000..92a3f0777 --- /dev/null +++ b/Tests/Imagine/Filter/PostProcessor/AbstractPostProcessorTestCase.php @@ -0,0 +1,140 @@ +getMockBuilder('\Liip\ImagineBundle\Binary\BinaryInterface') + ->getMock(); + } + + /** + * @param string $content + * @param string $file + * @param string $context + * @param array $options + */ + protected function assertTemporaryFile($content, $file, $context, array $options = array()) + { + $this->assertFileExists($file); + $this->assertContains($context, $file); + $this->assertSame($content, file_get_contents($file)); + + if (isset($options['temp_dir'])) { + $this->assertContains($options['temp_dir'], $file); + } + } + + /** + * @param \ReflectionObject|string $object + * @param string $method + * + * @return \ReflectionMethod + */ + protected function getProtectedReflectionMethodVisible($object, $method) + { + if ($object instanceof \ReflectionObject) { + $r = $object; + } else { + $r = new \ReflectionObject($object); + } + + $m = $r->getMethod($method); + $m->setAccessible(true); + + return $m; + } + + /** + * @param \ReflectionObject|string $object + * @param string $property + * + * @return \ReflectionProperty + */ + protected function getProtectedReflectionPropertyVisible($object, $property) + { + if ($object instanceof \ReflectionObject) { + $r = $object; + } else { + $r = new \ReflectionObject($object); + } + + $p = $r->getProperty($property); + $p->setAccessible(true); + + return $p; + } + + /** + * @param array $options + * + * @return array + */ + protected function getSetupProcessBuilderArguments(array $options) + { + $builder = $this + ->getProtectedReflectionMethodVisible($processor = $this->getPostProcessorInstance(), 'setupProcessBuilder') + ->invokeArgs($processor, array($options)); + + return $this + ->getProtectedReflectionPropertyVisible($builder, 'arguments') + ->getValue($builder); + } +} diff --git a/Tests/Imagine/Filter/PostProcessor/JpegOptimPostProcessorTest.php b/Tests/Imagine/Filter/PostProcessor/JpegOptimPostProcessorTest.php new file mode 100644 index 000000000..bdbe1ef17 --- /dev/null +++ b/Tests/Imagine/Filter/PostProcessor/JpegOptimPostProcessorTest.php @@ -0,0 +1,192 @@ +getPostProcessorInstance()->setMax(50); + } + + /** + * @group legacy + * + * @expectedDeprecation The %s::setProgressive() method was deprecated in 1.10.0 and will be removed in 2.0. You must setup the class state via its __construct() method. You can still pass filter-specific options to the process() method to overwrite behavior. + */ + public function testDeprecatedSetProgressiveMethod() + { + $this->getPostProcessorInstance()->setProgressive(50); + } + + /** + * @group legacy + * + * @expectedDeprecation The %s::setStripAll() method was deprecated in 1.10.0 and will be removed in 2.0. You must setup the class state via its __construct() method. You can still pass filter-specific options to the process() method to overwrite behavior. + */ + public function testDeprecatedSetStripAllMethod() + { + $this->getPostProcessorInstance()->setStripAll(50); + } + + /** + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "quality" option must be an int between 0 and 100 + */ + public function testInvalidLevelOption() + { + $this->getSetupProcessBuilderArguments(array('quality' => 1000)); + } + + /** + * @group legacy + * + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "max" and "quality" options cannot both be set + * @expectedDeprecation The "max" option was deprecated in 1.10.0 and will be removed in 2.0. Instead, use the "quality" option. + */ + public function testInvalidStripOptionAndDeprecation() + { + $this->getSetupProcessBuilderArguments(array('max' => 50, 'quality' => 50)); + } + + /** + * @group legacy + * + * @expectedDeprecation The "max" option was deprecated in 1.10.0 and will be removed in 2.0. Instead, use the "quality" option. + */ + public function testInvalidStripDeprecationMessage() + { + $this->assertContains('--max=50', $this->getSetupProcessBuilderArguments(array('max' => 50))); + } + + /** + * @return mixed[] + */ + public static function provideSetupProcessBuilderData() + { + $data = array( + array(array(), array('--strip-all', '--all-progressive')), + array(array('strip_all' => false), array('--all-progressive')), + array(array('strip_all' => true), array('--strip-all', '--all-progressive')), + array(array('quality' => 50), array('--strip-all', '--max=50', '--all-progressive')), + array(array('progressive' => false), array('--strip-all', '--all-normal')), + array(array('progressive' => true), array('--strip-all', '--all-progressive')), + ); + + return array_map(function (array $d) { + array_unshift($d[1], AbstractPostProcessorTestCase::getPostProcessAsFileExecutable()); + + return $d; + }, $data); + } + + /** + * @dataProvider provideSetupProcessBuilderData + */ + public function testSetupProcessBuilder(array $options, array $expected) + { + $this->assertSame($expected, $this->getSetupProcessBuilderArguments($options)); + } + + public function testProcessWithNonSupportedMimeType() + { + $binary = $this->getBinaryInterfaceMock(); + + $binary + ->expects($this->atLeastOnce()) + ->method('getMimeType') + ->willReturn('application/x-php'); + + $this->assertSame($binary, $this->getPostProcessorInstance()->process($binary, array())); + } + + /** + * @return mixed[] + */ + public static function provideProcessData() + { + $file = file_get_contents(__FILE__); + $data = array( + array(array(), '--strip-all --all-progressive'), + array(array('strip_all' => false), '--all-progressive'), + array(array('strip_all' => true), '--strip-all --all-progressive'), + array(array('quality' => 50), '--strip-all --max=50 --all-progressive'), + array(array('progressive' => false), '--strip-all --all-normal'), + array(array('progressive' => true), '--strip-all --all-progressive'), + ); + + return array_map(function ($d) use ($file) { + array_unshift($d, $file); + + return $d; + }, $data); + } + + /** + * @dataProvider provideProcessData + * + * @param string $content + * @param array $options + * @param string $expected + */ + public function testProcess($content, array $options, $expected) + { + $file = sys_get_temp_dir().'/test.jpeg'; + file_put_contents($file, $content); + + $process = $this->getPostProcessorInstance(); + $result = $process->process(new FileBinary($file, 'image/jpeg', 'jpeg'), $options); + + $this->assertContains($expected, $result->getContent()); + $this->assertContains($content, $result->getContent()); + + @unlink($file); + } + + /** + * @dataProvider provideProcessData + * + * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException + * + * @param array $options + * @param string $expected + */ + public function testProcessError($content, array $options, $expected) + { + $process = $this->getPostProcessorInstance(array(static::getPostProcessAsFileFailingExecutable())); + $process->process(new Binary('content', 'image/jpeg', 'jpeg'), $options); + } + + /** + * @param array $parameters + * + * @return JpegOptimPostProcessor + */ + protected function getPostProcessorInstance(array $parameters = array()) + { + return new JpegOptimPostProcessor(isset($parameters[0]) ? $parameters[0] : static::getPostProcessAsFileExecutable()); + } +} diff --git a/Tests/Imagine/Filter/PostProcessor/MozJpegPostProcessorTest.php b/Tests/Imagine/Filter/PostProcessor/MozJpegPostProcessorTest.php new file mode 100644 index 000000000..2e5b43c2d --- /dev/null +++ b/Tests/Imagine/Filter/PostProcessor/MozJpegPostProcessorTest.php @@ -0,0 +1,141 @@ +getPostProcessorInstance()->setQuality(50); + } + + /** + * @return mixed[] + */ + public static function provideSetupProcessBuilderData() + { + $data = array( + array(array(), array('-quant-table', 2, '-optimise')), + array(array('quant_table' => 10), array('-quant-table', 10, '-optimise')), + array(array('optimise' => false), array('-quant-table', 2)), + array(array('optimise' => true), array('-quant-table', 2, '-optimise')), + array(array('quality' => 50), array('-quant-table', 2, '-optimise', '-quality', 50)), + array(array('quant_table' => 4, 'optimise' => true, 'quality' => 100), array('-quant-table', 4, '-optimise', '-quality', 100)), + ); + + return array_map(function (array $d) { + array_unshift($d[1], AbstractPostProcessorTestCase::getPostProcessAsStdInExecutable()); + + return $d; + }, $data); + } + + /** + * @dataProvider provideSetupProcessBuilderData + */ + public function testSetupProcessBuilder(array $options, array $expected) + { + $this->assertSame($expected, $this->getSetupProcessBuilderArguments($options)); + } + + /** + * @return mixed[] + */ + public static function provideProcessData() + { + $file = 'stdio-file-content-string'; + $data = array( + array(array(), '-quant-table 2 -optimise'), + array(array('quant_table' => 10), '-quant-table 10 -optimise'), + array(array('optimise' => false), '-quant-table 2'), + array(array('optimise' => true), '-quant-table 2 -optimise'), + array(array('quality' => 50), '-quant-table 2 -optimise -quality 50'), + array(array('quant_table' => 4, 'optimise' => true, 'quality' => 100), '-quant-table 4 -optimise -quality 100'), + ); + + return array_map(function ($d) use ($file) { + array_unshift($d, $file); + + return $d; + }, $data); + } + + /** + * @dataProvider provideProcessData + * + * @param string $content + * @param array $options + * @param string $expected + */ + public function testProcess($content, array $options, $expected) + { + $file = sys_get_temp_dir().'/test.jpeg'; + file_put_contents($file, $content); + + $process = $this->getPostProcessorInstance(); + $result = $process->process(new FileBinary($file, 'image/jpeg', 'jpeg'), $options); + + $this->assertContains($expected, $result->getContent()); + $this->assertContains($content, $result->getContent()); + + @unlink($file); + } + + /** + * @dataProvider provideProcessData + * + * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException + * + * @param array $options + * @param string $expected + */ + public function testProcessError($content, array $options, $expected) + { + $process = $this->getPostProcessorInstance(array(static::getPostProcessAsStdInErrorExecutable())); + $process->process(new Binary('content', 'image/jpeg', 'jpeg'), $options); + } + + public function testProcessWithNonSupportedMimeType() + { + $binary = $this->getBinaryInterfaceMock(); + + $binary + ->expects($this->atLeastOnce()) + ->method('getMimeType') + ->willReturn('application/x-php'); + + $this->assertSame($binary, $this->getPostProcessorInstance()->process($binary, array())); + } + + /** + * @param array $parameters + * + * @return MozJpegPostProcessor + */ + protected function getPostProcessorInstance(array $parameters = array()) + { + return new MozJpegPostProcessor(isset($parameters[0]) ? $parameters[0] : static::getPostProcessAsStdinExecutable()); + } +} diff --git a/Tests/Imagine/Filter/PostProcessor/OptiPngPostProcessorTest.php b/Tests/Imagine/Filter/PostProcessor/OptiPngPostProcessorTest.php new file mode 100644 index 000000000..28504a754 --- /dev/null +++ b/Tests/Imagine/Filter/PostProcessor/OptiPngPostProcessorTest.php @@ -0,0 +1,192 @@ +getSetupProcessBuilderArguments(array('level' => 100)); + } + + /** + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage Invalid post-processor configuration provided (the "interlace_type" option must be either 0 or 1) + */ + public function testInvalidInterlaceOption() + { + $this->getSetupProcessBuilderArguments(array('interlace_type' => 10)); + } + + /** + * @group legacy + * + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "strip" and "strip_all" options cannot both be set + * @expectedDeprecation The "strip_all" option was deprecated in 1.10.0 and will be removed in 2.0. Instead, use the "strip" option. + */ + public function testInvalidStripOptionAndDeprecation() + { + $this->getSetupProcessBuilderArguments(array('strip_all' => true, 'strip' => 'all')); + } + + /** + * @group legacy + * + * @expectedDeprecation The "strip_all" option was deprecated in 1.10.0 and will be removed in 2.0. Instead, use the "strip" option. + */ + public function testInvalidStripDeprecationMessage() + { + $arguments = $this->getSetupProcessBuilderArguments(array('strip_all' => true)); + + $this->assertSame('all', array_pop($arguments)); + $this->assertSame('-strip', array_pop($arguments)); + } + + /** + * @return mixed[] + */ + public static function provideSetupProcessBuilderData() + { + $data = array( + array(array(), array('-o7', '-strip', 'all')), + array(array('level' => null), array('-o7', '-strip', 'all')), + array(array('level' => 0), array('-o0', '-strip', 'all')), + array(array('level' => 6), array('-o6', '-strip', 'all')), + array(array('snip' => false), array('-o7', '-strip', 'all')), + array(array('snip' => true), array('-o7', '-strip', 'all', '-snip')), + array(array('preserve_attributes' => false), array('-o7', '-strip', 'all')), + array(array('preserve_attributes' => true), array('-o7', '-strip', 'all', '-preserve')), + array(array('interlace_type' => null), array('-o7', '-strip', 'all')), + array(array('interlace_type' => 0), array('-o7', '-strip', 'all', '-i', 0)), + array(array('interlace_type' => 1), array('-o7', '-strip', 'all', '-i', 1)), + array(array('no_bit_depth_reductions' => false), array('-o7', '-strip', 'all')), + array(array('no_bit_depth_reductions' => true), array('-o7', '-strip', 'all', '-nb')), + array(array('no_color_type_reductions' => false), array('-o7', '-strip', 'all')), + array(array('no_color_type_reductions' => true), array('-o7', '-strip', 'all', '-nc')), + array(array('no_palette_reductions' => false), array('-o7', '-strip', 'all')), + array(array('no_palette_reductions' => true), array('-o7', '-strip', 'all', '-np')), + array(array('no_reductions' => false), array('-o7', '-strip', 'all')), + array(array('no_reductions' => true), array('-o7', '-strip', 'all', '-nx')), + array(array('level' => 4, 'snip' => true, 'preserve_attributes' => true, 'interlace_type' => 1, 'no_bit_depth_reductions' => true, 'no_palette_reductions' => true), array('-o4', '-strip', 'all', '-snip', '-preserve', '-i', 1, '-nb', '-np')), + ); + + return array_map(function (array $d) { + array_unshift($d[1], AbstractPostProcessorTestCase::getPostProcessAsFileExecutable()); + + return $d; + }, $data); + } + + /** + * @dataProvider provideSetupProcessBuilderData + */ + public function testSetupProcessBuilder(array $options, array $expected) + { + $this->assertSame($expected, $this->getSetupProcessBuilderArguments($options)); + } + + /** + * @return mixed[] + */ + public static function provideProcessData() + { + $file = file_get_contents(__FILE__); + $data = array( + array(array(), '--quality 80-100'), + array(array('quality' => null), '--quality 80-100'), + array(array('quality' => array(80, 100)), '--quality 80-100'), + array(array('quality' => array(100)), '--quality 0-100'), + array(array('quality' => '80'), '--quality 0-80'), + array(array('speed' => null), '--quality 80-100'), + array(array('speed' => 4), '--quality 80-100 --speed 4'), + array(array('dithering' => null), '--quality 80-100'), + array(array('dithering' => false), '--quality 80-100 --nofs'), + array(array('dithering' => 0.5), '--quality 80-100 --floyd 0.5'), + ); + + return array_map(function ($d) use ($file) { + array_unshift($d, $file); + + return $d; + }, $data); + } + + /** + * @dataProvider provideProcessData + * + * @param string $content + * @param array $options + * @param string $expected + */ + public function testProcess($content, array $options, $expected) + { + $file = sys_get_temp_dir().'/test.png'; + file_put_contents($file, $content); + + $process = $this->getPostProcessorInstance(); + $result = $process->process(new FileBinary($file, 'image/png', 'png'), $options); + + $this->assertContains($expected, $result->getContent()); + $this->assertContains($content, $result->getContent()); + + @unlink($file); + } + + /** + * @dataProvider provideProcessData + * + * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException + * + * @param array $options + * @param string $expected + */ + public function testProcessError($content, array $options, $expected) + { + $process = $this->getPostProcessorInstance(array(static::getPostProcessAsFileFailingExecutable())); + $process->process(new Binary('content', 'image/png', 'png'), $options); + } + + public function testProcessWithNonSupportedMimeType() + { + $binary = $this->getBinaryInterfaceMock(); + + $binary + ->expects($this->atLeastOnce()) + ->method('getMimeType') + ->willReturn('application/x-php'); + + $this->assertSame($binary, $this->getPostProcessorInstance()->process($binary, array())); + } + + /** + * @param array $parameters + * + * @return OptiPngPostProcessor + */ + protected function getPostProcessorInstance(array $parameters = array()) + { + return new OptiPngPostProcessor(isset($parameters[0]) ? $parameters[0] : static::getPostProcessAsFileExecutable()); + } +} diff --git a/Tests/Imagine/Filter/PostProcessor/PngquantPostProcessorTest.php b/Tests/Imagine/Filter/PostProcessor/PngquantPostProcessorTest.php new file mode 100644 index 000000000..940940197 --- /dev/null +++ b/Tests/Imagine/Filter/PostProcessor/PngquantPostProcessorTest.php @@ -0,0 +1,205 @@ +getPostProcessorInstance()->setQuality(50); + } + + /** + * @group legacy + * + * @expectedDeprecation Passing the "quality" option as a string was deprecated in 1.10.0 and will be removed in 2.0. Instead, pass wither an integer representing the max value or an array representing the minimum and maximum values. + */ + public function testQualityOptionDeprecation() + { + $this->getSetupProcessBuilderArguments(array('quality' => '0-100')); + } + + /** + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "quality" option cannot have a greater minimum value value than maximum quality value + */ + public function testQualityOptionThrowsOnLargerMinThanMaxValue() + { + $this->getSetupProcessBuilderArguments(array('quality' => array(75, 25))); + } + + /** + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "quality" option value(s) must be an int between 0 and 100 + */ + public function testQualityOptionThrowsOnOutOfScopeMaxInt() + { + $this->getSetupProcessBuilderArguments(array('quality' => array(25, 1000))); + } + + /** + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "quality" option value(s) must be an int between 0 and 100 + */ + public function testQualityOptionThrowsOnOutOfScopeMinInt() + { + $this->getSetupProcessBuilderArguments(array('quality' => array(-1000, 25))); + } + + /** + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "speed" option must be an int between 1 and 11 + */ + public function testSpeedOptionThrowsOnOutOfScopeInt() + { + $this->getSetupProcessBuilderArguments(array('speed' => 15)); + } + + /** + * @expectedException \Liip\ImagineBundle\Exception\Imagine\Filter\PostProcessor\InvalidOptionException + * @expectedExceptionMessage the "dithering" option must be a float between 0 and 1 or a bool + */ + public function testDitheringOptionThrowsOnOutOfScopeInt() + { + $this->getSetupProcessBuilderArguments(array('dithering' => 2)); + } + + /** + * @return mixed[] + */ + public static function provideSetupProcessBuilderData() + { + $data = array( + array(array(), array('80-100')), + array(array('quality' => null), array('80-100')), + array(array('quality' => array(80, 100)), array('80-100')), + array(array('quality' => array(100)), array('0-100')), + array(array('quality' => '80'), array('0-80')), + array(array('speed' => null), array('80-100')), + array(array('speed' => 4), array('80-100', '--speed', 4)), + array(array('dithering' => null), array('80-100')), + array(array('dithering' => false), array('80-100', '--nofs')), + array(array('dithering' => 0.5), array('80-100', '--floyd', 0.5)), + ); + + return array_map(function (array $d) { + array_unshift($d[1], '--quality'); + array_unshift($d[1], AbstractPostProcessorTestCase::getPostProcessAsStdInExecutable()); + + return $d; + }, $data); + } + + /** + * @dataProvider provideSetupProcessBuilderData + */ + public function testSetupProcessBuilder(array $options, array $expected) + { + $this->assertSame($expected, $this->getSetupProcessBuilderArguments($options)); + } + + /** + * @return mixed[] + */ + public static function provideProcessData() + { + $file = 'stdio-file-content-string'; + $data = array( + array(array(), '--quality 80-100'), + array(array('quality' => null), '--quality 80-100'), + array(array('quality' => array(80, 100)), '--quality 80-100'), + array(array('quality' => array(100)), '--quality 0-100'), + array(array('quality' => '80'), '--quality 0-80'), + array(array('speed' => null), '--quality 80-100'), + array(array('speed' => 4), '--quality 80-100 --speed 4'), + array(array('dithering' => null), '--quality 80-100'), + array(array('dithering' => false), '--quality 80-100 --nofs'), + array(array('dithering' => 0.5), '--quality 80-100 --floyd 0.5'), + ); + + return array_map(function ($d) use ($file) { + array_unshift($d, $file); + + return $d; + }, $data); + } + + /** + * @dataProvider provideProcessData + * + * @param string $content + * @param array $options + * @param string $expected + */ + public function testProcess($content, array $options, $expected) + { + $file = sys_get_temp_dir().'/test.png'; + file_put_contents($file, $content); + + $process = $this->getPostProcessorInstance(); + $result = $process->process(new FileBinary($file, 'image/png', 'png'), $options); + + $this->assertContains($expected, $result->getContent()); + $this->assertContains($content, $result->getContent()); + + @unlink($file); + } + + /** + * @dataProvider provideProcessData + * + * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException + * + * @param array $options + * @param string $expected + */ + public function testProcessError($content, array $options, $expected) + { + $process = $this->getPostProcessorInstance(array(static::getPostProcessAsStdInErrorExecutable())); + $process->process(new Binary('content', 'image/png', 'png'), $options); + } + + public function testProcessWithNonSupportedMimeType() + { + $binary = $this->getBinaryInterfaceMock(); + + $binary + ->expects($this->atLeastOnce()) + ->method('getMimeType') + ->willReturn('application/x-php'); + + $this->assertSame($binary, $this->getPostProcessorInstance()->process($binary, array())); + } + + /** + * @param array $parameters + * + * @return PngquantPostProcessor + */ + protected function getPostProcessorInstance(array $parameters = array()) + { + return new PngquantPostProcessor(isset($parameters[0]) ? $parameters[0] : static::getPostProcessAsStdInExecutable()); + } +}