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());
+ }
+}