├── .gitignore ├── ImageOptim.png ├── README.md ├── composer.json ├── examples └── optimize_directory.php ├── phpunit.xml ├── src ├── API.php ├── APIException.php ├── AccessDeniedException.php ├── FileRequest.php ├── InvalidArgumentException.php ├── NetworkException.php ├── NotFoundException.php ├── OriginServerException.php ├── Request.php └── URLRequest.php └── test ├── BasicTest.php └── OnlineTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | -------------------------------------------------------------------------------- /ImageOptim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImageOptim/php-imageoptim-api/b73eb5d6747fc181de86b2de50fb158dff463618/ImageOptim.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImageOptim API PHP client 2 | 3 | This library allows you to resize and optimize images using ImageOptim API. 4 | 5 | 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. 6 | 7 | ## Installation 8 | 9 | The easiest is to use [PHP Composer](https://getcomposer.org/): 10 | 11 | ```sh 12 | composer require imageoptim/imageoptim 13 | ``` 14 | 15 | If you don't use Composer, then `require` or autoload files from the `src` directory. 16 | 17 | ## Usage 18 | 19 | First, [register to use the API](https://im2.io/register). 20 | 21 | ```php 22 | imageFromURL('http://example.com/photo.jpg') // read this image 28 | ->resize(160, 100, 'crop') // optional: resize to a thumbnail 29 | ->dpr(2) // optional: double number of pixels for high-resolution "Retina" displays 30 | ->getBytes(); // perform these operations and return the image data as binary string 31 | 32 | file_put_contents("images/photo_optimized.jpg", $imageData); 33 | ``` 34 | 35 | There's a longer example at the end of the readme. 36 | 37 | ### Methods 38 | 39 | #### `API($username)` constructor 40 | 41 | new ImageOptim\API("your api username goes here"); 42 | 43 | Creates new instance of the API. You need to give it [your username](https://im2.io/api/username). 44 | 45 | #### `imageFromPath($filePath)` — local source image 46 | 47 | 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`). 48 | 49 | For images that have a public URLs (e.g. published on a website) it's faster to use the URL method instead: 50 | 51 | #### `imageFromURL($url)` — remote source image 52 | 53 | Creates a new request that will read the image from the given public URL, and then resize and optimize it. 54 | 55 | Please pass full absolute URL to images on your website. 56 | 57 | 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. 58 | 59 | #### `resize($width, $height = optional, $fit = optional)` — desired dimensions 60 | 61 | * `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. 62 | 63 | * `resize($width, $height)` — same as above, but image will also have height same or smaller. Aspect ratio is always preserved. 64 | 65 | * `resize($width, $height, 'crop')` — resizes and crops image exactly to these dimensions. 66 | 67 | If you don't call `resize()`, then the original image size will be preserved. 68 | 69 | [See options reference](https://im2.io/api/post#options) for more resizing options. 70 | 71 | #### `dpr($x)` — pixel doubling for responsive images (HTML `srcset`) 72 | 73 | The default is `dpr(1)`, which means image is for regular displays, and `resize()` does the obvious thing you'd expect. 74 | 75 | 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. 76 | 77 | [See options reference](https://im2.io/api/post#opt-2x) for explanation how DPR works. 78 | 79 | #### `quality($preset)` — if you need even smaller or extra sharp images 80 | 81 | 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. 82 | 83 | #### `getBytes()` — get the resized image 84 | 85 | Makes request to ImageOptim API and returns optimized image as a string. You should save that to your server's disk. 86 | 87 | 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. 88 | 89 | #### `apiURL()` — debug or use another HTTPS client 90 | 91 | 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. 92 | 93 | ### Error handling 94 | 95 | All methods throw on error. You can expect the following exception subclasses: 96 | 97 | * `ImageOptim\InvalidArgumentException` means arguments to functions are incorrect and you need to fix your code. 98 | * `ImageOptim\NetworkException` is thrown when there is problem comunicating with the API. You can retry the request. 99 | * `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). 100 | * `ImageOptim\OriginServerException` is thrown when URL given to `imageFromURL()` returned 4xx or 5xx error. Make sure your server allows access to the file. 101 | 102 | 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). 103 | 104 | ### Help and info 105 | 106 | See [imageoptim.com/api](https://imageoptim.com/api) for documentation and contact info. I'm happy to help! 107 | 108 | ### Example 109 | 110 | 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. 111 | 112 | 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. 113 | 114 | ```php 115 | imageFromURL('http://example.com/photo.jpg'); 127 | 128 | // You set various settings on this object (or none to get the defaults). 129 | $imageParams->quality('low'); 130 | $imageParams->resize(1024); 131 | 132 | // Next, to start the optimizations and get the optimized image, call: 133 | $imageData = $imageParams->getBytes(); 134 | 135 | /* 136 | the getBytes() call may take a while to run, so it's intended to be 137 | called only once per image (e.g. only when a new image is uploaded 138 | to your server). If you'd like to "lazily" optimize arbitrary images 139 | on-the-fly when they're requested, there is a better API for that: 140 | https://im2.io/api/get 141 | */ 142 | 143 | // Save the image data somewhere on the server, e.g. 144 | file_put_contents("images/photo_optimized.jpg", $imageData); 145 | 146 | // Note that this script only prepares a static image file 147 | // (in this example in images/photo_optimized.jpg), 148 | // and does not serve it to the browser. Once the optimized 149 | // image is saved to disk you should serve it normally 150 | // as you'd do with any regular image file. 151 | 152 | ``` 153 | 154 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imageoptim/imageoptim", 3 | "description": "ImageOptim API for PHP", 4 | "minimum-stability": "stable", 5 | "license": "BSD-2-Clause", 6 | "authors": [ 7 | { 8 | "name": "Kornel", 9 | "email": "kornel@imageoptim.com" 10 | } 11 | ], 12 | "homepage": "https://imageoptim.com/api", 13 | "keywords": ["image","resize","optimize","scale","performance"], 14 | "autoload": { 15 | "psr-4" : { 16 | "ImageOptim\\" : "src" 17 | } 18 | }, 19 | "require": { 20 | "php" : "^5.4 || ^7.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^5.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/optimize_directory.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | /www/example.com/optimized/hello-640.png\n\n"; 32 | echo "If you have questions, ask support@imageoptim.com\n"; 33 | } 34 | 35 | if (count($_SERVER['argv']) < 4) { // the arg 0 is the command name 36 | usage(); 37 | exit(1); 38 | } 39 | 40 | $argn = 1; 41 | $apiUsername = $_SERVER['argv'][$argn++]; 42 | if (!$apiUsername || ctype_digit($apiUsername) || file_exists($apiUsername)) { 43 | echo "The first argument (". escapeshellarg($apiUsername) . ") must be an ImageOptim API username.\n"; 44 | echo "Get your username from https://imageoptim.com/api/register\n"; 45 | exit(1); 46 | } 47 | 48 | $width = null; 49 | if (count($_SERVER['argv']) > 4 && ctype_digit($_SERVER['argv'][$argn])) { 50 | $width = $_SERVER['argv'][$argn++]; 51 | } 52 | 53 | $sourceDir = $_SERVER['argv'][$argn++]; 54 | if (!is_dir($sourceDir)) { 55 | echo "ERROR: ", $sourceDir, " does not exist or is not a directory.\n\n"; 56 | usage(); 57 | exit(1); 58 | } 59 | 60 | $destDir = $_SERVER['argv'][$argn++]; 61 | if (!is_dir($destDir)) { 62 | if (is_dir(dirname($destDir))) { 63 | if (!mkdir($destDir)) { 64 | echo "ERROR: can't create ", $destDir, ". Please create this directory first.\n"; 65 | exit(1); 66 | } 67 | } else { 68 | echo "ERROR: ", $destDir, " does not exist or is not a directory.\n\n"; 69 | usage(); 70 | exit(1); 71 | } 72 | } 73 | 74 | // Clears symlinks from paths, makes them absolute and comparable 75 | $sourceDir = realpath($sourceDir); 76 | $destDir = realpath($destDir); 77 | 78 | try { 79 | $api = new ImageOptim\API($apiUsername); 80 | 81 | // This is a fancy way of getting a list of all files in a directory 82 | $items = new RecursiveIteratorIterator( 83 | new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS), 84 | RecursiveIteratorIterator::SELF_FIRST, 85 | RecursiveIteratorIterator::CATCH_GET_CHILD); 86 | 87 | $nonImage = []; 88 | $skipped = 0; 89 | $done = 0; 90 | 91 | foreach ($items as $item) { 92 | if ($item->isDir()) continue; 93 | $filename = $item->getFilename(); 94 | if (!preg_match('/\.(png|jpe?g|gif|svgz?|bmp|tiff?)/i', $filename)) { 95 | $nonImage[] = $filename; 96 | continue; 97 | } 98 | 99 | $sourcePath = $item->getPathname(); 100 | $destRelPath = substr($sourcePath, strlen($sourceDir)); 101 | $destPath = $destDir . $destRelPath; 102 | 103 | // Append .min extension if source and destination are the same 104 | if ($destPath === $sourcePath && false === strpos($destRelPath, '.min.')) { 105 | $destRelPath = preg_replace('/\.[^.]+$/', '.min$0', $destRelPath); 106 | $destPath = $destDir . $destRelPath; 107 | } 108 | 109 | echo substr($destRelPath,1),"... "; 110 | 111 | if (file_exists($destPath) && filemtime($destPath) > filemtime($sourcePath)) { 112 | echo " already exists (skipped)\n"; 113 | $skipped++; 114 | continue; 115 | } 116 | 117 | // The process preserves directory structure, so it needs to create dirs 118 | $destSubdir = dirname($destPath); 119 | if (!is_dir($destSubdir)) { 120 | if (!mkdir($destSubdir, 0777, true)) { 121 | echo "error: unable to create", $destSubdir,"\n"; 122 | continue; 123 | } 124 | } 125 | 126 | $apiRequest = $api->imageFromPath($sourcePath); 127 | if ($width) { 128 | // You could add more options here 129 | $apiRequest->resize($width); 130 | } 131 | $data = $apiRequest->getBytes(); 132 | if (!file_put_contents($destPath, $data)) { 133 | echo "ERROR: unable to save file $destPath\n"; 134 | break; 135 | } 136 | 137 | $inSize = filesize($sourcePath); 138 | $outSize = strlen($data); 139 | echo "ok (", ($inSize > $outSize ? "$inSize -> $outSize bytes" : "already optimized"), ")\n"; 140 | $done++; 141 | } 142 | 143 | if (count($nonImage)) { 144 | echo "Skipped ", count($nonImage), " non-image file(s) ", implode(', ', array_slice($nonImage, 0, 50)), "\n"; 145 | $nonImage = []; 146 | } 147 | 148 | if ($skipped) { 149 | echo "\nSkipped $skipped alredy-existing file(s) in $destDir"; 150 | } 151 | echo "\nImageOptim API processed $done file(s)\n"; 152 | 153 | } catch(\ImageOptim\AccessDeniedException $e) { 154 | echo "ERROR\n\n"; 155 | echo "Please got to https://imageoptim.com/api/register\n"; 156 | echo "get your API username, and replace '$apiUsername' with\n"; 157 | echo "your new registered API username.\n\n"; 158 | echo $e; 159 | exit(1); 160 | } catch(\Exception $e) { 161 | echo "ERROR\n\n"; 162 | echo $e; 163 | exit(1); 164 | } 165 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | test 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/API.php: -------------------------------------------------------------------------------- 1 | username = $username; 13 | } 14 | 15 | function imageFromURL($url) { 16 | return new URLRequest($this->username, $url); 17 | } 18 | 19 | function imageFromPath($file) { 20 | return new FileRequest($this->username, $file); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/APIException.php: -------------------------------------------------------------------------------- 1 | path = $path; 18 | } 19 | 20 | function apiURL() { 21 | return parent::apiURL(); 22 | } 23 | 24 | function getBytes() { 25 | $fileData = @file_get_contents($this->path); 26 | if (!$fileData) { 27 | throw new APIException("Unable to read {$this->path}"); 28 | } 29 | 30 | $contentHash = md5($this->path); 31 | $boundary = "XXX$contentHash"; 32 | $nameEscaped = addslashes(basename($this->path)); 33 | 34 | $url = $this->apiURL(); 35 | $content = "--$boundary\r\n" . 36 | "Content-Disposition: form-data; name=\"file\"; filename=\"{$nameEscaped}\"\r\n" . 37 | "Content-Type: application/octet-stream\r\n" . 38 | "Content-Transfer-Encoding: binary\r\n" . 39 | "\r\n$fileData\r\n--$boundary--"; 40 | 41 | return $this->getBytesWithOptions([ 42 | 'header' => "Content-Length: " . strlen($content) . "\r\n" . 43 | "Content-MD5: $contentHash\r\n" . 44 | "Content-Type: multipart/form-data, boundary=$boundary\r\n", 45 | 'content' => $content, 46 | ], $this->path); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | username = $username; 14 | } 15 | 16 | public function resize($width, $height_or_fit = null, $fit = null) { 17 | if (!is_numeric($width)) { 18 | throw new InvalidArgumentException("Width is not a number: $width"); 19 | } 20 | 21 | $width = intval($width); 22 | if (null === $height_or_fit) { 23 | $height = null; 24 | } else if (is_numeric($height_or_fit)) { 25 | $height = intval($height_or_fit); 26 | } else if ($fit) { 27 | throw new InvalidArgumentException("Height is not a number: $height_or_fit"); 28 | } else { 29 | $fit = $height_or_fit; 30 | $height = null; 31 | } 32 | 33 | if ($width < 1 || $width > 10000) { 34 | throw new InvalidArgumentException("Width is out of allowed range: $width"); 35 | } 36 | if ($height !== null && ($height < 1 || $height > 10000)) { 37 | throw new InvalidArgumentException("Height is out of allowed range: $height"); 38 | } 39 | 40 | $allowedFitOptions = ['fit', 'crop', 'scale-down', 'pad']; 41 | if (null !== $fit && !in_array($fit, $allowedFitOptions)) { 42 | throw new InvalidArgumentException("Fit is not one of ".implode(', ',$allowedFitOptions).". Got: $fit"); 43 | } 44 | 45 | if (!$height && ('pad' === $fit || 'crop' === $fit)) { 46 | throw new InvalidArgumentException("Height is required for '$fit' scaling mode\nPlease specify height or use 'fit' scaling mode to allow flexible height"); 47 | } 48 | 49 | $this->width = $width; 50 | $this->height = $height; 51 | $this->fit = $fit; 52 | 53 | 54 | return $this; 55 | } 56 | 57 | public function timeout($timeout) { 58 | if (!is_numeric($timeout) || $timeout <= 0) { 59 | throw new InvalidArgumentException("Timeout not a positive number: $timeout"); 60 | } 61 | $this->timeout = $timeout; 62 | 63 | return $this; 64 | } 65 | 66 | public function bgcolor($background_color) { 67 | if ('transparent' === $background_color || false === $background_color || null === $background_color) { 68 | $this->bgcolor = null; 69 | } else if (is_string($background_color) && preg_match('/^#?([0-9a-f]+)$/i', $background_color, $m)) { 70 | $this->bgcolor = $m[1]; 71 | } else { 72 | throw new InvalidArgumentException("Background color must be a hex string (e.g. AABBCC). Got: $background_color"); 73 | } 74 | return $this; 75 | } 76 | 77 | public function dpr($dpr) { 78 | if (!preg_match('/^\d[.\d]*(x)?$/', $dpr, $m)) { 79 | throw new InvalidArgumentException("DPR should be 1x, 2x or 3x. Got: $dpr"); 80 | } 81 | $this->dpr = $dpr . (empty($m[1]) ? 'x' : ''); 82 | 83 | return $this; 84 | } 85 | 86 | public function quality($quality) { 87 | $allowedQualityOptions = ['low', 'medium', 'high', 'lossless']; 88 | if (!in_array($quality, $allowedQualityOptions)) { 89 | throw new InvalidArgumentException("Quality is not one of ".implode(', ',$allowedQualityOptions).". Got: $quality"); 90 | } 91 | $this->quality = $quality; 92 | 93 | return $this; 94 | } 95 | 96 | function optimize() { 97 | // always. This is here to make order of calls flexible 98 | return $this; 99 | } 100 | 101 | protected function apiURL() { 102 | $options = []; 103 | if ($this->width) { 104 | $size = $this->width; 105 | if ($this->height) { 106 | $size .= 'x' . $this->height; 107 | } 108 | $options[] = $size; 109 | if ($this->fit) $options[] = $this->fit; 110 | } else { 111 | $options[] = 'full'; 112 | } 113 | if ($this->dpr) $options[] = $this->dpr; 114 | if ($this->quality) $options[] = 'quality=' . $this->quality; 115 | if ($this->timeout) $options[] = 'timeout=' . $this->timeout; 116 | if ($this->bgcolor) $options[] = 'bgcolor=' . $this->bgcolor; 117 | 118 | return self::BASE_URL . '/' . rawurlencode($this->username) . '/' . implode(',', $options); 119 | } 120 | 121 | protected function getBytesWithOptions(array $options, $sourceURL) { 122 | $url = $this->apiURL(); 123 | $options['timeout'] = max(30, $this->timeout); 124 | $options['ignore_errors'] = true; 125 | $options['method'] = 'POST'; 126 | $options['header'] .= "Accept: image/*,application/im2+json\r\n" . 127 | "User-Agent: ImageOptim-php/1.1 PHP/" . phpversion(); 128 | 129 | $stream = @fopen($url, 'r', false, stream_context_create(['http'=>$options])); 130 | 131 | if (!$stream) { 132 | $err = error_get_last(); 133 | throw new NetworkException("Can't send HTTPS request to: $url\n" . ($err ? $err['message'] : '')); 134 | } 135 | 136 | $res = @stream_get_contents($stream); 137 | if (!$res) { 138 | $err = error_get_last(); 139 | fclose($stream); 140 | throw new NetworkException("Error reading HTTPS response from: $url\n" . ($err ? $err['message'] : '')); 141 | } 142 | 143 | $meta = @stream_get_meta_data($stream); 144 | if (!$meta) { 145 | $err = error_get_last(); 146 | fclose($stream); 147 | throw new NetworkException("Error reading HTTPS response from: $url\n" . ($err ? $err['message'] : '')); 148 | } 149 | fclose($stream); 150 | 151 | if (!$meta || !isset($meta['wrapper_data'], $meta['wrapper_data'][0])) { 152 | throw new NetworkException("Unable to read headers from HTTP request to: $url"); 153 | } 154 | if (!empty($meta['timed_out'])) { 155 | throw new NetworkException("Request timed out: $url", 504); 156 | } 157 | 158 | if (!preg_match('/HTTP\/[\d.]+ (\d+) (.*)/', $meta['wrapper_data'][0], $status)) { 159 | throw new NetworkException("Unexpected response: ". $meta['wrapper_data'][0]); 160 | } 161 | 162 | $status = intval($status[1]); 163 | $errorMessage = $status[2]; 164 | 165 | if ($res && preg_grep('/content-type:\s*application\/im2\+json/i', $meta['wrapper_data'])) { 166 | $json = @json_decode($res); 167 | if ($json) { 168 | if (isset($json->status)) { 169 | $status = $json->status; 170 | } 171 | if (isset($json->error)) { 172 | $errorMessage = $json->error; 173 | } 174 | if (isset($json->code) && $json->code === 'IM2ACCOUNT') { 175 | throw new AccessDeniedException($errorMessage, $status); 176 | } 177 | } 178 | } 179 | 180 | if ($status >= 500) { 181 | throw new APIException($errorMessage, $status); 182 | } 183 | if ($status == 404) { 184 | throw new NotFoundException("Could not find the image: {$sourceURL}", $status); 185 | } 186 | if ($status == 403) { 187 | throw new OriginServerException("Origin server denied access to {$sourceURL}", $status); 188 | } 189 | if ($status >= 400) { 190 | throw new InvalidArgumentException($errorMessage, $status); 191 | } 192 | 193 | return $res; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/URLRequest.php: -------------------------------------------------------------------------------- 1 | url = $url; 17 | } 18 | 19 | function apiURL() { 20 | return parent::apiURL() . '/' . rawurlencode($this->url); 21 | } 22 | 23 | function getBytes() { 24 | return $this->getBytesWithOptions(['header' => ""], $this->url); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/BasicTest.php: -------------------------------------------------------------------------------- 1 | api = new ImageOptim\API("testtest"); 7 | } 8 | 9 | /** 10 | * @expectedException \ImageOptim\InvalidArgumentException 11 | */ 12 | public function testRequiresUsername1() { 13 | new ImageOptim\API([]); 14 | } 15 | 16 | /** 17 | * @expectedException \ImageOptim\InvalidArgumentException 18 | * @expectedExceptionMessage username 19 | */ 20 | public function testRequiresUsername2() { 21 | new ImageOptim\API(null); 22 | } 23 | 24 | /** 25 | * @expectedException InvalidArgumentException 26 | * @expectedExceptionMessage URL 27 | */ 28 | public function testNeedsURL() { 29 | $this->api->imageFromURL('local/path.png'); 30 | } 31 | 32 | /** 33 | * @expectedException InvalidArgumentException 34 | * @expectedExceptionMessage could not be found 35 | */ 36 | public function testNeedsPath() { 37 | $this->api->imageFromPath('http://nope/path.png'); 38 | } 39 | 40 | /** 41 | * @expectedException InvalidArgumentException 42 | * @expectedExceptionMessage Width 43 | */ 44 | public function testResizeWidth() { 45 | $this->api->imageFromURL('http://example.com')->resize("bad"); 46 | } 47 | 48 | /** 49 | * @expectedException InvalidArgumentException 50 | * @expectedExceptionMessage Height 51 | */ 52 | public function testResizeBadHeight() { 53 | $this->api->imageFromURL('http://example.com')->resize(320, "bad", "crop"); 54 | } 55 | 56 | /** 57 | * @expectedException InvalidArgumentException 58 | * @expectedExceptionMessage Height 59 | */ 60 | public function testResizeNegativeHeight() { 61 | $this->api->imageFromURL('http://example.com')->resize(320, -1, "crop"); 62 | } 63 | 64 | public function testResizeWithoutHeight() { 65 | $this->api->imageFromURL('http://example.com')->resize(320, "fit"); 66 | } 67 | 68 | public function testResizeWithHeight() { 69 | $this->api->imageFromURL('http://example.com')->resize(320, 100, "crop"); 70 | } 71 | 72 | /** 73 | * @expectedException InvalidArgumentException 74 | * @expectedExceptionMessage Fit 75 | */ 76 | public function testResizeInvalidKeyword() { 77 | $this->api->imageFromURL('http://example.com')->resize(320, 100, "loose"); 78 | } 79 | 80 | /** 81 | * @expectedException InvalidArgumentException 82 | * @expectedExceptionMessage Height 83 | */ 84 | public function testCropNeedsHeight() { 85 | $this->api->imageFromURL('http://example.com')->resize(320, null, "crop"); 86 | } 87 | 88 | /** 89 | * @expectedException InvalidArgumentException 90 | * @expectedExceptionMessage Height 91 | */ 92 | public function testPadNeedsHeight() { 93 | $this->api->imageFromURL('http://example.com')->resize(320, null, "pad"); 94 | } 95 | 96 | public function testEncodesURLIfNeeded() { 97 | $example = 'http://example.com/%2F'; 98 | $this->assertContains(rawurlencode($example), $this->api->imageFromURL($example)->apiURL()); 99 | } 100 | 101 | public function testPad() { 102 | $apiurl = $this->api->imageFromURL('http://example.com')->resize(10,15,'pad')->bgcolor('#FFffFF')->apiURL(); 103 | 104 | $this->assertInternalType('string', $apiurl); 105 | $this->assertContains('10x15', $apiurl); 106 | $this->assertContains('pad', $apiurl); 107 | $this->assertContains('bgcolor=FFffFF', $apiurl); 108 | } 109 | 110 | public function testChains() { 111 | $c1 = $this->api->imageFromURL('http://example.com')->resize(1280)->optimize()->timeout(34) 112 | ->quality('low')->resize(1280)->dpr('2x')->resize(1280, 300); 113 | 114 | $c2 = $this->api->imageFromURL('http://example.com')->optimize()->resize(1280)->resize(1280) 115 | ->dpr(2)->timeout(34)->resize(1280, 300)->quality('low'); 116 | 117 | $this->assertInternalType('string', $c1->apiURL()); 118 | $this->assertEquals($c1->apiURL(), $c2->apiURL()); 119 | $this->assertContains('quality=low', $c2->apiURL()); 120 | $this->assertContains('2x', $c2->apiURL()); 121 | $this->assertContains('1280x300', $c1->apiURL()); 122 | $this->assertContains('timeout=34', $c1->apiURL()); 123 | $this->assertContains('/http%3A%2F%2Fexample.com', $c1->apiURL()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/OnlineTest.php: -------------------------------------------------------------------------------- 1 | api = new ImageOptim\API("gnbkrbjhzb"); 6 | } 7 | 8 | public function testFullMonty() { 9 | $imageData = $this->api->imageFromURL('http://example.com/image.png')->resize(160,100,'crop')->dpr('2x')->getBytes(); 10 | 11 | $gdimg = imagecreatefromstring($imageData); 12 | $this->assertEquals(160*2, imagesx($gdimg)); 13 | $this->assertEquals(100*2, imagesy($gdimg)); 14 | } 15 | 16 | public function testUpload() { 17 | $imageData = $this->api->imageFromPath(__dir__ . '/../ImageOptim.png')->resize(32)->getBytes(); 18 | 19 | $gdimg = imagecreatefromstring($imageData); 20 | $this->assertEquals(32, imagesx($gdimg)); 21 | $this->assertEquals(32, imagesy($gdimg)); 22 | } 23 | 24 | /** 25 | * @expectedException ImageOptim\AccessDeniedException 26 | * @expectedExceptionCode 403 27 | */ 28 | public function testBadKey() { 29 | $api = new ImageOptim\API("zzzzzzzz"); 30 | $api->imageFromURL('http://example.com/image.png')->dpr('2x')->getBytes(); 31 | } 32 | 33 | /** 34 | * @expectedException ImageOptim\OriginServerException 35 | * @expectedExceptionCode 403 36 | */ 37 | public function testGoodKeyUpstream403() { 38 | $this->api->imageFromURL('https://im2.io/.htdeny')->dpr('2x')->getBytes(); 39 | } 40 | 41 | /** 42 | * @expectedException ImageOptim\NotFoundException 43 | * @expectedExceptionCode 404 44 | */ 45 | public function testUpstreamError() { 46 | $this->api->imageFromURL('http://fail.example.com/nope')->getBytes(); 47 | } 48 | 49 | } 50 | --------------------------------------------------------------------------------