diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6f5331f --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "imageoptim/imageoptim": "^1.3" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..30f7383 --- /dev/null +++ b/composer.lock @@ -0,0 +1,66 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "bf14e9b7c85efaed15e3e84e7c063394", + "content-hash": "4bb4ba769cbf71a3e1875addaccbe5af", + "packages": [ + { + "name": "imageoptim/imageoptim", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/ImageOptim/php-imageoptim-api.git", + "reference": "b73eb5d6747fc181de86b2de50fb158dff463618" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ImageOptim/php-imageoptim-api/zipball/b73eb5d6747fc181de86b2de50fb158dff463618", + "reference": "b73eb5d6747fc181de86b2de50fb158dff463618", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "ImageOptim\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Kornel", + "email": "kornel@imageoptim.com" + } + ], + "description": "ImageOptim API for PHP", + "homepage": "https://imageoptim.com/api", + "keywords": [ + "image", + "optimize", + "performance", + "resize", + "scale" + ], + "time": "2017-01-09 23:58:20" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/kirby-imageoptim.php b/kirby-imageoptim.php new file mode 100644 index 0000000..90204e6 --- /dev/null +++ b/kirby-imageoptim.php @@ -0,0 +1,94 @@ +url(); + + if($width == false) { + $width = $file->width(); + } + + if($height == false) { + $height = round($file->height() * $width / $file->width()); + } + + $imageoptimAPIKey = trim(c::get('plugin.imageoptim.apikey','')); + + // If can do imageoptim... + if(!KirbyImageOptim::is_localhost() && + c::get('plugin.imageoptim', false) && + strlen($imageoptimAPIKey) > 0) { + + $wxh = $width.'x'.$height; + $hash = sha1( + $file->name().'-'. + $wxh.'-'. + $dpr.'-'. + $quality.'-'. + $file->modified()). + '.'.$file->extension(); + + $filepath = str_replace( + $file->filename(), + $hash, + kirby()->roots()->thumbs().DS.$file->uri()); + + $urlOptim = str_replace( + $file->filename(), + $hash, + kirby()->urls()->thumbs().'/'.$file->uri()); + + if(!f::exists($filepath)) { + $api = new ImageOptim\API($imageoptimAPIKey); + try{ + $imageData = $api->imageFromURL($file->url()) + ->quality($quality) + ->dpr(intval($dpr)) + ->resize($width, $height, $crop) + ->getBytes(); + + f::write($filepath, $imageData); + $url = $urlOptim; + } catch (Exception $ex) { + return $ex->getMessage(); + } + } else { + $url = $urlOptim; + } + + // ... use kirby thumb instead + } else { + + if($file->orientation() == 'portrait') { + $nw = round($file->width() * $height / $file->height()); + $url = $file->resize($nw, $height); + } else { + $url = $file->resize($width); + } + $url = str_replace([''],['',''], $url); + } + + return $url; + } +} + +$kirby->set('file::method', 'imageoptim', + function($file, $width = false, $height = false, $crop = 'fit', $dpr = 1) { + return KirbyImageOptim::imageoptim($file, $width, $height, $crop, $dpr); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..aad326b --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "kirby-imageoptim", + "description": "Kirby CMS file method to optimize images using ImageOptim within your template code.", + "version": "1.0.0", + "type": "kirby-plugin", + "license": "MIT" +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..34c4e82 --- /dev/null +++ b/readme.md @@ -0,0 +1,93 @@ +# Kirby Imageoptim + +![GitHub release](https://img.shields.io/github/release/bnomei/kirby-imageoptim.svg?maxAge=1800) ![License](https://img.shields.io/github/license/mashape/apistatus.svg) ![Kirby Version](https://img.shields.io/badge/Kirby-2.3%2B-red.svg) + +Kirby CMS file method to optimize images using [ImageOptim PHP API](https://github.com/ImageOptim/php-imageoptim-api) within your template code. Optimized image is refreshed if file is changed or calling code requests different parameters. It is saved to the `/thumbs` folder (`kirby()->roots()->thumbs()`). + +Note: ImageOptim API will only be called on webserver. On localhost the kirby thumbs api will be used to avoid the timeconsuming [upload api call](https://github.com/ImageOptim/php-imageoptim-api#imagefrompathfilepath--local-source-image). + +## Requirements + +- [**Kirby**](https://getkirby.com/) 2.3+ +- [ImageOptim API key](https://imageoptim.com/api/register) (trial available). This plugin uses v1.3.1. +- `allow_url_fopen` PHP setting must be enabled for the API to work. Check with `ini_get('allow_url_fopen')`. Please be aware of the potential security risks caused by allow_url_fopen! + +## Installation + +### [Kirby CLI](https://github.com/getkirby/cli) + +``` +kirby plugin:install bnomei/kirby-imageoptim +``` + +### Git Submodule + +``` +$ git submodule add https://github.com/bnomei/kirby-imageoptim.git site/plugins/kirby-imageoptim +``` + +### Copy and Paste + +1. [Download](https://github.com/bnomei/kirby-imageoptim/archive/master.zip) the contents of this repository as ZIP-file. +2. Rename the extracted folder to `kirby-imageoptim` and copy it into the `site/plugins/` directory in your Kirby project. + +## Usage + +In your `site/config.php` activate the plugin and set the [ImageOptim API key](https://imageoptim.com/api/register). + +``` +c::set('plugin.imageoptim', true); // default is false +c::set('plugin.imageoptim.apikey', 'YOUR_API_KEY_HERE'); +``` + +The plugin adds a `$myFile->imageoptim()` function to [$file objects](https://getkirby.com/docs/cheatsheet#file). + +``` +file('image.jpg'); + + // get url (on your webserver) for optimized thumb + $url = $myFile->imageoptim(); + + // echo the url as image + // https://getkirby.com/docs/toolkit/api#brick + $img = brick('img') + ->attr('src', $url) + ->attr('alt', $myFile->filename()); + echo $img; + +?> +``` + +Changing width, height and/or fitting is also supported. Modifying dpr and quality setting as well. + +``` +imageoptim(400); + + // fit to 400px width and 300px height + $url = $myFile->imageoptim(400, 300); + + // crop to 800x600px dimension + $url = $myFile->imageoptim(800, 600, 'crop'); + + // fit to 400px width and 300px height at 2x dpr + $url = $myFile->imageoptim(400, 300, 'fit', 2); + + // fit to 400px width and 300px height at 2x dpr and 'high' quality + $url = $myFile->imageoptim(400, 300, 'fit', 2, 'high'); + +?> +``` + +## Disclaimer + +This plugin is provided "as is" with no guarantee. Use it at your own risk and always test it yourself before using it in a production environment. If you find any issues, please [create a new issue](https://github.com/bnomei/kirby-imageoptim/issues/new). + +## License + +[MIT](https://opensource.org/licenses/MIT) + +It is discouraged to use this plugin in any project that promotes racism, sexism, homophobia, animal abuse, violence or any other form of hate speech. diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..805150d --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..1a28124 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2016 Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..7a91153 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($vendorDir . '/imageoptim/imageoptim/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..5efbe4c --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,52 @@ += 50600 && !defined('HHVM_VERSION'); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitdb60f64c7c86327cd38317daa842af05::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..80f08c7 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,31 @@ + + array ( + 'ImageOptim\\' => 11, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'ImageOptim\\' => + array ( + 0 => __DIR__ . '/..' . '/imageoptim/imageoptim/src', + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitdb60f64c7c86327cd38317daa842af05::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitdb60f64c7c86327cd38317daa842af05::$prefixDirsPsr4; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..7d60f93 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,51 @@ +[ + { + "name": "imageoptim/imageoptim", + "version": "1.3.1", + "version_normalized": "1.3.1.0", + "source": { + "type": "git", + "url": "https://github.com/ImageOptim/php-imageoptim-api.git", + "reference": "b73eb5d6747fc181de86b2de50fb158dff463618" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ImageOptim/php-imageoptim-api/zipball/b73eb5d6747fc181de86b2de50fb158dff463618", + "reference": "b73eb5d6747fc181de86b2de50fb158dff463618", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.3" + }, + "time": "2017-01-09 23:58:20", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "ImageOptim\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Kornel", + "email": "kornel@imageoptim.com" + } + ], + "description": "ImageOptim API for PHP", + "homepage": "https://imageoptim.com/api", + "keywords": [ + "image", + "optimize", + "performance", + "resize", + "scale" + ] + } +] diff --git a/vendor/imageoptim/imageoptim/.gitignore b/vendor/imageoptim/imageoptim/.gitignore new file mode 100644 index 0000000..d8a7996 --- /dev/null +++ b/vendor/imageoptim/imageoptim/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor/ diff --git a/vendor/imageoptim/imageoptim/ImageOptim.png b/vendor/imageoptim/imageoptim/ImageOptim.png new file mode 100644 index 0000000..3b4555b Binary files /dev/null and b/vendor/imageoptim/imageoptim/ImageOptim.png differ diff --git a/vendor/imageoptim/imageoptim/README.md b/vendor/imageoptim/imageoptim/README.md new file mode 100644 index 0000000..d28f5b8 --- /dev/null +++ b/vendor/imageoptim/imageoptim/README.md @@ -0,0 +1,153 @@ +# ImageOptim API PHP client + +This library allows you to resize and optimize images using ImageOptim API. + +ImageOptim offers [advanced compression, high-DPI/responsive image mode, and color profile support](https://imageoptim.com/features.html) that are much better than PHP's built-in image resizing functions. + +## Installation + +The easiest is to use [PHP Composer](https://getcomposer.org/): + +```sh +composer require imageoptim/imageoptim +``` + +If you don't use Composer, then `require` or autoload files from the `src` directory. + +## Usage + +First, [register to use the API](https://im2.io/register). + +```php +imageFromURL('http://example.com/photo.jpg') // read this image + ->resize(160, 100, 'crop') // optional: resize to a thumbnail + ->dpr(2) // optional: double number of pixels for high-resolution "Retina" displays + ->getBytes(); // perform these operations and return the image data as binary string + +file_put_contents("images/photo_optimized.jpg", $imageData); +``` + +There's a longer example at the end of the readme. + +### Methods + +#### `API($username)` constructor + + new ImageOptim\API("your api username goes here"); + +Creates new instance of the API. You need to give it [your username](https://im2.io/api/username). + +#### `imageFromPath($filePath)` — local source image + +Creates a new request that will [upload](https://im2.io/api/upload) the image to the API, and then resize and optimize it. The upload method is necessary for optimizing files that are not on the web (e.g. `localhost`, files in `/tmp`). + +For images that have a public URLs (e.g. published on a website) it's faster to use the URL method instead: + +#### `imageFromURL($url)` — remote source image + +Creates a new request that will read the image from the given public URL, and then resize and optimize it. + +Please pass full absolute URL to images on your website. + +Ideally you should supply source image at very high quality (e.g. JPEG saved at 99%), so that ImageOptim can adjust quality itself. If source images you provide are already saved at low quality, ImageOptim will not be able to make them look better. + +#### `resize($width, $height = optional, $fit = optional)` — desired dimensions + +* `resize($width)` — sets maximum width for the image, so it'll be resized to this width. If the image is smaller than this, it won't be enlarged. + +* `resize($width, $height)` — same as above, but image will also have height same or smaller. Aspect ratio is always preserved. + +* `resize($width, $height, 'crop')` — resizes and crops image exactly to these dimensions. + +If you don't call `resize()`, then the original image size will be preserved. + +[See options reference](https://im2.io/api/post#options) for more resizing options. + +#### `dpr($x)` — pixel doubling for responsive images (HTML `srcset`) + +The default is `dpr(1)`, which means image is for regular displays, and `resize()` does the obvious thing you'd expect. + +If you set `dpr(2)` then pixel width and height of the image will be *doubled* to match density of "2x" displays. This is better than `resize($width*2)`, because it also adjusts sharpness and image quality to be optimal for high-DPI displays. + +[See options reference](https://im2.io/api/post#opt-2x) for explanation how DPR works. + +#### `quality($preset)` — if you need even smaller or extra sharp images + +Quality is set as a string, and can be `low`, `medium` or `high`. The default is `medium` and should be good enough for most cases. + +#### `getBytes()` — get the resized image + +Makes request to ImageOptim API and returns optimized image as a string. You should save that to your server's disk. + +ImageOptim performs optimizations that sometimes may take a few seconds, so instead of converting images on the fly on every request, you should convert them once and keep them. + +#### `apiURL()` — debug or use another HTTPS client + +Returns string with URL to `https://im2.io/…` that is equivalent of the options set. You can open this URL in your web browser to get more information about it. Or you can [make a `POST` request to it](https://im2.io/api/post#making-the-request) to download the image yourself, if you don't want to use the `getBytes()` method. + +### Error handling + +All methods throw on error. You can expect the following exception subclasses: + +* `ImageOptim\InvalidArgumentException` means arguments to functions are incorrect and you need to fix your code. +* `ImageOptim\NetworkException` is thrown when there is problem comunicating with the API. You can retry the request. +* `ImageOptim\NotFoundException` is thrown when URL given to `imageFromURL()` returned 404. Make sure paths and urlencoding are correct. [More](https://im2.io/api/post#response). +* `ImageOptim\OriginServerException` is thrown when URL given to `imageFromURL()` returned 4xx or 5xx error. Make sure your server allows access to the file. + +If you're writing a script that processes a large number of images in one go, don't launch it from a web browser, as it will likely time out. It's best to launch such scripts via CLI (e.g. via SSH). + +### Help and info + +See [imageoptim.com/api](https://imageoptim.com/api) for documentation and contact info. I'm happy to help! + +### Example + +This is a script that optimizes an image. Such script usually would be ran when a new image is uploaded to the server. You don't need to run any PHP code to *serve* optimized images. + +The API operates on a single image at a time. When you want to generate multiple image sizes/thumbnails, repeat the whole procedure for each image at each size. + +```php +imageFromURL('http://example.com/photo.jpg'); + +// You set various settings on this object (or none to get the defaults). +$imageParams->quality('low'); +$imageParams->resize(1024); + +// Next, to start the optimizations and get the optimized image, call: +$imageData = $imageParams->getBytes(); + +/* + the getBytes() call may take a while to run, so it's intended to be + called only once per image (e.g. only when a new image is uploaded + to your server). If you'd like to "lazily" optimize arbitrary images + on-the-fly when they're requested, there is a better API for that: + https://im2.io/api/get +*/ + +// Save the image data somewhere on the server, e.g. +file_put_contents("images/photo_optimized.jpg", $imageData); + +// Note that this script only prepares a static image file +// (in this example in images/photo_optimized.jpg), +// and does not serve it to the browser. Once the optimized +// image is saved to disk you should serve it normally +// as you'd do with any regular image file. + +``` + diff --git a/vendor/imageoptim/imageoptim/composer.json b/vendor/imageoptim/imageoptim/composer.json new file mode 100644 index 0000000..00ca812 --- /dev/null +++ b/vendor/imageoptim/imageoptim/composer.json @@ -0,0 +1,25 @@ +{ + "name": "imageoptim/imageoptim", + "description": "ImageOptim API for PHP", + "minimum-stability": "stable", + "license": "BSD-2-Clause", + "authors": [ + { + "name": "Kornel", + "email": "kornel@imageoptim.com" + } + ], + "homepage": "https://imageoptim.com/api", + "keywords": ["image","resize","optimize","scale","performance"], + "autoload": { + "psr-4" : { + "ImageOptim\\" : "src" + } + }, + "require": { + "php" : "^5.4 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.3" + } +} diff --git a/vendor/imageoptim/imageoptim/examples/optimize_directory.php b/vendor/imageoptim/imageoptim/examples/optimize_directory.php new file mode 100755 index 0000000..354c1eb --- /dev/null +++ b/vendor/imageoptim/imageoptim/examples/optimize_directory.php @@ -0,0 +1,164 @@ +#!/usr/bin/env php + /www/example.com/optimized/hello-640.png\n\n"; + echo "If you have questions, ask support@imageoptim.com\n"; +} + +if (count($_SERVER['argv']) < 4) { // the arg 0 is the command name + usage(); + exit(1); +} + +$argn = 1; +$apiUsername = $_SERVER['argv'][$argn++]; +if (!$apiUsername || ctype_digit($apiUsername) || file_exists($apiUsername)) { + echo "The first argument (". escapeshellarg($apiUsername) . ") must be an ImageOptim API username.\n"; + echo "Get your username from https://imageoptim.com/api/register\n"; + exit(1); +} + +$width = null; +if (count($_SERVER['argv']) > 4 && ctype_digit($_SERVER['argv'][$argn])) { + $width = $_SERVER['argv'][$argn++]; +} + +$sourceDir = $_SERVER['argv'][$argn++]; +if (!is_dir($sourceDir)) { + echo "ERROR: ", $sourceDir, " does not exist or is not a directory.\n\n"; + usage(); + exit(1); +} + +$destDir = $_SERVER['argv'][$argn++]; +if (!is_dir($destDir)) { + if (is_dir(dirname($destDir))) { + if (!mkdir($destDir)) { + echo "ERROR: can't create ", $destDir, ". Please create this directory first.\n"; + exit(1); + } + } else { + echo "ERROR: ", $destDir, " does not exist or is not a directory.\n\n"; + usage(); + exit(1); + } +} + +// Clears symlinks from paths, makes them absolute and comparable +$sourceDir = realpath($sourceDir); +$destDir = realpath($destDir); + +try { + $api = new ImageOptim\API($apiUsername); + + // This is a fancy way of getting a list of all files in a directory + $items = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST, + RecursiveIteratorIterator::CATCH_GET_CHILD); + + $nonImage = []; + $skipped = 0; + $done = 0; + + foreach ($items as $item) { + if ($item->isDir()) continue; + $filename = $item->getFilename(); + if (!preg_match('/\.(png|jpe?g|gif|svgz?|bmp|tiff?)/i', $filename)) { + $nonImage[] = $filename; + continue; + } + + $sourcePath = $item->getPathname(); + $destRelPath = substr($sourcePath, strlen($sourceDir)); + $destPath = $destDir . $destRelPath; + + // Append .min extension if source and destination are the same + if ($destPath === $sourcePath && false === strpos($destRelPath, '.min.')) { + $destRelPath = preg_replace('/\.[^.]+$/', '.min$0', $destRelPath); + $destPath = $destDir . $destRelPath; + } + + echo substr($destRelPath,1),"... "; + + if (file_exists($destPath) && filemtime($destPath) > filemtime($sourcePath)) { + echo " already exists (skipped)\n"; + $skipped++; + continue; + } + + // The process preserves directory structure, so it needs to create dirs + $destSubdir = dirname($destPath); + if (!is_dir($destSubdir)) { + if (!mkdir($destSubdir, 0777, true)) { + echo "error: unable to create", $destSubdir,"\n"; + continue; + } + } + + $apiRequest = $api->imageFromPath($sourcePath); + if ($width) { + // You could add more options here + $apiRequest->resize($width); + } + $data = $apiRequest->getBytes(); + if (!file_put_contents($destPath, $data)) { + echo "ERROR: unable to save file $destPath\n"; + break; + } + + $inSize = filesize($sourcePath); + $outSize = strlen($data); + echo "ok (", ($inSize > $outSize ? "$inSize -> $outSize bytes" : "already optimized"), ")\n"; + $done++; + } + + if (count($nonImage)) { + echo "Skipped ", count($nonImage), " non-image file(s) ", implode(', ', array_slice($nonImage, 0, 50)), "\n"; + $nonImage = []; + } + + if ($skipped) { + echo "\nSkipped $skipped alredy-existing file(s) in $destDir"; + } + echo "\nImageOptim API processed $done file(s)\n"; + +} catch(\ImageOptim\AccessDeniedException $e) { + echo "ERROR\n\n"; + echo "Please got to https://imageoptim.com/api/register\n"; + echo "get your API username, and replace '$apiUsername' with\n"; + echo "your new registered API username.\n\n"; + echo $e; + exit(1); +} catch(\Exception $e) { + echo "ERROR\n\n"; + echo $e; + exit(1); +} diff --git a/vendor/imageoptim/imageoptim/phpunit.xml b/vendor/imageoptim/imageoptim/phpunit.xml new file mode 100644 index 0000000..093e2d2 --- /dev/null +++ b/vendor/imageoptim/imageoptim/phpunit.xml @@ -0,0 +1,13 @@ + + + + + test + + + + diff --git a/vendor/imageoptim/imageoptim/src/API.php b/vendor/imageoptim/imageoptim/src/API.php new file mode 100644 index 0000000..46e51ac --- /dev/null +++ b/vendor/imageoptim/imageoptim/src/API.php @@ -0,0 +1,22 @@ +username = $username; + } + + function imageFromURL($url) { + return new URLRequest($this->username, $url); + } + + function imageFromPath($file) { + return new FileRequest($this->username, $file); + } +} diff --git a/vendor/imageoptim/imageoptim/src/APIException.php b/vendor/imageoptim/imageoptim/src/APIException.php new file mode 100644 index 0000000..493a68b --- /dev/null +++ b/vendor/imageoptim/imageoptim/src/APIException.php @@ -0,0 +1,7 @@ +path = $path; + } + + function apiURL() { + return parent::apiURL(); + } + + function getBytes() { + $fileData = @file_get_contents($this->path); + if (!$fileData) { + throw new APIException("Unable to read {$this->path}"); + } + + $contentHash = md5($this->path); + $boundary = "XXX$contentHash"; + $nameEscaped = addslashes(basename($this->path)); + + $url = $this->apiURL(); + $content = "--$boundary\r\n" . + "Content-Disposition: form-data; name=\"file\"; filename=\"{$nameEscaped}\"\r\n" . + "Content-Type: application/octet-stream\r\n" . + "Content-Transfer-Encoding: binary\r\n" . + "\r\n$fileData\r\n--$boundary--"; + + return $this->getBytesWithOptions([ + 'header' => "Content-Length: " . strlen($content) . "\r\n" . + "Content-MD5: $contentHash\r\n" . + "Content-Type: multipart/form-data, boundary=$boundary\r\n", + 'content' => $content, + ], $this->path); + } +} diff --git a/vendor/imageoptim/imageoptim/src/InvalidArgumentException.php b/vendor/imageoptim/imageoptim/src/InvalidArgumentException.php new file mode 100644 index 0000000..c5d0ba1 --- /dev/null +++ b/vendor/imageoptim/imageoptim/src/InvalidArgumentException.php @@ -0,0 +1,7 @@ +username = $username; + } + + public function resize($width, $height_or_fit = null, $fit = null) { + if (!is_numeric($width)) { + throw new InvalidArgumentException("Width is not a number: $width"); + } + + $width = intval($width); + if (null === $height_or_fit) { + $height = null; + } else if (is_numeric($height_or_fit)) { + $height = intval($height_or_fit); + } else if ($fit) { + throw new InvalidArgumentException("Height is not a number: $height_or_fit"); + } else { + $fit = $height_or_fit; + $height = null; + } + + if ($width < 1 || $width > 10000) { + throw new InvalidArgumentException("Width is out of allowed range: $width"); + } + if ($height !== null && ($height < 1 || $height > 10000)) { + throw new InvalidArgumentException("Height is out of allowed range: $height"); + } + + $allowedFitOptions = ['fit', 'crop', 'scale-down', 'pad']; + if (null !== $fit && !in_array($fit, $allowedFitOptions)) { + throw new InvalidArgumentException("Fit is not one of ".implode(', ',$allowedFitOptions).". Got: $fit"); + } + + if (!$height && ('pad' === $fit || 'crop' === $fit)) { + throw new InvalidArgumentException("Height is required for '$fit' scaling mode\nPlease specify height or use 'fit' scaling mode to allow flexible height"); + } + + $this->width = $width; + $this->height = $height; + $this->fit = $fit; + + + return $this; + } + + public function timeout($timeout) { + if (!is_numeric($timeout) || $timeout <= 0) { + throw new InvalidArgumentException("Timeout not a positive number: $timeout"); + } + $this->timeout = $timeout; + + return $this; + } + + public function bgcolor($background_color) { + if ('transparent' === $background_color || false === $background_color || null === $background_color) { + $this->bgcolor = null; + } else if (is_string($background_color) && preg_match('/^#?([0-9a-f]+)$/i', $background_color, $m)) { + $this->bgcolor = $m[1]; + } else { + throw new InvalidArgumentException("Background color must be a hex string (e.g. AABBCC). Got: $background_color"); + } + return $this; + } + + public function dpr($dpr) { + if (!preg_match('/^\d[.\d]*(x)?$/', $dpr, $m)) { + throw new InvalidArgumentException("DPR should be 1x, 2x or 3x. Got: $dpr"); + } + $this->dpr = $dpr . (empty($m[1]) ? 'x' : ''); + + return $this; + } + + public function quality($quality) { + $allowedQualityOptions = ['low', 'medium', 'high', 'lossless']; + if (!in_array($quality, $allowedQualityOptions)) { + throw new InvalidArgumentException("Quality is not one of ".implode(', ',$allowedQualityOptions).". Got: $quality"); + } + $this->quality = $quality; + + return $this; + } + + function optimize() { + // always. This is here to make order of calls flexible + return $this; + } + + protected function apiURL() { + $options = []; + if ($this->width) { + $size = $this->width; + if ($this->height) { + $size .= 'x' . $this->height; + } + $options[] = $size; + if ($this->fit) $options[] = $this->fit; + } else { + $options[] = 'full'; + } + if ($this->dpr) $options[] = $this->dpr; + if ($this->quality) $options[] = 'quality=' . $this->quality; + if ($this->timeout) $options[] = 'timeout=' . $this->timeout; + if ($this->bgcolor) $options[] = 'bgcolor=' . $this->bgcolor; + + return self::BASE_URL . '/' . rawurlencode($this->username) . '/' . implode(',', $options); + } + + protected function getBytesWithOptions(array $options, $sourceURL) { + $url = $this->apiURL(); + $options['timeout'] = max(30, $this->timeout); + $options['ignore_errors'] = true; + $options['method'] = 'POST'; + $options['header'] .= "Accept: image/*,application/im2+json\r\n" . + "User-Agent: ImageOptim-php/1.1 PHP/" . phpversion(); + + $stream = @fopen($url, 'r', false, stream_context_create(['http'=>$options])); + + if (!$stream) { + $err = error_get_last(); + throw new NetworkException("Can't send HTTPS request to: $url\n" . ($err ? $err['message'] : '')); + } + + $res = @stream_get_contents($stream); + if (!$res) { + $err = error_get_last(); + fclose($stream); + throw new NetworkException("Error reading HTTPS response from: $url\n" . ($err ? $err['message'] : '')); + } + + $meta = @stream_get_meta_data($stream); + if (!$meta) { + $err = error_get_last(); + fclose($stream); + throw new NetworkException("Error reading HTTPS response from: $url\n" . ($err ? $err['message'] : '')); + } + fclose($stream); + + if (!$meta || !isset($meta['wrapper_data'], $meta['wrapper_data'][0])) { + throw new NetworkException("Unable to read headers from HTTP request to: $url"); + } + if (!empty($meta['timed_out'])) { + throw new NetworkException("Request timed out: $url", 504); + } + + if (!preg_match('/HTTP\/[\d.]+ (\d+) (.*)/', $meta['wrapper_data'][0], $status)) { + throw new NetworkException("Unexpected response: ". $meta['wrapper_data'][0]); + } + + $status = intval($status[1]); + $errorMessage = $status[2]; + + if ($res && preg_grep('/content-type:\s*application\/im2\+json/i', $meta['wrapper_data'])) { + $json = @json_decode($res); + if ($json) { + if (isset($json->status)) { + $status = $json->status; + } + if (isset($json->error)) { + $errorMessage = $json->error; + } + if (isset($json->code) && $json->code === 'IM2ACCOUNT') { + throw new AccessDeniedException($errorMessage, $status); + } + } + } + + if ($status >= 500) { + throw new APIException($errorMessage, $status); + } + if ($status == 404) { + throw new NotFoundException("Could not find the image: {$sourceURL}", $status); + } + if ($status == 403) { + throw new OriginServerException("Origin server denied access to {$sourceURL}", $status); + } + if ($status >= 400) { + throw new InvalidArgumentException($errorMessage, $status); + } + + return $res; + } +} diff --git a/vendor/imageoptim/imageoptim/src/URLRequest.php b/vendor/imageoptim/imageoptim/src/URLRequest.php new file mode 100644 index 0000000..2b825ea --- /dev/null +++ b/vendor/imageoptim/imageoptim/src/URLRequest.php @@ -0,0 +1,26 @@ +url = $url; + } + + function apiURL() { + return parent::apiURL() . '/' . rawurlencode($this->url); + } + + function getBytes() { + return $this->getBytesWithOptions(['header' => ""], $this->url); + } +} diff --git a/vendor/imageoptim/imageoptim/test/BasicTest.php b/vendor/imageoptim/imageoptim/test/BasicTest.php new file mode 100644 index 0000000..34b0960 --- /dev/null +++ b/vendor/imageoptim/imageoptim/test/BasicTest.php @@ -0,0 +1,125 @@ +api = new ImageOptim\API("testtest"); + } + + /** + * @expectedException \ImageOptim\InvalidArgumentException + */ + public function testRequiresUsername1() { + new ImageOptim\API([]); + } + + /** + * @expectedException \ImageOptim\InvalidArgumentException + * @expectedExceptionMessage username + */ + public function testRequiresUsername2() { + new ImageOptim\API(null); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage URL + */ + public function testNeedsURL() { + $this->api->imageFromURL('local/path.png'); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage could not be found + */ + public function testNeedsPath() { + $this->api->imageFromPath('http://nope/path.png'); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Width + */ + public function testResizeWidth() { + $this->api->imageFromURL('http://example.com')->resize("bad"); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Height + */ + public function testResizeBadHeight() { + $this->api->imageFromURL('http://example.com')->resize(320, "bad", "crop"); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Height + */ + public function testResizeNegativeHeight() { + $this->api->imageFromURL('http://example.com')->resize(320, -1, "crop"); + } + + public function testResizeWithoutHeight() { + $this->api->imageFromURL('http://example.com')->resize(320, "fit"); + } + + public function testResizeWithHeight() { + $this->api->imageFromURL('http://example.com')->resize(320, 100, "crop"); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Fit + */ + public function testResizeInvalidKeyword() { + $this->api->imageFromURL('http://example.com')->resize(320, 100, "loose"); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Height + */ + public function testCropNeedsHeight() { + $this->api->imageFromURL('http://example.com')->resize(320, null, "crop"); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Height + */ + public function testPadNeedsHeight() { + $this->api->imageFromURL('http://example.com')->resize(320, null, "pad"); + } + + public function testEncodesURLIfNeeded() { + $example = 'http://example.com/%2F'; + $this->assertContains(rawurlencode($example), $this->api->imageFromURL($example)->apiURL()); + } + + public function testPad() { + $apiurl = $this->api->imageFromURL('http://example.com')->resize(10,15,'pad')->bgcolor('#FFffFF')->apiURL(); + + $this->assertInternalType('string', $apiurl); + $this->assertContains('10x15', $apiurl); + $this->assertContains('pad', $apiurl); + $this->assertContains('bgcolor=FFffFF', $apiurl); + } + + public function testChains() { + $c1 = $this->api->imageFromURL('http://example.com')->resize(1280)->optimize()->timeout(34) + ->quality('low')->resize(1280)->dpr('2x')->resize(1280, 300); + + $c2 = $this->api->imageFromURL('http://example.com')->optimize()->resize(1280)->resize(1280) + ->dpr(2)->timeout(34)->resize(1280, 300)->quality('low'); + + $this->assertInternalType('string', $c1->apiURL()); + $this->assertEquals($c1->apiURL(), $c2->apiURL()); + $this->assertContains('quality=low', $c2->apiURL()); + $this->assertContains('2x', $c2->apiURL()); + $this->assertContains('1280x300', $c1->apiURL()); + $this->assertContains('timeout=34', $c1->apiURL()); + $this->assertContains('/http%3A%2F%2Fexample.com', $c1->apiURL()); + } +} diff --git a/vendor/imageoptim/imageoptim/test/OnlineTest.php b/vendor/imageoptim/imageoptim/test/OnlineTest.php new file mode 100644 index 0000000..a215185 --- /dev/null +++ b/vendor/imageoptim/imageoptim/test/OnlineTest.php @@ -0,0 +1,49 @@ +api = new ImageOptim\API("gnbkrbjhzb"); + } + + public function testFullMonty() { + $imageData = $this->api->imageFromURL('http://example.com/image.png')->resize(160,100,'crop')->dpr('2x')->getBytes(); + + $gdimg = imagecreatefromstring($imageData); + $this->assertEquals(160*2, imagesx($gdimg)); + $this->assertEquals(100*2, imagesy($gdimg)); + } + + public function testUpload() { + $imageData = $this->api->imageFromPath(__dir__ . '/../ImageOptim.png')->resize(32)->getBytes(); + + $gdimg = imagecreatefromstring($imageData); + $this->assertEquals(32, imagesx($gdimg)); + $this->assertEquals(32, imagesy($gdimg)); + } + + /** + * @expectedException ImageOptim\AccessDeniedException + * @expectedExceptionCode 403 + */ + public function testBadKey() { + $api = new ImageOptim\API("zzzzzzzz"); + $api->imageFromURL('http://example.com/image.png')->dpr('2x')->getBytes(); + } + + /** + * @expectedException ImageOptim\OriginServerException + * @expectedExceptionCode 403 + */ + public function testGoodKeyUpstream403() { + $this->api->imageFromURL('https://im2.io/.htdeny')->dpr('2x')->getBytes(); + } + + /** + * @expectedException ImageOptim\NotFoundException + * @expectedExceptionCode 404 + */ + public function testUpstreamError() { + $this->api->imageFromURL('http://fail.example.com/nope')->getBytes(); + } + +}