Skip to content

Commit 93cefcc

Browse files
committed
Hello
0 parents  commit 93cefcc

13 files changed

+486
-0
lines changed

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
composer.lock
2+
vendor/

Diff for: README.md

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
<?php
23+
require "vendor/autoload.php"; // created by Composer
24+
25+
$api = new ImageOptim\API("🔶your api username goes here🔶");
26+
27+
$imageData = $api->fromURL('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+
### Methods
36+
37+
#### `API($username)` constructor
38+
39+
new ImageOptim\API("your api username goes here");
40+
41+
Creates new instance of the API. You need to give it [your username](https://im2.io/api/username).
42+
43+
#### `fromURL($url)` — source image
44+
45+
Creates new request that will read image from the given URL, and then resize and optimize it.
46+
47+
Please pass full absolute URL to images on your website.
48+
49+
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.
50+
51+
#### `resize($width, $height = optional, $fit = optional)` — desired dimensions
52+
53+
* `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.
54+
55+
* `resize($width, $height)` — same as above, but image will also have height same or smaller. Aspect ratio is always preserved.
56+
57+
* `resize($width, $height, 'crop')` — resizes and crops image exactly to these dimensions.
58+
59+
If you don't call `resize()`, then the original image size will be preserved.
60+
61+
[See options reference](https://im2.io/api/post#options) for more resizing options.
62+
63+
#### `dpr($x)` — pixel doubling for responsive images (HTML `srcset`)
64+
65+
The default is `dpr(1)`, which means image is for regular displays, and `resize()` does the obvious thing you'd expect.
66+
67+
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.
68+
69+
[See options reference](https://im2.io/api/post#opt-2x) for explanation how DPR works.
70+
71+
#### `quality($preset)` — if you need even smaller or extra sharp images
72+
73+
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.
74+
75+
#### `getBytes()` — get the resized image
76+
77+
Makes request to ImageOptim API and returns optimized image as a string. You should save that to your server's disk.
78+
79+
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.
80+
81+
#### `apiURL()` — debug or use another HTTPS client
82+
83+
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.
84+
85+
### Error handling
86+
87+
All methods throw on error. You can expect the following exception subclasses:
88+
89+
* `ImageOptim\InvalidArgumentException` means arguments to functions are incorrect and you need to fix your code.
90+
* `ImageOptim\NetworkException` is thrown when there is problem comunicating with the API. You can retry the request.
91+
* `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).
92+
93+
### Help and info
94+
95+
See [imageoptim.com/api](https://imageoptim.com/api) for documentation and contact info. I'm happy to help!

Diff for: composer.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "imageoptim/imageoptim",
3+
"description": "ImageOptim API for PHP",
4+
"license": "BSD-2-Clause",
5+
"authors": [
6+
{
7+
"name": "Kornel",
8+
"email": "kornel@imageoptim.com"
9+
}
10+
],
11+
"homepage": "https://imageoptim.com/api",
12+
"keywords": ["image","resize","optimize","scale","performance"],
13+
"autoload": {
14+
"psr-4" : {
15+
"ImageOptim\\" : "src"
16+
}
17+
},
18+
"require": {
19+
"php" : "^5.4 || ^7.0"
20+
},
21+
"require-dev": {
22+
"phpunit/phpunit": "^5.3"
23+
}
24+
}

Diff for: phpunit.xml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="phpunit.xsd"
4+
backupGlobals="false"
5+
bootstrap="vendor/autoload.php"
6+
verbose="true">
7+
<testsuites>
8+
<testsuite name="imageoptim">
9+
<directory suffix="Test.php">test</directory>
10+
</testsuite>
11+
</testsuites>
12+
</phpunit>
13+

Diff for: src/API.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace ImageOptim;
4+
5+
class API {
6+
private $username;
7+
8+
function __construct($username) {
9+
if (empty($username) || !is_string($username)) {
10+
throw new InvalidArgumentException("First argument to ImageOptim\\API must be the username\nGet your username from https://im2.io/register\n");
11+
}
12+
$this->username = $username;
13+
}
14+
15+
function imageFromURL($url) {
16+
return new Request($this->username, $url);
17+
}
18+
}

Diff for: src/APIException.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ImageOptim;
4+
5+
class APIException extends \RuntimeException {
6+
7+
}

Diff for: src/AccessDeniedException.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ImageOptim;
4+
5+
class AccessDeniedException extends \RuntimeException {
6+
7+
}

Diff for: src/InvalidArgumentException.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ImageOptim;
4+
5+
class InvalidArgumentException extends \InvalidArgumentException {
6+
7+
}

Diff for: src/NetworkException.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ImageOptim;
4+
5+
class NetworkException extends \RuntimeException {
6+
7+
}

Diff for: src/NotFoundException.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ImageOptim;
4+
5+
class NotFoundException extends \RuntimeException {
6+
7+
}

Diff for: src/Request.php

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
namespace ImageOptim;
4+
5+
class Request {
6+
const BASE_URL = 'https://im2.io';
7+
8+
private $username, $url;
9+
private $width, $height, $dpr, $fit, $quality, $timeout;
10+
11+
function __construct($username, $url) {
12+
if (!$username) throw new InvalidArgumentException();
13+
if (!$url) {
14+
throw new InvalidArgumentException("Image URL is required");
15+
}
16+
if (!preg_match('/^https?:\/\//', $url)) {
17+
throw new InvalidArgumentException("The API requires absolute image URL (starting with http:// or https://). Got: $url");
18+
}
19+
$this->username = $username;
20+
$this->url = $url;
21+
}
22+
23+
public function resize($width, $height_or_fit = null, $fit = null) {
24+
if (!is_numeric($width)) {
25+
throw new InvalidArgumentException("Width is not a number: $width");
26+
}
27+
28+
$width = intval($width);
29+
if (null === $height_or_fit) {
30+
$height = null;
31+
} else if (is_numeric($height_or_fit)) {
32+
$height = intval($height_or_fit);
33+
} else if ($fit) {
34+
throw new InvalidArgumentException("Height is not a number: $height_or_fit");
35+
} else {
36+
$fit = $height_or_fit;
37+
$height = null;
38+
}
39+
40+
if ($width < 1 || $width > 10000) {
41+
throw new InvalidArgumentException("Width is out of allowed range: $width");
42+
}
43+
if ($height !== null && ($height < 1 || $height > 10000)) {
44+
throw new InvalidArgumentException("Height is out of allowed range: $height");
45+
}
46+
47+
$allowedFitOptions = ['fit', 'crop', 'scale-down'];
48+
if (null !== $fit && !in_array($fit, $allowedFitOptions)) {
49+
throw new InvalidArgumentException("Fit is not one of ".implode(', ',$allowedFitOptions).". Got: $fit");
50+
}
51+
52+
$this->width = $width;
53+
$this->height = $height;
54+
$this->fit = $fit;
55+
56+
return $this;
57+
}
58+
59+
public function timeout($timeout) {
60+
if (!is_numeric($timeout) || $timeout <= 0) {
61+
throw new InvalidArgumentException("Timeout not a positive number: $timeout");
62+
}
63+
$this->timeout = $timeout;
64+
65+
return $this;
66+
}
67+
68+
public function dpr($dpr) {
69+
if (!preg_match('/^\d[.\d]*(x)?$/', $dpr, $m)) {
70+
throw new InvalidArgumentException("DPR should be 1x, 2x or 3x. Got: $dpr");
71+
}
72+
$this->dpr = $dpr . (empty($m[1]) ? 'x' : '');
73+
74+
return $this;
75+
}
76+
77+
public function quality($quality) {
78+
$allowedQualityOptions = ['low', 'medium', 'high', 'lossless'];
79+
if (!in_array($quality, $allowedQualityOptions)) {
80+
throw new InvalidArgumentException("Quality is not one of ".implode(', ',$allowedQualityOptions).". Got: $quality");
81+
}
82+
$this->quality = $quality;
83+
84+
return $this;
85+
}
86+
87+
function optimize() {
88+
// always. This is here to make order of calls flexible
89+
return $this;
90+
}
91+
92+
function apiURL() {
93+
$options = [];
94+
if ($this->width) {
95+
$size = $this->width;
96+
if ($this->height) {
97+
$size .= 'x' . $this->height;
98+
}
99+
$options[] = $size;
100+
if ($this->fit) $options[] = $this->fit;
101+
} else {
102+
$options[] = 'full';
103+
}
104+
if ($this->dpr) $options[] = $this->dpr;
105+
if ($this->quality) $options[] = 'quality=' . $this->quality;
106+
if ($this->timeout) $options[] = 'timeout=' . $this->timeout;
107+
108+
$imageURL = $this->url;
109+
if (preg_match('/[\s%+]/', $imageURL)) {
110+
$imageURL = rawurlencode($imageURL);
111+
}
112+
113+
return self::BASE_URL . '/' . rawurlencode($this->username) . '/' . implode(',', $options) . '/' . $imageURL;
114+
}
115+
116+
function getBytes() {
117+
$url = $this->apiURL();
118+
$stream = @fopen($url, 'r', false, stream_context_create([
119+
'http' => [
120+
'ignore_errors' => true,
121+
'method' => 'POST',
122+
'header' => "User-Agent: ImageOptim-php/1.0 PHP/" . phpversion(),
123+
'timeout' => max(30, $this->timeout),
124+
],
125+
]));
126+
127+
if (!$stream) {
128+
$err = error_get_last();
129+
throw new NetworkException("Can't send HTTPS request to: $url\n" . ($err ? $err['message'] : ''));
130+
}
131+
132+
$res = @stream_get_contents($stream);
133+
if (!$res) {
134+
$err = error_get_last();
135+
fclose($stream);
136+
throw new NetworkException("Error reading HTTPS response from: $url\n" . ($err ? $err['message'] : ''));
137+
}
138+
139+
$meta = @stream_get_meta_data($stream);
140+
if (!$meta) {
141+
$err = error_get_last();
142+
fclose($stream);
143+
throw new NetworkException("Error reading HTTPS response from: $url\n" . ($err ? $err['message'] : ''));
144+
}
145+
fclose($stream);
146+
147+
if (!$meta || !isset($meta['wrapper_data'], $meta['wrapper_data'][0])) {
148+
throw new NetworkException("Unable to read headers from HTTP request to: $url");
149+
}
150+
if (!empty($meta['timed_out'])) {
151+
throw new NetworkException("Request timed out: $url", 504);
152+
}
153+
154+
if (!preg_match('/HTTP\/[\d.]+ (\d+) (.*)/', $meta['wrapper_data'][0], $status)) {
155+
throw new NetworkException("Unexpected response: ". $meta['wrapper_data'][0]);
156+
}
157+
158+
$code = intval($status[1]);
159+
if ($code >= 500) {
160+
throw new APIException($status[2], $code);
161+
}
162+
if ($code == 404) {
163+
throw new NotFoundException("Could not find the image: {$this->url}", $code);
164+
}
165+
if ($code == 403) {
166+
throw new AccessDeniedException("API username was not accepted: {$this->username}", $code);
167+
}
168+
if ($code >= 400) {
169+
throw new InvalidArgumentException($status[2], $code);
170+
}
171+
172+
return $res;
173+
}
174+
}

0 commit comments

Comments
 (0)