taskImageMinify('assets/images/*') * ->to('dist/images/') * ->run(); * ``` * * This will use the following minifiers: * * - PNG: optipng * - GIF: gifsicle * - JPG, JPEG: jpegtran * - SVG: svgo * * When the minifier is specified the task will use that for all the input files. In that case * it is useful to filter the files with the extension: * * ```php * $this->taskImageMinify('assets/images/*.png') * ->to('dist/images/') * ->minifier('pngcrush'); * ->run(); * ``` * * The task supports the following minifiers: * * - optipng * - pngquant * - advpng * - pngout * - zopflipng * - pngcrush * - gifsicle * - jpegoptim * - jpeg-recompress * - jpegtran * - svgo (only minification, no downloading) * * You can also specifiy extra options for the minifiers: * * ```php * $this->taskImageMinify('assets/images/*.jpg') * ->to('dist/images/') * ->minifier('jpegtran', ['-progressive' => null, '-copy' => 'none']) * ->run(); * ``` * * This will execute as: * `jpegtran -copy none -progressive -optimize -outfile "dist/images/test.jpg" "/var/www/test/assets/images/test.jpg"` */ class ImageMinify extends BaseTask { /** * Destination directory for the minified images. * * @var string */ protected $to; /** * Array of the source files. * * @var array */ protected $dirs = []; /** * Symfony 2 filesystem. * * @var sfFilesystem */ protected $fs; /** * Target directory for the downloaded binary executables. * * @var string */ protected $executableTargetDir; /** * Array for the downloaded binary executables. * * @var array */ protected $executablePaths = []; /** * Array for the individual results of all the files. * * @var array */ protected $results = []; /** * Default minifier to use. * * @var string */ protected $minifier; /** * Array for minifier options. * * @var array */ protected $minifierOptions = []; /** * Supported minifiers. * * @var array */ protected $minifiers = [ // Default 4 'optipng', 'gifsicle', 'jpegtran', 'svgo', // PNG 'pngquant', 'advpng', 'pngout', 'zopflipng', 'pngcrush', // JPG 'jpegoptim', 'jpeg-recompress', ]; /** * Binary repositories of Imagemin. * * @link https://github.com/imagemin * * @var array */ protected $imageminRepos = [ // PNG 'optipng' => 'https://github.com/imagemin/optipng-bin', 'pngquant' => 'https://github.com/imagemin/pngquant-bin', 'advpng' => 'https://github.com/imagemin/advpng-bin', 'pngout' => 'https://github.com/imagemin/pngout-bin', 'zopflipng' => 'https://github.com/imagemin/zopflipng-bin', 'pngcrush' => 'https://github.com/imagemin/pngcrush-bin', // Gif 'gifsicle' => 'https://github.com/imagemin/gifsicle-bin', // JPG 'jpegtran' => 'https://github.com/imagemin/jpegtran-bin', 'jpegoptim' => 'https://github.com/imagemin/jpegoptim-bin', 'cjpeg' => 'https://github.com/imagemin/mozjpeg-bin', // note: we do not support this minifier because it creates JPG from non-JPG files 'jpeg-recompress' => 'https://github.com/imagemin/jpeg-recompress-bin', // WebP 'cwebp' => 'https://github.com/imagemin/cwebp-bin', // note: we do not support this minifier because it creates WebP from non-WebP files ]; public function __construct($dirs) { is_array($dirs) ? $this->dirs = $dirs : $this->dirs[] = $dirs; $this->fs = new sfFilesystem(); // guess the best path for the executables based on __DIR__ if (($pos = strpos(__DIR__, 'consolidation/robo')) !== false) { // the executables should be stored in vendor/bin $this->executableTargetDir = substr(__DIR__, 0, $pos).'bin'; } // check if the executables are already available foreach ($this->imageminRepos as $exec => $url) { $path = $this->executableTargetDir.'/'.$exec; // if this is Windows add a .exe extension if (substr($this->getOS(), 0, 3) == 'win') { $path .= '.exe'; } if (is_file($path)) { $this->executablePaths[$exec] = $path; } } } /** * {@inheritdoc} */ public function run() { // find the files $files = $this->findFiles($this->dirs); // minify the files $result = $this->minify($files); // check if there was an error if ($result instanceof Result) { return $result; } $amount = (count($files) == 1 ? 'image' : 'images'); $message = "Minified {filecount} out of {filetotal} $amount into {destination}"; $context = ['filecount' => count($this->results['success']), 'filetotal' => count($files), 'destination' => $this->to]; if (count($this->results['success']) == count($files)) { $this->printTaskSuccess($message, $context); return Result::success($this, $message, $context); } else { return Result::error($this, $message, $context); } } /** * Sets the target directory where the files will be copied to. * * @param string $target * * @return $this */ public function to($target) { $this->to = rtrim($target, '/'); return $this; } /** * Sets the minifier. * * @param string $minifier * @param array $options * * @return $this */ public function minifier($minifier, array $options = []) { $this->minifier = $minifier; $this->minifierOptions = array_merge($this->minifierOptions, $options); return $this; } /** * @param array $dirs * * @return array|\Robo\Result * * @throws \Robo\Exception\TaskException */ protected function findFiles($dirs) { $files = array(); // find the files foreach ($dirs as $k => $v) { // reset finder $finder = new Finder(); $dir = $k; $to = $v; // check if target was given with the to() method instead of key/value pairs if (is_int($k)) { $dir = $v; if (isset($this->to)) { $to = $this->to; } else { throw new TaskException($this, 'target directory is not defined'); } } try { $finder->files()->in($dir); } catch (\InvalidArgumentException $e) { // if finder cannot handle it, try with in()->name() if (strpos($dir, '/') === false) { $dir = './'.$dir; } $parts = explode('/', $dir); $new_dir = implode('/', array_slice($parts, 0, -1)); try { $finder->files()->in($new_dir)->name(array_pop($parts)); } catch (\InvalidArgumentException $e) { return Result::fromException($this, $e); } } foreach ($finder as $file) { // store the absolute path as key and target as value in the files array $files[$file->getRealpath()] = $this->getTarget($file->getRealPath(), $to); } $fileNoun = count($finder) == 1 ? ' file' : ' files'; $this->printTaskInfo("Found {filecount} $fileNoun in {dir}", ['filecount' => count($finder), 'dir' => $dir]); } return $files; } /** * @param string $file * @param string $to * * @return string */ protected function getTarget($file, $to) { $target = $to.'/'.basename($file); return $target; } /** * @param array $files * * @return \Robo\Result */ protected function minify($files) { // store the individual results into the results array $this->results = [ 'success' => [], 'error' => [], ]; // loop through the files foreach ($files as $from => $to) { if (!isset($this->minifier)) { // check filetype based on the extension $extension = strtolower(pathinfo($from, PATHINFO_EXTENSION)); // set the default minifiers based on the extension switch ($extension) { case 'png': $minifier = 'optipng'; break; case 'jpg': case 'jpeg': $minifier = 'jpegtran'; break; case 'gif': $minifier = 'gifsicle'; break; case 'svg': $minifier = 'svgo'; break; } } else { if (!in_array($this->minifier, $this->minifiers, true) && !is_callable(strtr($this->minifier, '-', '_')) ) { $message = sprintf('Invalid minifier %s!', $this->minifier); return Result::error($this, $message); } $minifier = $this->minifier; } // Convert minifier name to camelCase (e.g. jpeg-recompress) $funcMinifier = $this->camelCase($minifier); // call the minifier method which prepares the command if (is_callable($funcMinifier)) { $command = call_user_func($funcMinifier, $from, $to, $this->minifierOptions); } elseif (method_exists($this, $funcMinifier)) { $command = $this->{$funcMinifier}($from, $to); } else { $message = sprintf('Minifier method %s cannot be found!', $funcMinifier); return Result::error($this, $message); } // launch the command $this->printTaskInfo('Minifying {filepath} with {minifier}', ['filepath' => $from, 'minifier' => $minifier]); $result = $this->executeCommand($command); // check the return code if ($result->getExitCode() == 127) { $this->printTaskError('The {minifier} executable cannot be found', ['minifier' => $minifier]); // try to install from imagemin repository if (array_key_exists($minifier, $this->imageminRepos)) { $result = $this->installFromImagemin($minifier); if ($result instanceof Result) { if ($result->wasSuccessful()) { $this->printTaskSuccess($result->getMessage()); // retry the conversion with the downloaded executable if (is_callable($minifier)) { $command = call_user_func($minifier, $from, $to, $minifierOptions); } elseif (method_exists($this, $minifier)) { $command = $this->{$minifier}($from, $to); } // launch the command $this->printTaskInfo('Minifying {filepath} with {minifier}', ['filepath' => $from, 'minifier' => $minifier]); $result = $this->executeCommand($command); } else { $this->printTaskError($result->getMessage()); // the download was not successful return $result; } } } else { return $result; } } // check the success of the conversion if ($result->getExitCode() !== 0) { $this->results['error'][] = $from; } else { $this->results['success'][] = $from; } } } /** * @return string */ protected function getOS() { $os = php_uname('s'); $os .= '/'.php_uname('m'); // replace x86_64 to x64, because the imagemin repo uses that $os = str_replace('x86_64', 'x64', $os); // replace i386, i686, etc to x86, because of imagemin $os = preg_replace('/i[0-9]86/', 'x86', $os); // turn info to lowercase, because of imagemin $os = strtolower($os); return $os; } /** * @param string $command * * @return \Robo\Result */ protected function executeCommand($command) { // insert the options into the command $a = explode(' ', $command); $executable = array_shift($a); foreach ($this->minifierOptions as $key => $value) { // first prepend the value if (!empty($value)) { array_unshift($a, $value); } // then add the key if (!is_numeric($key)) { array_unshift($a, $key); } } // check if the executable can be replaced with the downloaded one if (array_key_exists($executable, $this->executablePaths)) { $executable = $this->executablePaths[$executable]; } array_unshift($a, $executable); $command = implode(' ', $a); // execute the command $exec = new Exec($command); return $exec->inflect($this)->printed(false)->run(); } /** * @param string $executable * * @return \Robo\Result */ protected function installFromImagemin($executable) { // check if there is an url defined for the executable if (!array_key_exists($executable, $this->imageminRepos)) { $message = sprintf('The executable %s cannot be found in the defined imagemin repositories', $executable); return Result::error($this, $message); } $this->printTaskInfo('Downloading the {executable} executable from the imagemin repository', ['executable' => $executable]); $os = $this->getOS(); $url = $this->imageminRepos[$executable].'/blob/master/vendor/'.$os.'/'.$executable.'?raw=true'; if (substr($os, 0, 3) == 'win') { // if it is win, add a .exe extension $url = $this->imageminRepos[$executable].'/blob/master/vendor/'.$os.'/'.$executable.'.exe?raw=true'; } $data = @file_get_contents($url, false, null); if ($data === false) { // there is something wrong with the url, try it without the version info $url = preg_replace('/x[68][64]\//', '', $url); $data = @file_get_contents($url, false, null); if ($data === false) { // there is still something wrong with the url if it is win, try with win32 if (substr($os, 0, 3) == 'win') { $url = preg_replace('win/', 'win32/', $url); $data = @file_get_contents($url, false, null); if ($data === false) { // there is nothing more we can do $message = sprintf('Could not download the executable %s', $executable); return Result::error($this, $message); } } // if it is not windows there is nothing we can do $message = sprintf('Could not download the executable %s', $executable); return Result::error($this, $message); } } // check if target directory exists if (!is_dir($this->executableTargetDir)) { mkdir($this->executableTargetDir); } // save the executable into the target dir $path = $this->executableTargetDir.'/'.$executable; if (substr($os, 0, 3) == 'win') { // if it is win, add a .exe extension $path = $this->executableTargetDir.'/'.$executable.'.exe'; } $result = file_put_contents($path, $data); if ($result === false) { $message = sprintf('Could not copy the executable %s to %s', $executable, $target_dir); return Result::error($this, $message); } // set the binary to executable chmod($path, 0755); // if everything successful, store the executable path $this->executablePaths[$executable] = $this->executableTargetDir.'/'.$executable; // if it is win, add a .exe extension if (substr($os, 0, 3) == 'win') { $this->executablePaths[$executable] .= '.exe'; } $message = sprintf('Executable %s successfully downloaded', $executable); return Result::success($this, $message); } /** * @param string $from * @param string $to * * @return string */ protected function optipng($from, $to) { $command = sprintf('optipng -quiet -out "%s" -- "%s"', $to, $from); if ($from != $to && is_file($to)) { // earlier versions of optipng do not overwrite the target without a backup // http://sourceforge.net/p/optipng/bugs/37/ unlink($to); } return $command; } /** * @param string $from * @param string $to * * @return string */ protected function jpegtran($from, $to) { $command = sprintf('jpegtran -optimize -outfile "%s" "%s"', $to, $from); return $command; } protected function gifsicle($from, $to) { $command = sprintf('gifsicle -o "%s" "%s"', $to, $from); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function svgo($from, $to) { $command = sprintf('svgo "%s" "%s"', $from, $to); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function pngquant($from, $to) { $command = sprintf('pngquant --force --output "%s" "%s"', $to, $from); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function advpng($from, $to) { // advpng does not have any output parameters, copy the file and then compress the copy $command = sprintf('advpng --recompress --quiet "%s"', $to); $this->fs->copy($from, $to, true); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function pngout($from, $to) { $command = sprintf('pngout -y -q "%s" "%s"', $from, $to); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function zopflipng($from, $to) { $command = sprintf('zopflipng -y "%s" "%s"', $from, $to); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function pngcrush($from, $to) { $command = sprintf('pngcrush -q -ow "%s" "%s"', $from, $to); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function jpegoptim($from, $to) { // jpegoptim only takes the destination directory as an argument $command = sprintf('jpegoptim --quiet -o --dest "%s" "%s"', dirname($to), $from); return $command; } /** * @param string $from * @param string $to * * @return string */ protected function jpegRecompress($from, $to) { $command = sprintf('jpeg-recompress --quiet "%s" "%s"', $from, $to); return $command; } /** * @param string $text * * @return string */ public static function camelCase($text) { // non-alpha and non-numeric characters become spaces $text = preg_replace('/[^a-z0-9]+/i', ' ', $text); $text = trim($text); // uppercase the first character of each word $text = ucwords($text); $text = str_replace(" ", "", $text); $text = lcfirst($text); return $text; } }