Skip to content

Commit

Permalink
Hello
Browse files Browse the repository at this point in the history
  • Loading branch information
kornelski committed May 21, 2016
0 parents commit 93cefcc
Show file tree
Hide file tree
Showing 13 changed files with 486 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
composer.lock
vendor/
95 changes: 95 additions & 0 deletions README.md
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!
24 changes: 24 additions & 0 deletions composer.json
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"
}
}
13 changes: 13 additions & 0 deletions phpunit.xml
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>

18 changes: 18 additions & 0 deletions src/API.php
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);
}
}
7 changes: 7 additions & 0 deletions src/APIException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace ImageOptim;

class APIException extends \RuntimeException {

}
7 changes: 7 additions & 0 deletions src/AccessDeniedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace ImageOptim;

class AccessDeniedException extends \RuntimeException {

}
7 changes: 7 additions & 0 deletions src/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace ImageOptim;

class InvalidArgumentException extends \InvalidArgumentException {

}
7 changes: 7 additions & 0 deletions src/NetworkException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace ImageOptim;

class NetworkException extends \RuntimeException {

}
7 changes: 7 additions & 0 deletions src/NotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace ImageOptim;

class NotFoundException extends \RuntimeException {

}
174 changes: 174 additions & 0 deletions src/Request.php
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;
}
}
Loading

0 comments on commit 93cefcc

Please sign in to comment.