-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 93cefcc
Showing
13 changed files
with
486 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
composer.lock | ||
vendor/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# 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 | ||
<?php | ||
require "vendor/autoload.php"; // created by Composer | ||
|
||
$api = new ImageOptim\API("🔶your api username goes here🔶"); | ||
|
||
$imageData = $api->fromURL('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); | ||
``` | ||
|
||
### 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). | ||
|
||
#### `fromURL($url)` — source image | ||
|
||
Creates new request that will read image from the given 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 `fromURL()` returns 404. Make sure paths and urlencoding are correct. [More](https://im2.io/api/post#response). | ||
|
||
### Help and info | ||
|
||
See [imageoptim.com/api](https://imageoptim.com/api) for documentation and contact info. I'm happy to help! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"name": "imageoptim/imageoptim", | ||
"description": "ImageOptim API for PHP", | ||
"license": "BSD-2-Clause", | ||
"authors": [ | ||
{ | ||
"name": "Kornel", | ||
"email": "[email protected]" | ||
} | ||
], | ||
"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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="phpunit.xsd" | ||
backupGlobals="false" | ||
bootstrap="vendor/autoload.php" | ||
verbose="true"> | ||
<testsuites> | ||
<testsuite name="imageoptim"> | ||
<directory suffix="Test.php">test</directory> | ||
</testsuite> | ||
</testsuites> | ||
</phpunit> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
namespace ImageOptim; | ||
|
||
class API { | ||
private $username; | ||
|
||
function __construct($username) { | ||
if (empty($username) || !is_string($username)) { | ||
throw new InvalidArgumentException("First argument to ImageOptim\\API must be the username\nGet your username from https://im2.io/register\n"); | ||
} | ||
$this->username = $username; | ||
} | ||
|
||
function imageFromURL($url) { | ||
return new Request($this->username, $url); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace ImageOptim; | ||
|
||
class APIException extends \RuntimeException { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace ImageOptim; | ||
|
||
class AccessDeniedException extends \RuntimeException { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace ImageOptim; | ||
|
||
class InvalidArgumentException extends \InvalidArgumentException { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace ImageOptim; | ||
|
||
class NetworkException extends \RuntimeException { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace ImageOptim; | ||
|
||
class NotFoundException extends \RuntimeException { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
<?php | ||
|
||
namespace ImageOptim; | ||
|
||
class Request { | ||
const BASE_URL = 'https://im2.io'; | ||
|
||
private $username, $url; | ||
private $width, $height, $dpr, $fit, $quality, $timeout; | ||
|
||
function __construct($username, $url) { | ||
if (!$username) throw new InvalidArgumentException(); | ||
if (!$url) { | ||
throw new InvalidArgumentException("Image URL is required"); | ||
} | ||
if (!preg_match('/^https?:\/\//', $url)) { | ||
throw new InvalidArgumentException("The API requires absolute image URL (starting with http:// or https://). Got: $url"); | ||
} | ||
$this->username = $username; | ||
$this->url = $url; | ||
} | ||
|
||
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']; | ||
if (null !== $fit && !in_array($fit, $allowedFitOptions)) { | ||
throw new InvalidArgumentException("Fit is not one of ".implode(', ',$allowedFitOptions).". Got: $fit"); | ||
} | ||
|
||
$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 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; | ||
} | ||
|
||
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; | ||
|
||
$imageURL = $this->url; | ||
if (preg_match('/[\s%+]/', $imageURL)) { | ||
$imageURL = rawurlencode($imageURL); | ||
} | ||
|
||
return self::BASE_URL . '/' . rawurlencode($this->username) . '/' . implode(',', $options) . '/' . $imageURL; | ||
} | ||
|
||
function getBytes() { | ||
$url = $this->apiURL(); | ||
$stream = @fopen($url, 'r', false, stream_context_create([ | ||
'http' => [ | ||
'ignore_errors' => true, | ||
'method' => 'POST', | ||
'header' => "User-Agent: ImageOptim-php/1.0 PHP/" . phpversion(), | ||
'timeout' => max(30, $this->timeout), | ||
], | ||
])); | ||
|
||
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]); | ||
} | ||
|
||
$code = intval($status[1]); | ||
if ($code >= 500) { | ||
throw new APIException($status[2], $code); | ||
} | ||
if ($code == 404) { | ||
throw new NotFoundException("Could not find the image: {$this->url}", $code); | ||
} | ||
if ($code == 403) { | ||
throw new AccessDeniedException("API username was not accepted: {$this->username}", $code); | ||
} | ||
if ($code >= 400) { | ||
throw new InvalidArgumentException($status[2], $code); | ||
} | ||
|
||
return $res; | ||
} | ||
} |
Oops, something went wrong.