├── .gitignore ├── .travis.yml ├── Dockerfile ├── README.md ├── class-image-editor-vips.php ├── composer.json ├── composer.lock ├── docker-compose.yml ├── readme.txt └── vips-image-editor.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.0.0' 4 | script: 5 | - composer install --ignore-platform-reqs 6 | - zip -r vips-image-editor.zip . -x \*.git\* 7 | deploy: 8 | provider: releases 9 | api_key: '$GITHUB_API_KEY' 10 | file: "vips-image-editor.zip" 11 | skip_cleanup: true 12 | on: 13 | tags: true 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM wordpress 2 | RUN apt-get update &&\ 3 | apt-get -y install procps libvips libvips-dev 4 | 5 | RUN pecl install vips &&\ 6 | docker-php-ext-enable vips 7 | 8 | RUN echo "file_uploads = On\n" \ 9 | "memory_limit = 500M\n" \ 10 | "upload_max_filesize = 500M\n" \ 11 | "post_max_size = 500M\n" \ 12 | "max_execution_time = 600\n" \ 13 | > /usr/local/etc/php/conf.d/uploads.ini -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIPS Image Editor 2 | 3 | [![Build Status](https://travis-ci.com/CreunaFI/vips-image-editor.svg?branch=master)](https://travis-ci.com/CreunaFI/vips-image-editor) [![Packagist](https://img.shields.io/packagist/v/joppuyo/vips-image-editor.svg)](https://packagist.org/packages/joppuyo/vips-image-editor) 4 | 5 | High performance WordPress image processing with [VIPS](https://libvips.github.io/libvips/). 6 | 7 | ## Requirements 8 | 9 | * PHP 7 or later 10 | * vips package installed on your Linux system 11 | * vips PHP extension 12 | 13 | ## Installation 14 | 15 | 1. Install vips on your system. On Ubuntu, this can be done using `apt install libvips-dev` 16 | 2. Install vips extension using [pecl](https://pecl.php.net/), this can be done with command `pecl install vips` 17 | 3. Enable PHP extension, this is usually done by adding the line `extension=vips.so` to `php.ini` 18 | 4. Download the latest plugin version from the [releases tab](https://github.com/CreunaFI/vips-image-editor/releases) 19 | 5. Extract the plugin under `wp-content/plugins` 20 | 6. Enable the plugin in WordPress admin interface 21 | -------------------------------------------------------------------------------- /class-image-editor-vips.php: -------------------------------------------------------------------------------- 1 | file into new VIPS Resource. 68 | * 69 | * @since 3.5.0 70 | * 71 | * @return bool|WP_Error True if loaded successfully; WP_Error on failure. 72 | */ 73 | public function load() 74 | { 75 | if ($this->image) 76 | return true; 77 | 78 | if (!is_file($this->file)) { 79 | return new WP_Error('error_loading_image', __('File doesn’t exist?'), $this->file); 80 | } 81 | 82 | // Increase memory 83 | wp_raise_memory_limit('image'); 84 | 85 | try { 86 | $this->image = Jcupitt\Vips\Image::newFromFile($this->file); 87 | $this->update_size($this->image->width, $this->image->height); 88 | $this->mime_type = mime_content_type($this->file); 89 | 90 | return $this->set_quality(); 91 | } catch (Exception $exception) { 92 | return new WP_Error('image_load_error', __('Failed to load image.'), $exception); 93 | } 94 | } 95 | 96 | /** 97 | * Sets or updates current image size. 98 | * 99 | * @since 3.5.0 100 | * 101 | * @param int $width 102 | * @param int $height 103 | * @return true 104 | */ 105 | protected function update_size($width = false, $height = false) 106 | { 107 | if (!$width) { 108 | $width = $this->image->width; 109 | } 110 | 111 | if (!$height) { 112 | $height = $this->image->height; 113 | } 114 | 115 | return parent::update_size($width, $height); 116 | } 117 | 118 | /** 119 | * Resizes current image. 120 | * Wraps _resize, since _resize returns a VIPS Resource. 121 | * 122 | * At minimum, either a height or width must be provided. 123 | * If one of the two is set to null, the resize will 124 | * maintain aspect ratio according to the provided dimension. 125 | * 126 | * @since 3.5.0 127 | * 128 | * @param int|null $max_w Image width. 129 | * @param int|null $max_h Image height. 130 | * @param bool $crop 131 | * @return true|WP_Error 132 | */ 133 | public function resize($max_w, $max_h, $crop = false) 134 | { 135 | if (($this->size['width'] == $max_w) && ($this->size['height'] == $max_h)) { 136 | return true; 137 | } 138 | try { 139 | $resized = $this->_resize($max_w, $max_h, $crop); 140 | $this->image = $resized; 141 | return true; 142 | } catch (Exception $exception) { 143 | return new WP_Error('failed_to_crop', __('Failed to crop image'), $exception); 144 | } 145 | } 146 | 147 | /** 148 | * 149 | * @param int $max_w 150 | * @param int $max_h 151 | * @param bool|array $crop 152 | * @return resource|WP_Error 153 | */ 154 | protected function _resize($max_w, $max_h, $crop = false) 155 | { 156 | $dims = image_resize_dimensions($this->size['width'], $this->size['height'], $max_w, $max_h, $crop); 157 | if (!$dims) { 158 | return new WP_Error('error_getting_dimensions', __('Could not calculate resized image dimensions'), $this->file); 159 | } 160 | list($dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) = $dims; 161 | 162 | try { 163 | 164 | $resized = null; 165 | 166 | if (version_compare(vips_version(), '8.6.0', '>=') && apply_filters('vips_ie_thumbnail', true) === true) { 167 | // Vips thumbnail() is faster than resizing, let's use it if it's available 168 | 169 | $scale = max($this->size['width'] / $src_w, $this->size['height'] / $src_h); 170 | 171 | $target_w = ceil($dst_w * $scale); 172 | $target_h = ceil($dst_h * $scale); 173 | 174 | $resized = $this->image->thumbnail_image($target_w, [ 175 | 'height' => $target_h, 176 | ]); 177 | } else { 178 | $resized = $this->image->resize(max($dst_h / $src_h, $dst_w / $src_w)); 179 | } 180 | 181 | $scale = max($dst_w / $src_w, $dst_h / $src_h); 182 | 183 | $dst_x_scaled = floor($src_x * $scale); 184 | $dst_y_scaled = floor($src_y * $scale); 185 | 186 | $cropped = $resized->crop($dst_x_scaled, $dst_y_scaled, $dst_w, $dst_h); 187 | 188 | $this->update_size($dst_w, $dst_h); 189 | return $cropped; 190 | } catch (Exception $exception) { 191 | return new WP_Error('crop_error', __('Failed to crop image'), $exception); 192 | } 193 | } 194 | 195 | /** 196 | * Resize multiple images from a single source. 197 | * 198 | * @since 3.5.0 199 | * 200 | * @param array $sizes { 201 | * An array of image size arrays. Default sizes are 'small', 'medium', 'medium_large', 'large'. 202 | * 203 | * Either a height or width must be provided. 204 | * If one of the two is set to null, the resize will 205 | * maintain aspect ratio according to the provided dimension. 206 | * 207 | * @type array $size { 208 | * Array of height, width values, and whether to crop. 209 | * 210 | * @type int $width Image width. Optional if `$height` is specified. 211 | * @type int $height Image height. Optional if `$width` is specified. 212 | * @type bool $crop Optional. Whether to crop the image. Default false. 213 | * } 214 | * } 215 | * @return array An array of resized images' metadata by size. 216 | */ 217 | public function multi_resize($sizes) 218 | { 219 | $metadata = []; 220 | $orig_size = $this->size; 221 | 222 | foreach ($sizes as $size => $size_data) { 223 | if (!isset($size_data['width']) && !isset($size_data['height'])) { 224 | continue; 225 | } 226 | 227 | if (!isset($size_data['width'])) { 228 | $size_data['width'] = null; 229 | } 230 | if (!isset($size_data['height'])) { 231 | $size_data['height'] = null; 232 | } 233 | 234 | if (!isset($size_data['crop'])) { 235 | $size_data['crop'] = false; 236 | } 237 | 238 | $image = $this->_resize($size_data['width'], $size_data['height'], $size_data['crop']); 239 | $duplicate = (($orig_size['width'] == $size_data['width']) && ($orig_size['height'] == $size_data['height'])); 240 | 241 | if (!is_wp_error($image) && !$duplicate) { 242 | $resized = $this->_save($image); 243 | if (!is_wp_error($resized) && $resized) { 244 | unset($resized['path']); 245 | $metadata[$size] = $resized; 246 | } 247 | } 248 | 249 | $this->size = $orig_size; 250 | } 251 | 252 | return $metadata; 253 | } 254 | 255 | /** 256 | * Crops Image. 257 | * 258 | * @since 3.5.0 259 | * 260 | * @param int $src_x The start x position to crop from. 261 | * @param int $src_y The start y position to crop from. 262 | * @param int $src_w The width to crop. 263 | * @param int $src_h The height to crop. 264 | * @param int $dst_w Optional. The destination width. 265 | * @param int $dst_h Optional. The destination height. 266 | * @param bool $src_abs Optional. If the source crop points are absolute. 267 | * @return bool|WP_Error 268 | */ 269 | public function crop($src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false) 270 | { 271 | // If destination width/height isn't specified, use same as 272 | // width/height from source. 273 | if (!$dst_w) 274 | $dst_w = $src_w; 275 | if (!$dst_h) 276 | $dst_h = $src_h; 277 | 278 | if ($src_abs) { 279 | $src_w -= $src_x; 280 | $src_h -= $src_y; 281 | } 282 | 283 | try { 284 | $this->image = $this->image->crop($src_x, $src_y, $src_w, $src_h); 285 | $this->update_size(); 286 | return true; 287 | } catch (Exception $exception) { 288 | return new WP_Error('image_crop_error', __('Image crop failed.'), $exception); 289 | } 290 | } 291 | 292 | /** 293 | * Rotates current image counter-clockwise by $angle. 294 | * Ported from image-edit.php 295 | * 296 | * @since 3.5.0 297 | * 298 | * @param float $angle 299 | * @return true|WP_Error 300 | */ 301 | public function rotate($angle) 302 | { 303 | try { 304 | // Angle is counter clockwise because WordPress is strange 305 | $angle = -$angle; 306 | // Modulo magic 307 | $angle = (360 + ($angle % 360)) % 360; 308 | if ($angle === 90) { 309 | $this->image = $this->image->rot90(); 310 | } else if ($angle === 180) { 311 | $this->image = $this->image->rot180(); 312 | } else if ($angle === 270) { 313 | $this->image = $this->image->rot270(); 314 | } 315 | $this->update_size(); 316 | return true; 317 | } catch (Exception $exception) { 318 | return new WP_Error('image_rotate_error', __('Image rotate failed.'), $exception); 319 | } 320 | } 321 | 322 | /** 323 | * Flips current image. 324 | * 325 | * @since 3.5.0 326 | * 327 | * @param bool $horz Flip along Horizontal Axis 328 | * @param bool $vert Flip along Vertical Axis 329 | * @return true|WP_Error 330 | */ 331 | public function flip($horz, $vert) 332 | { 333 | try { 334 | if ($vert) { 335 | $this->image = $this->image->fliphor(); 336 | } 337 | if ($horz) { 338 | $this->image = $this->image->flipver(); 339 | } 340 | return true; 341 | } catch (Exception $exception) { 342 | return new WP_Error('image_flip_Error', __('Failed to flip image.'), $exception); 343 | } 344 | } 345 | 346 | /** 347 | * Saves current in-memory image to file. 348 | * 349 | * @since 3.5.0 350 | * 351 | * @param string|null $filename 352 | * @param string|null $mime_type 353 | * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string} 354 | */ 355 | public function save($filename = null, $mime_type = null) 356 | { 357 | $saved = $this->_save($this->image, $filename, $mime_type); 358 | 359 | if (!is_wp_error($saved)) { 360 | $this->file = $saved['path']; 361 | $this->mime_type = $saved['mime-type']; 362 | } 363 | 364 | return $saved; 365 | } 366 | 367 | /** 368 | * @param resource $image 369 | * @param string|null $filename 370 | * @param string|null $mime_type 371 | * @return WP_Error|array 372 | */ 373 | protected function _save($image, $filename = null, $mime_type = null) 374 | { 375 | list($filename, $extension, $mime_type) = $this->get_output_format($filename, $mime_type); 376 | 377 | if (!$filename) { 378 | $filename = $this->generate_filename(null, null, $extension); 379 | } 380 | 381 | $parameters = []; 382 | 383 | if ($mime_type === 'image/jpeg') { 384 | 385 | $interlace = apply_filters('vips_ie_interlace', false); 386 | $optimize_coding = apply_filters('vips_ie_optimize_coding', false); 387 | $trellis_quant = apply_filters('vips_ie_trellis_quant', false); 388 | $overshoot_deringing = apply_filters('vips_ie_overshoot_deringing', false); 389 | $optimize_scans = apply_filters('vips_ie_optimize_scans', false); 390 | 391 | $parameters = [ 392 | 'Q' => $this->get_quality(), 393 | 'interlace' => $interlace, 394 | 'trellis_quant' => $trellis_quant, 395 | 'overshoot_deringing' => $overshoot_deringing, 396 | 'optimize_scans' => $optimize_scans, 397 | ]; 398 | } 399 | 400 | try { 401 | 402 | // Check directory, vips does not create folders for us 403 | $directory = dirname($filename); 404 | if (!is_dir($directory)) { 405 | mkdir($directory); 406 | } 407 | 408 | if (is_wp_error($image)) { 409 | throw new Exception('Image is a WP_Error'); 410 | } 411 | 412 | $image->writeToFile($filename, $parameters); 413 | // Set correct file permissions 414 | $stat = stat(dirname($filename)); 415 | $perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits 416 | @ chmod($filename, $perms); 417 | /** 418 | * Filters the name of the saved image file. 419 | * 420 | * @since 2.6.0 421 | * 422 | * @param string $filename Name of the file. 423 | */ 424 | return [ 425 | 'path' => $filename, 426 | 'file' => wp_basename(apply_filters('image_make_intermediate_size', $filename)), 427 | 'width' => $this->size['width'], 428 | 'height' => $this->size['height'], 429 | 'mime-type' => $mime_type, 430 | ]; 431 | } catch (Exception $exception) { 432 | return new WP_Error('image_save_error', 'Failed to save image', $exception); 433 | } 434 | } 435 | 436 | /** 437 | * Returns stream of current image. 438 | * 439 | * @since 3.5.0 440 | * 441 | * @param string $mime_type The mime type of the image. 442 | * @return bool True on success, false on failure. 443 | */ 444 | public function stream($mime_type = null) 445 | { 446 | list($filename, $extension, $mime_type) = $this->get_output_format(null, $mime_type); 447 | 448 | switch ($mime_type) { 449 | case 'image/png': 450 | header('Content-Type: image/png'); 451 | echo $this->image->writeToBuffer('.png'); 452 | return true; 453 | case 'image/jpeg': 454 | header('Content-Type: image/jpeg'); 455 | echo $this->image->writeToBuffer('.jpg', [ 456 | 'Q' => $this->get_quality() 457 | ]); 458 | return true; 459 | } 460 | return false; 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joppuyo/vips-image-editor", 3 | "description": "High performance WordPress image processing with VIPS", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-2.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Johannes Siipola", 9 | "email": "johannes@siipo.la" 10 | } 11 | ], 12 | "require": { 13 | "jcupitt/vips": "^1.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "f033a50e76e50776805699329f46e781", 8 | "packages": [ 9 | { 10 | "name": "jcupitt/vips", 11 | "version": "v1.0.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/jcupitt/php-vips.git", 15 | "reference": "456cbad0b428a7cef52fcf23eec804a975923ab3" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/jcupitt/php-vips/zipball/456cbad0b428a7cef52fcf23eec804a975923ab3", 20 | "reference": "456cbad0b428a7cef52fcf23eec804a975923ab3", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-vips": ">=0.1.2", 25 | "php": ">=7.0.11", 26 | "psr/log": "^1.0.1" 27 | }, 28 | "require-dev": { 29 | "jakub-onderka/php-parallel-lint": "^0.9.2", 30 | "phpdocumentor/phpdocumentor": "^2.9", 31 | "phpunit/phpunit": "^6.0" 32 | }, 33 | "type": "library", 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "1.0.x-dev" 37 | } 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Jcupitt\\Vips\\": "src" 42 | } 43 | }, 44 | "notification-url": "https://packagist.org/downloads/", 45 | "license": [ 46 | "MIT" 47 | ], 48 | "authors": [ 49 | { 50 | "name": "John Cupitt", 51 | "email": "jcupitt@gmail.com", 52 | "homepage": "https://github.com/jcupitt", 53 | "role": "Developer" 54 | } 55 | ], 56 | "description": "A high-level interface to the libvips image processing library.", 57 | "homepage": "https://github.com/jcupitt/php-vips", 58 | "keywords": [ 59 | "image", 60 | "libvips", 61 | "processing" 62 | ], 63 | "time": "2017-04-30T15:40:43+00:00" 64 | }, 65 | { 66 | "name": "psr/log", 67 | "version": "1.0.2", 68 | "source": { 69 | "type": "git", 70 | "url": "https://github.com/php-fig/log.git", 71 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" 72 | }, 73 | "dist": { 74 | "type": "zip", 75 | "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 76 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 77 | "shasum": "" 78 | }, 79 | "require": { 80 | "php": ">=5.3.0" 81 | }, 82 | "type": "library", 83 | "extra": { 84 | "branch-alias": { 85 | "dev-master": "1.0.x-dev" 86 | } 87 | }, 88 | "autoload": { 89 | "psr-4": { 90 | "Psr\\Log\\": "Psr/Log/" 91 | } 92 | }, 93 | "notification-url": "https://packagist.org/downloads/", 94 | "license": [ 95 | "MIT" 96 | ], 97 | "authors": [ 98 | { 99 | "name": "PHP-FIG", 100 | "homepage": "http://www.php-fig.org/" 101 | } 102 | ], 103 | "description": "Common interface for logging libraries", 104 | "homepage": "https://github.com/php-fig/log", 105 | "keywords": [ 106 | "log", 107 | "psr", 108 | "psr-3" 109 | ], 110 | "time": "2016-10-10T12:19:37+00:00" 111 | } 112 | ], 113 | "packages-dev": [], 114 | "aliases": [], 115 | "minimum-stability": "stable", 116 | "stability-flags": [], 117 | "prefer-stable": false, 118 | "prefer-lowest": false, 119 | "platform": [], 120 | "platform-dev": [] 121 | } 122 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | volumes: 7 | - db_data:/var/lib/mysql 8 | restart: always 9 | environment: 10 | MYSQL_ROOT_PASSWORD: somewordpress 11 | MYSQL_DATABASE: wordpress 12 | MYSQL_USER: wordpress 13 | MYSQL_PASSWORD: wordpress 14 | 15 | wordpress: 16 | depends_on: 17 | - db 18 | build: . 19 | ports: 20 | - "9876:80" 21 | restart: always 22 | environment: 23 | WORDPRESS_DB_HOST: db:3306 24 | WORDPRESS_DB_USER: wordpress 25 | WORDPRESS_DB_PASSWORD: wordpress 26 | WORDPRESS_DB_NAME: wordpress 27 | volumes: 28 | - wordpress:/var/www/html 29 | - ./:/var/www/html/wp-content/plugins/vips-image-editor 30 | - ./wp-content:/var/www/html/wp-content 31 | volumes: 32 | db_data: 33 | wordpress: -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === VIPS Image Editor === 2 | Contributors: joppuyo 3 | Tags: vips, image 4 | Requires at least: 4.9.0 5 | Tested up to: 4.9.8 6 | Requires PHP: 7.0.0 7 | Stable tag: trunk 8 | License: GPLv2 or later 9 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | High performance WordPress image processing with VIPS 12 | 13 | == Changelog == 14 | 15 | = 1.1.0 = 16 | * Feature: vips thumbnail is used instead of resize if vips version is newer than 8.6.0 for faster resizing 17 | * Fix: Fixed issue where error was not handled correctly if target size was larger than image size 18 | * Fix: Disabled vips cache by default since it took up more memory without any performance benefits 19 | 20 | = 1.0.3 = 21 | * Fix Bedrock compatibility 22 | 23 | = 1.0.2 = 24 | * Add package name to composer.json 25 | 26 | = 1.0.1 = 27 | * Add WordPress readme.txt 28 | 29 | = 1.0.0 = 30 | * Initial release 31 | -------------------------------------------------------------------------------- /vips-image-editor.php: -------------------------------------------------------------------------------- 1 |

'; 29 | echo __("VIPS PHP extension is not loaded. VIPS image editor can't function without it. VIPS editor has been disabled.", 'vips-image-editor'); 30 | echo '

'; 31 | } 32 | }); 33 | 34 | add_filter('wp_image_editors', function($image_editors) { 35 | if (extension_loaded('vips')) { 36 | array_unshift($image_editors, 'Image_Editor_Vips'); 37 | } 38 | return $image_editors; 39 | }, 20); 40 | 41 | --------------------------------------------------------------------------------