├── .editorconfig ├── .gitignore ├── .travis.yml ├── composer.json ├── phpunit.xml ├── readme.md ├── release.sh ├── scripts └── install_php_extensions.sh ├── src ├── Folklore │ └── Image │ │ ├── Events │ │ └── ImageSaved.php │ │ ├── Exception │ │ ├── Exception.php │ │ ├── FileMissingException.php │ │ ├── FormatException.php │ │ └── ParseException.php │ │ ├── Facades │ │ └── Image.php │ │ ├── ImageController.php │ │ ├── ImageManager.php │ │ ├── ImageProxy.php │ │ ├── ImageServe.php │ │ └── ImageServiceProvider.php └── resources │ ├── assets │ ├── .gitkeep │ └── js │ │ └── image.js │ └── config │ ├── .gitkeep │ └── image.php └── tests ├── .gitkeep ├── ImageProxyTestCase.php ├── ImageServeTestCase.php ├── ImageTestCase.php └── fixture ├── cache └── .gitignore ├── custom └── .gitignore ├── image.jpg ├── image_small.jpg └── wrong.jpg /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.php] 2 | indent_style = space 3 | indent_size = 4 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /_book 3 | /coverage 4 | /docs 5 | /js 6 | node_modules 7 | composer.phar 8 | composer.lock 9 | package-lock.json 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | cache: 4 | directories: 5 | - $HOME/.cache/pip 6 | - $HOME/.composer/cache/files 7 | - ${TRAVIS_BUILD_DIR}/travis/extension-cache 8 | 9 | php: 10 | - 5.5 11 | - 5.6 12 | - 7.0 13 | - 7.1 14 | 15 | env: 16 | - ILLUMINATE_VERSION=5.1.* PHPUNIT_VERSION=~4.0 17 | - ILLUMINATE_VERSION=5.2.* PHPUNIT_VERSION=~4.0 18 | - ILLUMINATE_VERSION=5.3.* PHPUNIT_VERSION=~5.0 19 | - ILLUMINATE_VERSION=5.4.* PHPUNIT_VERSION=~5.7 20 | - ILLUMINATE_VERSION=5.5.* PHPUNIT_VERSION=~6.0 21 | - ILLUMINATE_VERSION=5.6.* PHPUNIT_VERSION=~7.0 22 | - ILLUMINATE_VERSION=5.7.* PHPUNIT_VERSION=~7.0 COVERAGE=true 23 | 24 | matrix: 25 | # For each PHP version we exclude the coverage env, except for PHP 7.1 26 | exclude: 27 | # Test only Laravel 5.1 and 5.2 on PHP 5.5 28 | - php: 5.5 29 | env: ILLUMINATE_VERSION=5.3.* PHPUNIT_VERSION=~5.0 30 | - php: 5.5 31 | env: ILLUMINATE_VERSION=5.4.* PHPUNIT_VERSION=~5.7 32 | - php: 5.5 33 | env: ILLUMINATE_VERSION=5.5.* PHPUNIT_VERSION=~6.0 34 | - php: 5.5 35 | env: ILLUMINATE_VERSION=5.6.* PHPUNIT_VERSION=~7.0 36 | - php: 5.5 37 | env: ILLUMINATE_VERSION=5.7.* PHPUNIT_VERSION=~7.0 COVERAGE=true 38 | # Don't test Laravel 5.5 and up on PHP 5.6 39 | - php: 5.6 40 | env: ILLUMINATE_VERSION=5.5.* PHPUNIT_VERSION=~6.0 41 | - php: 5.6 42 | env: ILLUMINATE_VERSION=5.6.* PHPUNIT_VERSION=~7.0 43 | - php: 5.6 44 | env: ILLUMINATE_VERSION=5.7.* PHPUNIT_VERSION=~7.0 COVERAGE=true 45 | # Test Laravel 5.5 and down on PHP 7.0 46 | - php: 7.0 47 | env: ILLUMINATE_VERSION=5.6.* PHPUNIT_VERSION=~7.0 48 | - php: 7.0 49 | env: ILLUMINATE_VERSION=5.7.* PHPUNIT_VERSION=~7.0 COVERAGE=true 50 | # Test only Laravel 5.4 and up on PHP 7.1 51 | - php: 7.1 52 | env: ILLUMINATE_VERSION=5.1.* PHPUNIT_VERSION=~4.0 53 | - php: 7.1 54 | env: ILLUMINATE_VERSION=5.2.* PHPUNIT_VERSION=~4.0 55 | - php: 7.1 56 | env: ILLUMINATE_VERSION=5.3.* PHPUNIT_VERSION=~5.0 57 | 58 | before_install: 59 | - cp ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ~/xdebug.ini 60 | - phpenv config-rm xdebug.ini 61 | - sudo apt-get install graphicsmagick libgraphicsmagick1-dev 62 | - pear config-set preferred_state beta 63 | - pecl channel-update pecl.php.net 64 | - ./scripts/install_php_extensions.sh "imagick.so:imagick gmagick.so:gmagick" 65 | - composer global require hirak/prestissimo --update-no-dev 66 | - composer require "illuminate/support:${ILLUMINATE_VERSION}" --no-update --prefer-dist 67 | - composer require "orchestra/testbench:${ILLUMINATE_VERSION/5\./3\.}" --no-update --prefer-dist 68 | - composer require "phpunit/phpunit:${PHPUNIT_VERSION}" --no-update --prefer-dist 69 | 70 | install: travis_retry composer install --no-interaction --prefer-dist 71 | 72 | before_script: phpenv config-add ~/xdebug.ini 73 | 74 | script: vendor/bin/phpunit 75 | 76 | after_success: sh -c "if [ ! -z ${COVERAGE+x} ]; then travis_retry php vendor/bin/php-coveralls; fi" 77 | 78 | notifications: 79 | email: false 80 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "folklore/image", 3 | "description": "Image manipulation library for Laravel 5 based on Imagine and inspired by Croppa for easy url based manipulation", 4 | "keywords": ["laravel","image","imagick","gd","imagine","watermark","gmagick","thumbnail"], 5 | "homepage": "http://github.com/Folkloreatelier/laravel-image", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Folklore", 10 | "email": "info@atelierfolklore.ca", 11 | "homepage": "http://atelierfolklore.ca" 12 | }, 13 | { 14 | "name": "David Mongeau-Petitpas", 15 | "email": "dmp@atelierfolklore.ca", 16 | "homepage": "http://mongo.ca", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.5.9", 22 | "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*", 23 | "guzzlehttp/guzzle": "5.3|~6.0", 24 | "imagine/imagine": "0.6.*" 25 | }, 26 | "require-dev": { 27 | "fzaninotto/faker": "~1.4", 28 | "orchestra/testbench": "3.1.*|3.2.*|3.3.*|3.4.*|3.5.*|3.6.*|3.7.*", 29 | "mockery/mockery": "0.9.*|1.0.*", 30 | "phpunit/phpunit": "~4.0|~4.1|~5.4|~5.7|~6.0|~7.0", 31 | "php-coveralls/php-coveralls": "^2.1" 32 | }, 33 | "autoload": { 34 | "psr-0": { 35 | "Folklore\\Image\\": "src/", 36 | "Folklore\\Image\\Tests": "tests/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "classmap": [ 41 | "tests/" 42 | ] 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "Folklore\\Image\\ImageServiceProvider" 48 | ], 49 | "aliases": { 50 | "Image": "Folklore\\Image\\Facades\\Image" 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ImageTestCase.php 15 | ./tests/ImageServeTestCase.php 16 | ./tests/ImageProxyTestCase.php 17 | 18 | 19 | 20 | 21 | ./src/Folklore/Image 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Image 2 | Laravel Image is an image manipulation package for Laravel 4 and 5 based on the [PHP Imagine library](https://github.com/avalanche123/Imagine). It is inspired by [Croppa](https://github.com/BKWLD/croppa) as it can use specially formatted urls to do the manipulations. It supports basic image manipulations such as resize, crop, rotation and flip. It also supports effects such as negative, grayscale, gamma, colorize and blur. You can also define custom filters for greater flexibility. 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/folklore/image/v/stable.svg)](https://packagist.org/packages/folklore/image) 5 | [![Build Status](https://travis-ci.org/Folkloreatelier/laravel-image.png?branch=master)](https://travis-ci.org/Folkloreatelier/laravel-image) 6 | [![Total Downloads](https://poser.pugx.org/folklore/image/downloads.svg)](https://packagist.org/packages/folklore/image) 7 | 8 | The main difference between this package and other image manipulation libraries is that you can use parameters directly in the url to manipulate the image. A manipulated version of the image is then saved in the same path as the original image, **creating a static version of the file and bypassing PHP for all future requests**. 9 | 10 | For example, if you have an image at this URL: 11 | 12 | /uploads/photo.jpg 13 | 14 | To create a 300x300 version of this image in black and white, you use the URL: 15 | 16 | /uploads/photo-image(300x300-crop-grayscale).jpg 17 | 18 | To help you generate the URL to an image, you can use the `Image::url()` method 19 | 20 | ```php 21 | Image::url('/uploads/photo.jpg',300,300,array('crop','grayscale')); 22 | ``` 23 | 24 | or 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | Alternatively, you can programmatically manipulate images using the `Image::make()` method. It supports all the same options as the `Image::url()` method. 31 | 32 | ```php 33 | Image::make('/uploads/photo.jpg',array( 34 | 'width' => 300, 35 | 'height' => 300, 36 | 'grayscale' => true 37 | ))->save('/path/to/the/thumbnail.jpg'); 38 | ``` 39 | 40 | or use directly the Imagine library 41 | 42 | ```php 43 | $thumbnail = Image::open('/uploads/photo.jpg') 44 | ->thumbnail(new Imagine\Image\Box(300,300)); 45 | 46 | $thumbnail->effects()->grayscale(); 47 | 48 | $thumbnail->save('/path/to/the/thumbnail.jpg'); 49 | ``` 50 | 51 | ## Features 52 | 53 | This package use [Imagine](https://github.com/avalanche123/Imagine) for image manipulation. Imagine is compatible with GD2, Imagick, Gmagick and supports a lot of [features](http://imagine.readthedocs.org/en/latest/). 54 | 55 | This package also provides some common filters ready to use ([more on this](https://github.com/Folkloreatelier/laravel-image/wiki/Image-filters)): 56 | - Resize 57 | - Crop (with position) 58 | - Rotation 59 | - Black and white 60 | - Invert 61 | - Gamma 62 | - Blur 63 | - Colorization 64 | - Interlace 65 | 66 | ## Version Compatibility 67 | 68 | Laravel | Image 69 | :---------|:---------- 70 | 4.2.x | 0.1.x 71 | 5.0.x | 0.2.x 72 | 5.1.x | 0.3.x 73 | 5.2.x | 0.3.x 74 | 75 | ## Installation 76 | 77 | #### Dependencies: 78 | 79 | * [Laravel 5.x](https://github.com/laravel/laravel) 80 | * [Imagine 0.6.x](https://github.com/avalanche123/Imagine) 81 | 82 | #### Server Requirements: 83 | 84 | * [gd](http://php.net/manual/en/book.image.php) or [Imagick](http://php.net/manual/fr/book.imagick.php) or [Gmagick](http://www.php.net/manual/fr/book.gmagick.php) 85 | * [exif](http://php.net/manual/en/book.exif.php) - Required to get image format. 86 | 87 | #### Installation: 88 | 89 | **1-** Require the package via Composer in your `composer.json`. 90 | ```json 91 | { 92 | "require": { 93 | "folklore/image": "0.3.*" 94 | } 95 | } 96 | ``` 97 | 98 | **2-** Run Composer to install or update the new requirement. 99 | 100 | ```bash 101 | $ composer install 102 | ``` 103 | 104 | or 105 | 106 | ```bash 107 | $ composer update 108 | ``` 109 | 110 | **3-** Add the service provider to your `app/config/app.php` file 111 | ```php 112 | 'Folklore\Image\ImageServiceProvider', 113 | ``` 114 | 115 | **4-** Add the facade to your `app/config/app.php` file 116 | ```php 117 | 'Image' => 'Folklore\Image\Facades\Image', 118 | ``` 119 | 120 | **5-** Publish the configuration file and public files 121 | 122 | ```bash 123 | $ php artisan vendor:publish --provider="Folklore\Image\ImageServiceProvider" 124 | ``` 125 | 126 | **6-** Review the configuration file 127 | 128 | ``` 129 | app/config/image.php 130 | ``` 131 | 132 | ## Documentation 133 | * [Complete documentation](https://github.com/Folkloreatelier/image/wiki) 134 | * [Configuration options](https://github.com/Folkloreatelier/image/wiki/Configuration-options) 135 | 136 | ## Roadmap 137 | Here are some features we would like to add in the future. Feel free to collaborate and improve this library. 138 | 139 | * More built-in filters such as Brightness and Contrast 140 | * More configuration when serving images 141 | * Artisan command to manipulate images 142 | * Support for batch operations on multiple files 143 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | #Fetch remote tags 4 | git fetch origin 'refs/tags/*:refs/tags/*' 5 | 6 | #Variables 7 | LAST_VERSION=$(git tag -l | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -n 1) 8 | NEXT_VERSION=$(echo $LAST_VERSION | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') 9 | VERSION=${1-${NEXT_VERSION}} 10 | DEFAULT_MESSAGE="Release" 11 | MESSAGE=${2-${DEFAULT_MESSAGE}} 12 | RELEASE_BRANCH="release/$VERSION" 13 | 14 | # Commit uncommited changes 15 | git add . 16 | git commit -am $MESSAGE 17 | git push origin develop 18 | 19 | # Merge develop branch in master 20 | git checkout master 21 | git merge develop 22 | 23 | # Tag and push master 24 | git tag $VERSION 25 | git push origin master --tags 26 | 27 | # Return to develop 28 | git checkout develop 29 | -------------------------------------------------------------------------------- /scripts/install_php_extensions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | EXTENSIONS=$1 3 | EXTENSION_CACHE_DIR=${TRAVIS_BUILD_DIR}/travis/extension-cache/`php-config --vernum` 4 | INI_DIR=${TRAVIS_BUILD_DIR}/travis/ini/ 5 | PHP_TARGET_DIR=`php-config --extension-dir` 6 | 7 | mkdir -p ${EXTENSION_CACHE_DIR} 8 | 9 | if [ -d ${EXTENSION_CACHE_DIR} ] 10 | then 11 | cp ${EXTENSION_CACHE_DIR}/* ${PHP_TARGET_DIR} 12 | fi 13 | 14 | mkdir -p ${INI_DIR} 15 | mkdir -p ${EXTENSION_CACHE_DIR} 16 | 17 | for extension in $EXTENSIONS 18 | do 19 | FILENAME=`echo $extension|cut -d : -f 1` 20 | PACKAGE=`echo $extension|cut -d : -f 2` 21 | if [ ! -f ${PHP_TARGET_DIR}/${FILENAME} ] 22 | then 23 | echo "$FILENAME not found in extension dir, compiling" 24 | printf "yes\n" | pecl install ${PACKAGE} || true 25 | else 26 | echo "Adding $FILENAME to php config" 27 | echo "extension = $FILENAME" > ${INI_DIR}/${FILENAME}.ini 28 | phpenv config-add ${INI_DIR}/${FILENAME}.ini 29 | fi 30 | if [ -f ${PHP_TARGET_DIR}/${FILENAME} ] 31 | then 32 | echo "Copying $FILENAME to php config" 33 | cp ${PHP_TARGET_DIR}/${FILENAME} ${EXTENSION_CACHE_DIR} 34 | fi 35 | done 36 | -------------------------------------------------------------------------------- /src/Folklore/Image/Events/ImageSaved.php: -------------------------------------------------------------------------------- 1 | path = $path; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Folklore/Image/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | serve($path); 25 | } catch (ParseException $e) { 26 | return abort(404); 27 | } catch (FileMissingException $e) { 28 | return abort(404); 29 | } catch (Exception $e) { 30 | return abort(500); 31 | } 32 | } 33 | 34 | public function proxy($path) 35 | { 36 | // Serve the image response from proxy. If there is a file missing 37 | // exception or parse exception, throw a 404. 38 | try { 39 | return app('image')->proxy($path); 40 | } catch (ParseException $e) { 41 | return abort(404); 42 | } catch (FileMissingException $e) { 43 | return abort(404); 44 | } catch (Exception $e) { 45 | return abort(500); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Folklore/Image/ImageManager.php: -------------------------------------------------------------------------------- 1 | null, 24 | 'height' => null, 25 | 'quality' => 80, 26 | 'filters' => array() 27 | ); 28 | 29 | /** 30 | * All of the custom filters. 31 | * 32 | * @var array 33 | */ 34 | protected $filters = array(); 35 | 36 | /** 37 | * Return an URL to process the image 38 | * 39 | * @param string $src 40 | * @param int $width 41 | * @param int $height 42 | * @param array $options 43 | * @return string 44 | */ 45 | public function url($src, $width = null, $height = null, $options = array()) 46 | { 47 | 48 | // Don't allow empty strings 49 | if (empty($src)) { 50 | return; 51 | } 52 | 53 | // Extract the path from a URL if a URL was provided instead of a path 54 | $src = parse_url($src, PHP_URL_PATH); 55 | 56 | //If width parameter is an array, use it as options 57 | if (is_array($width)) { 58 | $options = $width; 59 | $width = null; 60 | $height = null; 61 | } 62 | 63 | $config = $this->app['config']; 64 | $url_parameter = isset($options['url_parameter']) ? $options['url_parameter']:$config['image.url_parameter']; 65 | $url_parameter_separator = isset($options['url_parameter_separator']) ? $options['url_parameter_separator']:$config['image.url_parameter_separator']; 66 | unset($options['url_parameter'],$options['url_parameter_separator']); 67 | 68 | //Get size 69 | if (isset($options['width'])) { 70 | $width = $options['width']; 71 | } 72 | if (isset($options['height'])) { 73 | $height = $options['height']; 74 | } 75 | if (empty($width)) { 76 | $width = '_'; 77 | } 78 | if (empty($height)) { 79 | $height = '_'; 80 | } 81 | 82 | // Produce the parameter parts 83 | $params = array(); 84 | 85 | //Add size only if present 86 | if ($width != '_' || $height != '_') { 87 | $params[] = $width.'x'.$height; 88 | } 89 | 90 | // Build options. If the key as no value or is equal to 91 | // true, only the key is added. 92 | if ($options && is_array($options)) { 93 | foreach ($options as $key => $val) { 94 | if (is_numeric($key)) { 95 | $params[] = $val; 96 | } elseif ($val === true || $val === null) { 97 | $params[] = $key; 98 | } elseif (is_array($val)) { 99 | $params[] = $key.'('.implode(',', $val).')'; 100 | } else { 101 | $params[] = $key.'('.$val.')'; 102 | } 103 | } 104 | } 105 | 106 | //Create the url parameter 107 | $params = implode($url_parameter_separator, $params); 108 | $parameter = str_replace('{options}', $params, $url_parameter); 109 | 110 | // Break the path apart and put back together again 111 | $parts = pathinfo($src); 112 | $host = isset($options['host']) ? $options['host']:$this->app['config']['image.host']; 113 | $dir = trim($parts['dirname'], '/'); 114 | 115 | $path = array(); 116 | $path[] = rtrim($host, '/'); 117 | 118 | if ($prefix = $this->app['config']->get('image.write_path')) { 119 | $path[] = trim($prefix, '/'); 120 | } 121 | 122 | if (!empty($dir)) { 123 | $path[] = $dir; 124 | } 125 | 126 | $filename = array(); 127 | $filename[] = $parts['filename'].$parameter; 128 | if (!empty($parts['extension'])) { 129 | $filename[] = $parts['extension']; 130 | } 131 | $path[] = implode('.', $filename); 132 | 133 | return implode('/', $path); 134 | 135 | } 136 | 137 | /** 138 | * Make an image and apply options 139 | * 140 | * @param string $path The path of the image 141 | * @param array $options The manipulations to apply on the image 142 | * @return ImageInterface 143 | */ 144 | public function make($path, $options = array()) 145 | { 146 | //Get app config 147 | $config = $this->app['config']; 148 | 149 | // See if the referenced file exists and is an image 150 | if (!($path = $this->getRealPath($path))) { 151 | throw new FileMissingException('Image file missing'); 152 | } 153 | 154 | // Get image format 155 | $format = $this->format($path); 156 | if (!$format) { 157 | throw new FormatException('Image format is not supported'); 158 | } 159 | 160 | // Check if all filters exists 161 | if (isset($options['filters']) && sizeof($options['filters'])) { 162 | foreach ($options['filters'] as $filter) { 163 | $filter = (array)$filter; 164 | $key = $filter[0]; 165 | if (!$this->filters[$key]) { 166 | throw new Exception('Custom filter "'.$key.'" doesn\'t exists.'); 167 | } 168 | } 169 | } 170 | 171 | // Increase memory limit, cause some images require a lot to resize 172 | if ($config->get('image.memory_limit')) { 173 | ini_set('memory_limit', $config->get('image.memory_limit')); 174 | } 175 | 176 | //Open the image 177 | $image = $this->open($path); 178 | 179 | //Merge options with the default 180 | $options = array_merge($this->defaultOptions, $options); 181 | 182 | // Apply the custom filter on the image. Replace the 183 | // current image with the return value. 184 | if (isset($options['filters']) && sizeof($options['filters'])) { 185 | foreach ($options['filters'] as $filter) { 186 | $arguments = (array)$filter; 187 | array_unshift($arguments, $image); 188 | 189 | $image = call_user_func_array(array($this,'applyCustomFilter'), $arguments); 190 | } 191 | } 192 | 193 | // Resize only if one or both width and height values are set. 194 | if ($options['width'] !== null || $options['height'] !== null) { 195 | $crop = isset($options['crop']) ? $options['crop']:false; 196 | 197 | $image = $this->thumbnail($image, $options['width'], $options['height'], $crop); 198 | } 199 | 200 | // Apply built-in filters by checking fi a method $this->filterName 201 | // exists. Also if the value of the option is false, the filter 202 | // is ignored. 203 | foreach ($options as $key => $arguments) { 204 | $method = 'filter'.ucfirst($key); 205 | 206 | if ($arguments !== false && method_exists($this, $method)) { 207 | $arguments = (array)$arguments; 208 | array_unshift($arguments, $image); 209 | 210 | $image = call_user_func_array(array($this, $method), $arguments); 211 | } 212 | } 213 | 214 | 215 | 216 | return $image; 217 | } 218 | 219 | /** 220 | * Serve an image from an url 221 | * 222 | * @param string $path 223 | * @param array $config 224 | * @return Illuminate\Support\Facades\Response 225 | */ 226 | public function serve($path, $config = array()) 227 | { 228 | //Use user supplied quality or the config value 229 | $quality = array_get($config, 'quality', $this->app['config']['image.quality']); 230 | //if nothing works fallback to the hardcoded value 231 | $quality = $quality ?: $this->defaultOptions['quality']; 232 | 233 | //Merge config with defaults 234 | $config = array_merge(array( 235 | 'quality' => $quality, 236 | 'custom_filters_only' => $this->app['config']['image.serve_custom_filters_only'], 237 | 'write_image' => $this->app['config']['image.write_image'], 238 | 'write_path' => $this->app['config']['image.write_path'] 239 | ), $config); 240 | 241 | $serve = new ImageServe($this, $config); 242 | 243 | return $serve->response($path); 244 | } 245 | 246 | /** 247 | * Proxy an image 248 | * 249 | * @param string $path 250 | * @param array $config 251 | * @return Illuminate\Support\Facades\Response 252 | */ 253 | public function proxy($path, $config = array()) 254 | { 255 | //Merge config with defaults 256 | $config = array_merge(array( 257 | 'tmp_path' => $this->app['config']['image.proxy_tmp_path'], 258 | 'filesystem' => $this->app['config']['image.proxy_filesystem'], 259 | 'cache' => $this->app['config']['image.proxy_cache'], 260 | 'cache_expiration' => $this->app['config']['image.proxy_cache_expiration'], 261 | 'write_image' => $this->app['config']['image.proxy_write_image'], 262 | 'cache_filesystem' => $this->app['config']['image.proxy_cache_filesystem'] 263 | ), $config); 264 | 265 | $serve = new ImageProxy($this, $config); 266 | return $serve->response($path); 267 | } 268 | 269 | /** 270 | * Register a custom filter. 271 | * 272 | * @param string $name The name of the filter 273 | * @param Closure|string $filter 274 | * @return void 275 | */ 276 | public function filter($name, $filter) 277 | { 278 | $this->filters[$name] = $filter; 279 | } 280 | 281 | /** 282 | * Create a thumbnail from an image 283 | * 284 | * @param ImageInterface|string $image An image instance or the path to an image 285 | * @param int $width 286 | * @return ImageInterface 287 | */ 288 | public function thumbnail($image, $width = null, $height = null, $crop = true) 289 | { 290 | //If $image is a path, open it 291 | if (is_string($image)) { 292 | $image = $this->open($image); 293 | } 294 | 295 | //Get new size 296 | $imageSize = $image->getSize(); 297 | $newWidth = $width === null ? $imageSize->getWidth():$width; 298 | $newHeight = $height === null ? $imageSize->getHeight():$height; 299 | $size = new Box($newWidth, $newHeight); 300 | 301 | $ratios = array( 302 | $size->getWidth() / $imageSize->getWidth(), 303 | $size->getHeight() / $imageSize->getHeight() 304 | ); 305 | 306 | $thumbnail = $image->copy(); 307 | 308 | $thumbnail->usePalette($image->palette()); 309 | $thumbnail->strip(); 310 | 311 | if (!$crop) { 312 | $ratio = min($ratios); 313 | } else { 314 | $ratio = max($ratios); 315 | } 316 | 317 | if ($crop) { 318 | 319 | $imageSize = $thumbnail->getSize()->scale($ratio); 320 | $thumbnail->resize($imageSize); 321 | 322 | $x = max(0, round(($imageSize->getWidth() - $size->getWidth()) / 2)); 323 | $y = max(0, round(($imageSize->getHeight() - $size->getHeight()) / 2)); 324 | 325 | $cropPositions = $this->getCropPositions($crop); 326 | 327 | if ($cropPositions[0] === 'top') { 328 | $y = 0; 329 | } elseif ($cropPositions[0] === 'bottom') { 330 | $y = $imageSize->getHeight() - $size->getHeight(); 331 | } 332 | 333 | if ($cropPositions[1] === 'left') { 334 | $x = 0; 335 | } elseif ($cropPositions[1] === 'right') { 336 | $x = $imageSize->getWidth() - $size->getWidth(); 337 | } 338 | 339 | $point = new Point($x, $y); 340 | 341 | $thumbnail->crop($point, $size); 342 | } else { 343 | if (!$imageSize->contains($size)) { 344 | $imageSize = $imageSize->scale($ratio); 345 | $thumbnail->resize($imageSize); 346 | } else { 347 | $imageSize = $thumbnail->getSize()->scale($ratio); 348 | $thumbnail->resize($imageSize); 349 | } 350 | } 351 | 352 | //Create the thumbnail 353 | return $thumbnail; 354 | } 355 | 356 | /** 357 | * Get the format of an image 358 | * 359 | * @param string $path The path to an image 360 | * @return ImageInterface 361 | */ 362 | public function format($path) 363 | { 364 | 365 | $format = @exif_imagetype($path); 366 | switch ($format) { 367 | case IMAGETYPE_GIF: 368 | return 'gif'; 369 | break; 370 | case IMAGETYPE_JPEG: 371 | return 'jpeg'; 372 | break; 373 | case IMAGETYPE_PNG: 374 | return 'png'; 375 | break; 376 | } 377 | 378 | return null; 379 | } 380 | 381 | /** 382 | * Delete a file and all manipulated files 383 | * 384 | * @param string $path The path to an image 385 | * @return void 386 | */ 387 | public function delete($path) 388 | { 389 | $files = $this->getFiles($path); 390 | 391 | foreach ($files as $file) { 392 | if (!unlink($file)) { 393 | throw new Exception('Unlink failed: '.$file); 394 | } 395 | } 396 | } 397 | 398 | /** 399 | * Delete all manipulated files 400 | * 401 | * @param string $path The path to an image 402 | * @return void 403 | */ 404 | public function deleteManipulated($path) 405 | { 406 | $files = $this->getFiles($path, false); 407 | 408 | foreach ($files as $file) { 409 | if (!unlink($file)) { 410 | throw new Exception('Unlink failed: '.$file); 411 | } 412 | } 413 | } 414 | 415 | /** 416 | * Get the URL pattern 417 | * 418 | * @return string 419 | */ 420 | public function pattern($parameter = null, $pattern = null) 421 | { 422 | //Replace the {options} with the options regular expression 423 | $config = $this->app['config']; 424 | $parameter = !isset($parameter) ? $config['image.url_parameter']:$parameter; 425 | $parameter = preg_replace('/\\\{\s*options\s*\\\}/', '([0-9a-zA-Z\(\),\-/._]+?)?', preg_quote($parameter)); 426 | 427 | if(!$pattern) 428 | { 429 | $pattern = $config->get('image.pattern', '^(.*){parameters}\.(jpg|jpeg|png|gif|JPG|JPEG|PNG|GIF)$'); 430 | } 431 | $pattern = preg_replace('/\{\s*parameters\s*\}/', $parameter, $pattern); 432 | 433 | return $pattern; 434 | } 435 | 436 | /** 437 | * Parse the path for the original path of the image and options 438 | * 439 | * @param string $path A path to parse 440 | * @param array $config Configuration options for the parsing 441 | * @return array 442 | */ 443 | public function parse($path, $config = array()) 444 | { 445 | //Default config 446 | $config = array_merge(array( 447 | 'custom_filters_only' => false, 448 | 'url_parameter' => null, 449 | 'url_parameter_separator' => $this->app['config']['image.url_parameter_separator'] 450 | ), $config); 451 | 452 | $parsedOptions = array(); 453 | if (preg_match('#'.$this->pattern($config['url_parameter']).'#i', $path, $matches)) { 454 | //Get path and options 455 | $path = $matches[1].'.'.$matches[3]; 456 | $pathOptions = $matches[2]; 457 | 458 | // Parse options from path 459 | $parsedOptions = $this->parseOptions($pathOptions, $config); 460 | } 461 | 462 | return array( 463 | 'path' => $path, 464 | 'options' => $parsedOptions 465 | ); 466 | } 467 | 468 | /** 469 | * Parse options from url string 470 | * 471 | * @param string $option_path The path contaning all the options 472 | * @param array $config Configuration options for the parsing 473 | * @return array 474 | */ 475 | protected function parseOptions($option_path, $config = array()) 476 | { 477 | 478 | //Default config 479 | $config = array_merge(array( 480 | 'custom_filters_only' => false, 481 | 'url_parameter_separator' => $this->app['config']['image.url_parameter_separator'] 482 | ), $config); 483 | 484 | $options = array(); 485 | 486 | // These will look like (depends on the url_parameter_separator): "-colorize(CC0000)-greyscale" 487 | $option_path_parts = explode($config['url_parameter_separator'], $option_path); 488 | 489 | // Loop through the params and make the options key value pairs 490 | foreach ($option_path_parts as $option) { 491 | //Check if the option is a size or is properly formatted 492 | if (!$config['custom_filters_only'] && preg_match('#([0-9]+|_)x([0-9]+|_)#i', $option, $matches)) { 493 | $options['width'] = $matches[1] === '_' ? null:(int)$matches[1]; 494 | $options['height'] = $matches[2] === '_' ? null:(int)$matches[2]; 495 | continue; 496 | } elseif (!preg_match('#(\w+)(?:\(([\w,.]+)\))?#i', $option, $matches)) { 497 | continue; 498 | } 499 | 500 | //Check if the key is valid 501 | $key = $matches[1]; 502 | if (!$this->isValidOption($key)) { 503 | throw new ParseException('The option key "'.$key.'" does not exists.'); 504 | } 505 | 506 | // If the option is a custom filter, check if it's a closure or an array. 507 | // If it's an array, merge it with options 508 | if (isset($this->filters[$key])) { 509 | if (is_object($this->filters[$key]) && is_callable($this->filters[$key])) { 510 | $arguments = isset($matches[2]) ? explode(',', $matches[2]):array(); 511 | array_unshift($arguments, $key); 512 | $options['filters'][] = $arguments; 513 | } elseif (is_array($this->filters[$key])) { 514 | $options = array_merge($options, $this->filters[$key]); 515 | } 516 | } elseif (!$config['custom_filters_only']) { 517 | if (isset($matches[2])) { 518 | $options[$key] = strpos($matches[2], ',') === true ? explode(',', $matches[2]):$matches[2]; 519 | } else { 520 | $options[$key] = true; 521 | } 522 | } else { 523 | throw new ParseException('The option key "'.$key.'" does not exists.'); 524 | } 525 | } 526 | 527 | // Merge the options with defaults 528 | return $options; 529 | } 530 | 531 | /** 532 | * Check if an option key is valid by checking if a 533 | * $this->filterName() method is present or if a custom filter 534 | * is registered. 535 | * 536 | * @param string $key Option key to check 537 | * @return boolean 538 | */ 539 | protected function isValidOption($key) 540 | { 541 | if (in_array($key, array('crop','width','height'))) { 542 | return true; 543 | } 544 | 545 | $method = 'filter'.ucfirst($key); 546 | if (method_exists($this, $method) || isset($this->filters[$key])) { 547 | return true; 548 | } 549 | return false; 550 | } 551 | 552 | /** 553 | * Get real path 554 | * 555 | * @param string $path Path to an original image 556 | * @return string 557 | */ 558 | public function getRealPath($path) 559 | { 560 | if (is_file(realpath($path))) { 561 | return realpath($path); 562 | } 563 | 564 | //Get directories 565 | $dirs = $this->app['config']['image.src_dirs']; 566 | if ($this->app['config']['image.write_path']) { 567 | $dirs[] = $this->app['config']['image.write_path']; 568 | } 569 | 570 | // Loop through all the directories files may be uploaded to 571 | foreach ($dirs as $dir) { 572 | $dir = rtrim($dir, '/'); 573 | 574 | // Check that directory exists 575 | if (!is_dir($dir)) { 576 | continue; 577 | } 578 | 579 | // Look for the image in the directory 580 | $src = realpath($dir.'/'.ltrim($path, '/')); 581 | if (is_file($src)) { 582 | return $src; 583 | } 584 | } 585 | 586 | // None found 587 | return false; 588 | } 589 | 590 | /** 591 | * Get all files (including manipulated images) 592 | * 593 | * @param string $path Path to an original image 594 | * @return array 595 | */ 596 | protected function getFiles($path, $withOriginal = true) 597 | { 598 | 599 | $images = array(); 600 | 601 | //Check path 602 | $path = urldecode($path); 603 | if (!($path = $this->getRealPath($path))) { 604 | return $images; 605 | } 606 | 607 | // Add the source image to the list 608 | if ($withOriginal) { 609 | $images[] = $path; 610 | } 611 | 612 | // Loop through the contents of the source and write directory and get 613 | // all files that match the pattern 614 | $parts = pathinfo($path); 615 | $dirs = [$parts['dirname']]; 616 | $dirs = [$parts['dirname']]; 617 | if ($this->app['config']['image.write_path']) { 618 | $dirs[] = $this->app['config']['image.write_path']; 619 | } 620 | foreach ($dirs as $directory) { 621 | $files = scandir($directory); 622 | foreach ($files as $file) { 623 | if (strpos($file, $parts['filename']) === false || !preg_match('#'.$this->pattern().'#', $file)) { 624 | continue; 625 | } 626 | $images[] = $directory.'/'.$file; 627 | } 628 | } 629 | 630 | // Return the list 631 | return $images; 632 | } 633 | 634 | /** 635 | * Apply a custom filter or an image 636 | * 637 | * @param ImageInterface $image An image instance 638 | * @param string $name The filter name 639 | * @return ImageInterface|array 640 | */ 641 | protected function applyCustomFilter(ImageInterface $image, $name) 642 | { 643 | //Get all arguments following $name and add $image as the first 644 | //arguments then call the filter closure 645 | $arguments = array_slice(func_get_args(), 2); 646 | array_unshift($arguments, $image); 647 | $return = call_user_func_array($this->filters[$name], $arguments); 648 | 649 | // If the return value is an instance of ImageInterface, 650 | // replace the current image with it. 651 | if ($return instanceof ImageInterface) { 652 | $image = $return; 653 | } 654 | 655 | return $image; 656 | } 657 | 658 | /** 659 | * Apply rotate filter 660 | * 661 | * @param ImageInterface $image An image instance 662 | * @param float $degree The rotation degree 663 | * @return void 664 | */ 665 | protected function filterRotate(ImageInterface $image, $degree) 666 | { 667 | return $image->rotate($degree); 668 | } 669 | 670 | /** 671 | * Apply grayscale filter 672 | * 673 | * @param ImageInterface $image An image instance 674 | * @return void 675 | */ 676 | protected function filterGrayscale(ImageInterface $image) 677 | { 678 | $image->effects()->grayscale(); 679 | return $image; 680 | } 681 | 682 | /** 683 | * Apply negative filter 684 | * 685 | * @param ImageInterface $image An image instance 686 | * @return void 687 | */ 688 | protected function filterNegative(ImageInterface $image) 689 | { 690 | $image->effects()->negative(); 691 | return $image; 692 | } 693 | 694 | /** 695 | * Apply gamma filter 696 | * 697 | * @param ImageInterface $image An image instance 698 | * @param float $gamma The gamma value 699 | * @return void 700 | */ 701 | protected function filterGamma(ImageInterface $image, $gamma) 702 | { 703 | $image->effects()->gamma($gamma); 704 | return $image; 705 | } 706 | 707 | /** 708 | * Apply blur filter 709 | * 710 | * @param ImageInterface $image An image instance 711 | * @param int $blur The amount of blur 712 | * @return void 713 | */ 714 | protected function filterBlur(ImageInterface $image, $blur) 715 | { 716 | $image->effects()->blur($blur); 717 | return $image; 718 | } 719 | 720 | /** 721 | * Apply colorize filter 722 | * 723 | * @param ImageInterface $image An image instance 724 | * @param string $color The hex value of the color 725 | * @return void 726 | */ 727 | protected function filterColorize(ImageInterface $image, $color) 728 | { 729 | $palettes = ['RGB','CMYK']; 730 | $parts = explode(',', $color); 731 | $color = $parts[0]; 732 | if(isset($parts[1]) && in_array(strtoupper($parts[1]), $palettes)) 733 | { 734 | $className = '\\Imagine\\Image\\Palette\\'.strtoupper($parts[1]); 735 | $palette = new $className(); 736 | } 737 | else 738 | { 739 | $palette = $image->palette(); 740 | } 741 | $color = $palette->color($color); 742 | $image->effects()->colorize($color); 743 | return $image; 744 | } 745 | 746 | /** 747 | * Apply interlace filter 748 | * 749 | * @param ImageInterface $image An image instance 750 | * @return void 751 | */ 752 | protected function filterInterlace(ImageInterface $image) 753 | { 754 | $image->interlace(ImageInterface::INTERLACE_LINE); 755 | return $image; 756 | } 757 | 758 | /** 759 | * Get mime type from image format 760 | * 761 | * @return string 762 | */ 763 | public function getMimeFromFormat($format) 764 | { 765 | 766 | switch ($format) { 767 | case 'gif': 768 | return 'image/gif'; 769 | break; 770 | case 'jpg': 771 | case 'jpeg': 772 | return 'image/jpeg'; 773 | break; 774 | case 'png': 775 | return 'image/png'; 776 | break; 777 | } 778 | 779 | return null; 780 | } 781 | 782 | /** 783 | * Return crop positions from the crop parameter 784 | * 785 | * @return array 786 | */ 787 | protected function getCropPositions($crop) 788 | { 789 | $crop = $crop === true ? 'center':$crop; 790 | 791 | $cropPositions = explode('_', $crop); 792 | if (sizeof($cropPositions) === 1) { 793 | if ($cropPositions[0] === 'top' || $cropPositions[0] === 'bottom' || $cropPositions[0] === 'center') { 794 | $cropPositions[] = 'center'; 795 | } elseif ($cropPositions[0] === 'left' || $cropPositions[0] === 'right') { 796 | array_unshift($cropPositions, 'center'); 797 | } 798 | } 799 | 800 | return $cropPositions; 801 | } 802 | 803 | /** 804 | * Create an instance of the Imagine Gd driver. 805 | * 806 | * @return \Imagine\Gd\Imagine 807 | */ 808 | protected function createGdDriver() 809 | { 810 | return new \Imagine\Gd\Imagine(); 811 | } 812 | 813 | /** 814 | * Create an instance of the Imagine Imagick driver. 815 | * 816 | * @return \Imagine\Imagick\Imagine 817 | */ 818 | protected function createImagickDriver() 819 | { 820 | return new \Imagine\Imagick\Imagine(); 821 | } 822 | 823 | /** 824 | * Create an instance of the Imagine Gmagick driver. 825 | * 826 | * @return \Imagine\Gmagick\Imagine 827 | */ 828 | protected function createGmagickDriver() 829 | { 830 | return new \Imagine\Gmagick\Imagine(); 831 | } 832 | 833 | /** 834 | * Get the default image driver name. 835 | * 836 | * @return string 837 | */ 838 | public function getDefaultDriver() 839 | { 840 | return $this->app['config']['image.driver']; 841 | } 842 | 843 | /** 844 | * Set the default image driver name. 845 | * 846 | * @param string $name 847 | * @return void 848 | */ 849 | public function setDefaultDriver($name) 850 | { 851 | $this->app['config']['image.driver'] = $name; 852 | } 853 | } 854 | -------------------------------------------------------------------------------- /src/Folklore/Image/ImageProxy.php: -------------------------------------------------------------------------------- 1 | image = $image; 18 | 19 | $this->config = array_merge([ 20 | 'tmp_path' => sys_get_temp_dir(), 21 | 'cache' => false, 22 | 'cache_expiration' => 60*24, 23 | 'write_image' => false, 24 | 'filesystem' => null, 25 | 'cache_filesystem' => null 26 | ], $config); 27 | } 28 | 29 | public function response($path) 30 | { 31 | // Increase memory limit, cause some images require a lot to resize 32 | if (config('image.memory_limit')) { 33 | ini_set('memory_limit', config('image.memory_limit')); 34 | } 35 | 36 | $app = app(); 37 | 38 | $disk = $this->getDisk(); 39 | 40 | //Check if file exists 41 | $fullPath = $path; 42 | $cache = $this->config['cache']; 43 | $existsCache = $cache ? $this->existsOnProxyCache($fullPath):false; 44 | $existsDisk = !$existsCache && $disk ? $this->existsOnProxyDisk($fullPath):false; 45 | if ($existsCache) { 46 | return $this->getResponseFromCache($fullPath); 47 | } elseif ($existsDisk) { 48 | $response = $this->getResponseFromDisk($fullPath); 49 | 50 | if ($cache) { 51 | $this->saveToProxyCache($path, $response->getContent()); 52 | } 53 | 54 | return $response; 55 | } 56 | 57 | $parse = $this->image->parse($fullPath); 58 | $originalPath = $parse['path']; 59 | $tmpPath = $this->config['tmp_path']; 60 | $extension = pathinfo($originalPath, PATHINFO_EXTENSION); 61 | $tmpOriginalPath = tempnam($tmpPath, 'original').'.'.$extension; 62 | $tmpTransformedPath = tempnam($tmpPath, 'transformed').'.'.$extension; 63 | if ($disk && !$disk->exists($originalPath)) { 64 | throw new FileMissingException(); 65 | } 66 | 67 | //Download original file 68 | if (!$disk) { 69 | $downloadFile = $this->downloadFile($originalPath); 70 | $contents = file_get_contents($downloadFile); 71 | } else { 72 | $contents = !$disk ? $this->getRemoteFile($originalPath):$disk->get($originalPath); 73 | } 74 | file_put_contents($tmpOriginalPath, $contents); 75 | $contents = null; 76 | 77 | //Get mime 78 | $format = $this->image->format($tmpOriginalPath); 79 | $mime = $this->image->getMimeFromFormat($format); 80 | 81 | $this->image->make($tmpOriginalPath, $parse['options']) 82 | ->save($tmpTransformedPath); 83 | 84 | // Trigger event 85 | event(new ImageSaved($tmpTransformedPath)); 86 | 87 | //Write image 88 | if ($this->config['write_image'] && $disk) { 89 | $resource = fopen($tmpTransformedPath, 'r'); 90 | $disk 91 | ->getDriver() 92 | ->put($fullPath, $resource, [ 93 | 'visibility' => 'public', 94 | 'ContentType' => $mime, 95 | 'CacheControl' => 'max-age='.(3600 * 24) 96 | ]); 97 | fclose($resource); 98 | } 99 | 100 | //Get response 101 | if (!$disk) { 102 | $response = $this->getResponseFromPath($tmpTransformedPath); 103 | } else { 104 | $response = $this->getResponseFromDisk($fullPath); 105 | } 106 | 107 | $response->header('Content-Type', $mime); 108 | 109 | //Save to cache 110 | $cache = $this->config['cache']; 111 | if ($cache) { 112 | $this->saveToProxyCache($path, $response->getContent()); 113 | } 114 | 115 | unlink($tmpOriginalPath); 116 | unlink($tmpTransformedPath); 117 | 118 | return $response; 119 | } 120 | 121 | protected function downloadFile($path) 122 | { 123 | $deleteOriginalFile = true; 124 | $tmpPath = tempnam($this->config['tmp_path'], 'image'); 125 | $client = new GuzzleClient(); 126 | $response = $client->request('GET', $path, [ 127 | 'sink' => $tmpPath 128 | ]); 129 | $path = $tmpPath; 130 | 131 | return $tmpPath; 132 | } 133 | 134 | protected function getDisk() 135 | { 136 | $filesystem = $this->config['filesystem']; 137 | if (!$filesystem) { 138 | return null; 139 | } 140 | 141 | return $filesystem === 'cloud' ? app('filesystem')->cloud():app('filesystem')->disk($filesystem); 142 | } 143 | 144 | protected function getCacheDisk() 145 | { 146 | $filesystem = $this->config['cache_filesystem']; 147 | if (!$filesystem) { 148 | return null; 149 | } 150 | 151 | return $filesystem === 'cloud' ? app('filesystem')->cloud():app('filesystem')->disk($filesystem); 152 | } 153 | 154 | protected function getCacheKey($path) 155 | { 156 | $key = md5($path).'_'.sha1($path); 157 | 158 | return 'image/'.preg_replace('/^([0-9a-z]{2})([0-9a-z]{2})/i', '$1/$2/', $key); 159 | } 160 | 161 | protected function getEscapedCacheKey($path) 162 | { 163 | $cacheKey = $this->getCacheKey($path); 164 | return preg_replace('/[^a-zA-Z0-9]+/i', '_', $cacheKey); 165 | } 166 | 167 | protected function existsOnProxyCache($path) 168 | { 169 | $disk = $this->getCacheDisk(); 170 | if ($disk) { 171 | $cacheKey = $this->getCacheKey($path); 172 | return $disk->exists($cacheKey); 173 | } 174 | 175 | $cacheKey = $this->getEscapedCacheKey($path); 176 | return app('cache')->has($cacheKey); 177 | } 178 | 179 | protected function existsOnProxyDisk($path) 180 | { 181 | $disk = $this->getDisk(); 182 | return $disk->exists($path); 183 | } 184 | 185 | protected function getMimeFromContent($content) 186 | { 187 | $finfo = new finfo(FILEINFO_MIME); 188 | return $finfo->buffer($content); 189 | } 190 | 191 | protected function getResponseFromCache($path) 192 | { 193 | $disk = $this->getCacheDisk(); 194 | if ($disk) { 195 | $cacheKey = $this->getCacheKey($path); 196 | $contents = $disk->get($cacheKey); 197 | } else { 198 | $cacheKey = $this->getEscapedCacheKey($path); 199 | $contents = app('cache')->get($cacheKey); 200 | } 201 | 202 | $response = response()->make($contents, 200); 203 | $response->header('Cache-control', 'max-age='.(3600*24).', public'); 204 | $response->header('Content-type', $this->getMimeFromContent($contents)); 205 | $contents = null; 206 | 207 | return $response; 208 | } 209 | 210 | protected function getResponseFromPath($path) 211 | { 212 | $content = file_get_contents($path); 213 | $response = $this->createResponseFromContent($content); 214 | $response->header('Content-type', $this->getMimeFromContent($content)); 215 | $content = null; 216 | 217 | return $response; 218 | } 219 | 220 | protected function getResponseFromDisk($path) 221 | { 222 | $disk = $this->getDisk(); 223 | $content = $disk->get($path); 224 | $response = $this->createResponseFromContent($content); 225 | $response->header('Content-type', $this->getMimeFromContent($content)); 226 | $content = null; 227 | 228 | return $response; 229 | } 230 | 231 | protected function getResponseExpires() 232 | { 233 | $proxyExpires = config('image.proxy_expires', null); 234 | return $proxyExpires ? $proxyExpires:config('image.serve_expires', 3600*24*31); 235 | } 236 | 237 | protected function saveToProxyCache($path, $contents) 238 | { 239 | $disk = $this->getCacheDisk(); 240 | if ($disk) { 241 | $cacheKey = $this->getCacheKey($path); 242 | $disk->put($cacheKey, $contents); 243 | } else { 244 | $cacheKey = $this->getEscapedCacheKey($path); 245 | $cacheExpiration = $this->config['cache_expiration']; 246 | if ($cacheExpiration === -1) { 247 | app('cache')->forever($cacheKey, $contents); 248 | } else { 249 | app('cache')->put($cacheKey, $contents, $cacheExpiration); 250 | } 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/Folklore/Image/ImageServe.php: -------------------------------------------------------------------------------- 1 | image = $image; 16 | 17 | $this->config = array_merge([ 18 | 'custom_filters_only' => false, 19 | 'write_image' => false, 20 | 'write_path' => null, 21 | 'quality' => 80, 22 | 'options' => [] 23 | ], $config); 24 | } 25 | 26 | public function response($path) 27 | { 28 | // Parse the current path 29 | $parsedPath = $this->image->parse($path, array( 30 | 'custom_filters_only' => $this->config['custom_filters_only'] 31 | )); 32 | $writePath = isset($this->config['write_path']) ? trim($this->config['write_path'], '/') : null; 33 | $parsedOptions = $parsedPath['options']; 34 | $imagePath = $parsedPath['path']; 35 | 36 | if ($writePath && strpos($imagePath, $writePath) === 0) { 37 | $imagePath = substr($imagePath, strlen($writePath)+1); 38 | } 39 | 40 | // See if the referenced file exists and is an image 41 | if (!($realPath = $this->image->getRealPath($imagePath))) { 42 | throw new FileMissingException('Image file missing'); 43 | } 44 | 45 | // create the destination if it does not exist 46 | if ($this->config['write_image']) { 47 | // make sure the path is relative to the document root 48 | if (strpos($realPath, public_path()) === 0) { 49 | $imagePath = substr($realPath, strlen(public_path())); 50 | } 51 | $destinationFolder = public_path(trim($writePath, '/') . '/' . ltrim(dirname($imagePath), '/')); 52 | 53 | if (isset($writePath)) { 54 | \File::makeDirectory($destinationFolder, 0770, true, true); 55 | } 56 | 57 | // Make sure destination is writeable 58 | if (!is_writable($destinationFolder)) { 59 | throw new Exception('Destination is not writeable'); 60 | } 61 | } 62 | 63 | 64 | // Merge all options with the following priority: 65 | // Options passed as an argument to the serve method 66 | // Options parsed from the URL 67 | // Default options 68 | $options = array_merge($parsedOptions, $this->config['options']); 69 | 70 | //Make the image 71 | $image = $this->image->make($imagePath, $options); 72 | 73 | //Get the image format 74 | $format = $this->image->format($realPath); 75 | 76 | //Get the image content 77 | $saveOptions = array(); 78 | $quality = array_get($options, 'quality', $this->config['quality']); 79 | if ($format === 'jpeg') { 80 | $saveOptions['jpeg_quality'] = $quality; 81 | } elseif ($format === 'png') { 82 | $saveOptions['png_compression_level'] = round($quality / 100 * 9); 83 | } 84 | 85 | //Write the image 86 | if ($this->config['write_image']) { 87 | $destinationPath = rtrim($destinationFolder, '/') . '/' . basename($path); 88 | $image->save($destinationPath, $saveOptions); 89 | 90 | // Trigger event 91 | event(new ImageSaved($destinationPath)); 92 | } 93 | 94 | $content = $image->get($format, $saveOptions); 95 | 96 | //Create the response 97 | $mime = $this->image->getMimeFromFormat($format); 98 | $response = $this->createResponseFromContent($content); 99 | $response->header('Content-type', $mime); 100 | 101 | return $response; 102 | } 103 | 104 | protected function getResponseExpires() 105 | { 106 | return config('image.serve_expires', 3600*24*31); 107 | } 108 | 109 | protected function createResponseFromContent($content) 110 | { 111 | $expires = $this->getResponseExpires(); 112 | $response = response()->make($content, 200); 113 | $response->header('Cache-control', 'max-age='.$expires.', public'); 114 | $response->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $expires)); 115 | return $response; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Folklore/Image/ImageServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom($configFile, 'image'); 28 | 29 | // Publish 30 | $this->publishes([ 31 | $configFile => config_path('image.php') 32 | ], 'config'); 33 | 34 | $this->publishes([ 35 | $publicFile => public_path('vendor/folklore/image') 36 | ], 'public'); 37 | 38 | $app = $this->app; 39 | $router = $app['router']; 40 | $config = $app['config']; 41 | 42 | $pattern = $app['image']->pattern(); 43 | $proxyPattern = $config->get('image.proxy_route_pattern'); 44 | $router->pattern('image_pattern', $pattern); 45 | $router->pattern('image_proxy_pattern', $proxyPattern ? $proxyPattern:$pattern); 46 | 47 | //Serve image 48 | $serve = config('image.serve'); 49 | if ($serve) { 50 | // Create a route that match pattern 51 | $serveRoute = $config->get('image.serve_route', '{image_pattern}'); 52 | $router->get($serveRoute, array( 53 | 'as' => 'image.serve', 54 | 'domain' => $config->get('image.domain', null), 55 | 'uses' => 'Folklore\Image\ImageController@serve' 56 | )); 57 | } 58 | 59 | //Proxy 60 | $proxy = $this->app['config']['image.proxy']; 61 | if ($proxy) { 62 | $serveRoute = $config->get('image.proxy_route', '{image_proxy_pattern}'); 63 | $router->get($serveRoute, array( 64 | 'as' => 'image.proxy', 65 | 'domain' => $config->get('image.proxy_domain'), 66 | 'uses' => 'Folklore\Image\ImageController@proxy' 67 | )); 68 | } 69 | } 70 | 71 | /** 72 | * Register the service provider. 73 | * 74 | * @return void 75 | */ 76 | public function register() 77 | { 78 | $this->app->singleton('image', function ($app) { 79 | return new ImageManager($app); 80 | }); 81 | } 82 | 83 | /** 84 | * Get the services provided by the provider. 85 | * 86 | * @return array 87 | */ 88 | public function provides() 89 | { 90 | return array('image'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/resources/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folkloreinc/laravel-image-legacy/146ba2710575c1f21f3b0eb1193af4ca19ac5f36/src/resources/assets/.gitkeep -------------------------------------------------------------------------------- /src/resources/assets/js/image.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define([], factory); 5 | } else if (typeof exports === 'object') { 6 | // Node. Does not work with strict CommonJS, but 7 | // only CommonJS-like environments that support module.exports, 8 | // like Node. 9 | module.exports = factory(); 10 | } else { 11 | // Browser globals (root is window) 12 | if(typeof(root.Folklore) === 'undefined') { 13 | root.Folklore = {}; 14 | } 15 | root.Folklore.Image = factory(); 16 | } 17 | }(this, function () { 18 | 19 | 'use strict'; 20 | 21 | var URL_PARAMETER = '-image({options})'; 22 | 23 | // Build a image formatted URL 24 | function url(src, width, height, options) { 25 | 26 | // Don't allow empty strings 27 | if (!src || !src.length) return; 28 | 29 | //If width parameter is an array, use it as options 30 | if(width instanceof Object) 31 | { 32 | options = width; 33 | width = null; 34 | height = null; 35 | } 36 | 37 | //Get size 38 | if (!width) width = '_'; 39 | if (!height) height = '_'; 40 | 41 | // Produce the image option 42 | var params = []; 43 | 44 | //Add size if presents 45 | if(width != '_' || height != '_') { 46 | params.push(width+'x'+height); 47 | } 48 | 49 | // Add options. 50 | if (options && options instanceof Object) { 51 | var val, key; 52 | for (key in options) { 53 | val = options[key]; 54 | if (val === true || val === null) { 55 | params.push(key); 56 | } 57 | else if (val instanceof Array) { 58 | params.push(key+'('+val.join(',')+')'); 59 | } 60 | else { 61 | params.push(key+'('+val+')'); 62 | } 63 | } 64 | } 65 | 66 | params = params.join('-'); 67 | var parameter = URL_PARAMETER.replace('{options}',params); 68 | 69 | // Break the path apart and put back together again 70 | return src.replace(/^(.+)(\.[a-z]+)$/i, "$1"+parameter+"$2"); 71 | 72 | } 73 | 74 | // Expose public methods. 75 | return { 76 | url: url 77 | }; 78 | })); 79 | -------------------------------------------------------------------------------- /src/resources/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folkloreinc/laravel-image-legacy/146ba2710575c1f21f3b0eb1193af4ca19ac5f36/src/resources/config/.gitkeep -------------------------------------------------------------------------------- /src/resources/config/image.php: -------------------------------------------------------------------------------- 1 | 'gd', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Memory limit 23 | |-------------------------------------------------------------------------- 24 | | 25 | | When manipulating an image, the memory limit is increased to this value 26 | | 27 | */ 28 | 'memory_limit' => '128M', 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Source directories 33 | |-------------------------------------------------------------------------- 34 | | 35 | | A list a directories to look for images 36 | | 37 | */ 38 | 'src_dirs' => array( 39 | public_path() 40 | ), 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Host 45 | |-------------------------------------------------------------------------- 46 | | 47 | | The http host where the image are served. Used by the Image::url() method 48 | | to generate the right URL. 49 | | 50 | */ 51 | 'host' => '', 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Pattern 56 | |-------------------------------------------------------------------------- 57 | | 58 | | The pattern that is used to match routes that will be handled by the 59 | | ImageController. The {parameters} will be remplaced by the url parameters 60 | | pattern. 61 | | 62 | */ 63 | 'pattern' => '^(.*){parameters}\.(jpg|jpeg|png|gif|JPG|JPEG|PNG|GIF)$', 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | URL parameter 68 | |-------------------------------------------------------------------------- 69 | | 70 | | The URL parameter that will be appended to your image filename containing 71 | | all the options for image manipulation. You have to put {options} where 72 | | you want options to be placed. Keep in mind that this parameter is used 73 | | in an url so all characters should be URL safe. 74 | | 75 | | Default: -image({options}) 76 | | 77 | | Example: /uploads/photo-image(300x300-grayscale).jpg 78 | | 79 | */ 80 | 'url_parameter' => '-image({options})', 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | URL parameter separator 85 | |-------------------------------------------------------------------------- 86 | | 87 | | The URL parameter separator is used to build the parameters string 88 | | that will replace {options} in url_parameter 89 | | 90 | | Default: - 91 | | 92 | | Example: /uploads/photo-image(300x300-grayscale).jpg 93 | | 94 | */ 95 | 'url_parameter_separator' => '-', 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Serve image 100 | |-------------------------------------------------------------------------- 101 | | 102 | | If true, a route will be added to catch image containing the 103 | | URL parameter above. 104 | | 105 | */ 106 | 'serve' => true, 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Serve route 111 | |-------------------------------------------------------------------------- 112 | | 113 | | If you want to restrict the route to a specific domain. 114 | | 115 | */ 116 | 'serve_domain' => null, 117 | 118 | /* 119 | |-------------------------------------------------------------------------- 120 | | Serve route 121 | |-------------------------------------------------------------------------- 122 | | 123 | | The route where image are served 124 | | 125 | */ 126 | 'serve_route' => '{image_pattern}', 127 | 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | Serve custom Filters only 131 | |-------------------------------------------------------------------------- 132 | | 133 | | Restrict options in url to custom filters only. This prevent direct 134 | | manipulation of the image. 135 | | 136 | */ 137 | 'serve_custom_filters_only' => false, 138 | 139 | /* 140 | |-------------------------------------------------------------------------- 141 | | Serve expires 142 | |-------------------------------------------------------------------------- 143 | | 144 | | The expires headers that are sent when sending image. 145 | | 146 | */ 147 | 'serve_expires' => (3600*24*31), 148 | 149 | /* 150 | |-------------------------------------------------------------------------- 151 | | Write image 152 | |-------------------------------------------------------------------------- 153 | | 154 | | When serving an image, write the manipulated image in the same directory 155 | | as the original image so the next request will serve this static file 156 | | 157 | */ 158 | 'write_image' => false, 159 | 160 | /* 161 | |-------------------------------------------------------------------------- 162 | | Write path 163 | |-------------------------------------------------------------------------- 164 | | 165 | | By default, the manipulated images are saved in the same path as the 166 | | as the original image, you can override this path here 167 | | 168 | */ 169 | 'write_path' => null, 170 | 171 | /* 172 | |-------------------------------------------------------------------------- 173 | | Proxy 174 | |-------------------------------------------------------------------------- 175 | | 176 | | This enable or disable the proxy route 177 | | 178 | */ 179 | 'proxy' => false, 180 | 181 | /* 182 | |-------------------------------------------------------------------------- 183 | | Proxy expires 184 | |-------------------------------------------------------------------------- 185 | | 186 | | The expires headers that are sent when proxying image. Defaults to 187 | | serve_expires 188 | | 189 | */ 190 | 'proxy_expires' => null, 191 | 192 | /* 193 | |-------------------------------------------------------------------------- 194 | | Proxy route 195 | |-------------------------------------------------------------------------- 196 | | 197 | | The route that will be used to serve proxied image 198 | | 199 | */ 200 | 'proxy_route' => '{image_proxy_pattern}', 201 | 202 | 203 | 204 | /* 205 | |-------------------------------------------------------------------------- 206 | | Proxy route pattern 207 | |-------------------------------------------------------------------------- 208 | | 209 | | The proxy route pattern that will be available as `image_proxy_pattern`. 210 | | If the value is null, the default image pattern will be used. 211 | | 212 | */ 213 | 'proxy_route_pattern' => null, 214 | 215 | /* 216 | |-------------------------------------------------------------------------- 217 | | Proxy route domain 218 | |-------------------------------------------------------------------------- 219 | | 220 | | If you wind to bind your route to a specific domain. 221 | | 222 | */ 223 | 'proxy_route_domain' => null, 224 | 225 | /* 226 | |-------------------------------------------------------------------------- 227 | | Proxy filesystem 228 | |-------------------------------------------------------------------------- 229 | | 230 | | The filesystem from which the file will be proxied 231 | | 232 | */ 233 | 'proxy_filesystem' => 'cloud', 234 | 235 | /* 236 | |-------------------------------------------------------------------------- 237 | | Proxy temporary directory 238 | |-------------------------------------------------------------------------- 239 | | 240 | | Write the manipulated image back to the file system 241 | | 242 | */ 243 | 'proxy_write_image' => true, 244 | 245 | /* 246 | |-------------------------------------------------------------------------- 247 | | Proxy cache 248 | |-------------------------------------------------------------------------- 249 | | 250 | | Cache the response of the proxy on the local filesystem. The proxy will be 251 | | cached using the laravel cache driver. 252 | | 253 | */ 254 | 'proxy_cache' => true, 255 | 256 | /* 257 | |-------------------------------------------------------------------------- 258 | | Proxy cache filesystem 259 | |-------------------------------------------------------------------------- 260 | | 261 | | If you want the proxy to cache files on a filesystem instead of using the 262 | | cache driver. 263 | | 264 | */ 265 | 'proxy_cache_filesystem' => null, 266 | 267 | /* 268 | |-------------------------------------------------------------------------- 269 | | Proxy cache expiration 270 | |-------------------------------------------------------------------------- 271 | | 272 | | The number of minuts that a proxied image can stay in cache. If the value 273 | | is -1, the image is cached forever. 274 | | 275 | */ 276 | 'proxy_cache_expiration' => 60*24, 277 | 278 | /* 279 | |-------------------------------------------------------------------------- 280 | | Proxy temporary path 281 | |-------------------------------------------------------------------------- 282 | | 283 | | The temporary path where the manipulated file are saved. 284 | | 285 | */ 286 | 'proxy_tmp_path' => sys_get_temp_dir(), 287 | 288 | ); 289 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folkloreinc/laravel-image-legacy/146ba2710575c1f21f3b0eb1193af4ca19ac5f36/tests/.gitkeep -------------------------------------------------------------------------------- /tests/ImageProxyTestCase.php: -------------------------------------------------------------------------------- 1 | image = $this->app['image']; 19 | $this->imageSize = getimagesize(public_path().$this->imagePath); 20 | $this->imageSmallSize = getimagesize(public_path().$this->imageSmallPath); 21 | } 22 | 23 | public function tearDown() 24 | { 25 | $customPath = $this->app['path.public'].'/custom'; 26 | $this->app['config']->set('image.write_path', $customPath); 27 | 28 | $this->image->deleteManipulated($this->imagePath); 29 | 30 | parent::tearDown(); 31 | } 32 | 33 | public function testProxy() 34 | { 35 | $url = $this->image->url($this->imagePath, 300, 300, [ 36 | 'crop' => true 37 | ]); 38 | $response = $this->call('GET', $url); 39 | $this->assertTrue($response->isOk()); 40 | 41 | $image = imagecreatefromstring($response->getContent()); 42 | $this->assertTrue($image !== false); 43 | 44 | $this->assertEquals(imagesx($image), 300); 45 | $this->assertEquals(imagesy($image), 300); 46 | 47 | imagedestroy($image); 48 | } 49 | 50 | public function testProxyURL() 51 | { 52 | $this->app['config']->set('image.host', '/proxy/http://placehold.it/'); 53 | $this->app['config']->set('image.proxy_filesystem', null); 54 | $this->app['config']->set('image.proxy_route_pattern', '^(.*)$'); 55 | 56 | $url = $this->image->url('/640x480.png', 300, 300, [ 57 | 'crop' => true 58 | ]); 59 | $response = $this->call('GET', $url); 60 | $this->assertTrue($response->isOk()); 61 | 62 | $image = imagecreatefromstring($response->getContent()); 63 | $this->assertTrue($image !== false); 64 | 65 | $this->assertEquals(imagesx($image), 300); 66 | $this->assertEquals(imagesy($image), 300); 67 | 68 | imagedestroy($image); 69 | } 70 | 71 | /** 72 | * Define environment setup. 73 | * 74 | * @param \Illuminate\Foundation\Application $app 75 | * @return void 76 | */ 77 | protected function getEnvironmentSetUp($app) 78 | { 79 | $app->instance('path.public', __DIR__.'/fixture'); 80 | 81 | $app['config']->set('image.host', '/proxy'); 82 | $app['config']->set('image.serve', false); 83 | $app['config']->set('image.proxy', true); 84 | $app['config']->set('image.proxy_route', '/proxy/{image_proxy_pattern}'); 85 | $app['config']->set('image.proxy_filesystem', 'image_testbench'); 86 | $app['config']->set('image.proxy_cache_filesystem', null); 87 | 88 | $app['config']->set('filesystems.default', 'image_testbench'); 89 | $app['config']->set('filesystems.cloud', 'image_testbench'); 90 | 91 | $app['config']->set('filesystems.disks.image_testbench', [ 92 | 'driver' => 'local', 93 | 'root' => __DIR__.'/fixture' 94 | ]); 95 | 96 | $app['config']->set('filesystems.disks.image_testbench_cache', [ 97 | 'driver' => 'local', 98 | 'root' => __DIR__.'/fixture/cache' 99 | ]); 100 | } 101 | 102 | protected function getPackageProviders($app) 103 | { 104 | return array('Folklore\Image\ImageServiceProvider'); 105 | } 106 | 107 | protected function getPackageAliases($app) 108 | { 109 | return array( 110 | 'Image' => 'Folklore\Image\Facades\Image' 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/ImageServeTestCase.php: -------------------------------------------------------------------------------- 1 | image = $this->app['image']; 19 | $this->imageSize = getimagesize(public_path().$this->imagePath); 20 | $this->imageSmallSize = getimagesize(public_path().$this->imageSmallPath); 21 | } 22 | 23 | public function tearDown() 24 | { 25 | $customPath = $this->app['path.public'].'/custom'; 26 | $this->app['config']->set('image.write_path', $customPath); 27 | 28 | $this->image->deleteManipulated($this->imagePath); 29 | 30 | parent::tearDown(); 31 | } 32 | 33 | public function testServeWriteImage() 34 | { 35 | $this->app['config']->set('image.write_image', true); 36 | 37 | $url = $this->image->url($this->imagePath, 300, 300, [ 38 | 'crop' => true 39 | ]); 40 | 41 | $response = $this->call('GET', $url); 42 | 43 | $this->assertTrue($response->isOk()); 44 | 45 | $imagePath = $this->app['path.public'].'/'.basename($url); 46 | $this->assertFileExists($imagePath); 47 | 48 | $sizeManipulated = getimagesize($imagePath); 49 | $this->assertEquals($sizeManipulated[0], 300); 50 | $this->assertEquals($sizeManipulated[1], 300); 51 | 52 | $this->app['config']->set('image.write_image', false); 53 | } 54 | 55 | public function testServeWriteImagePath() 56 | { 57 | $customPath = 'custom'; 58 | 59 | $this->app['config']->set('image.write_image', true); 60 | $this->app['config']->set('image.write_path', $customPath); 61 | 62 | $url = $this->image->url($this->imagePath, 300, 300, [ 63 | 'crop' => true 64 | ]); 65 | 66 | $response = $this->call('GET', $url); 67 | 68 | $this->assertTrue($response->isOk()); 69 | 70 | $imagePath = public_path($customPath.'/'.basename($url)); 71 | $this->assertFileExists($imagePath); 72 | 73 | $sizeManipulated = getimagesize($imagePath); 74 | $this->assertEquals($sizeManipulated[0], 300); 75 | $this->assertEquals($sizeManipulated[1], 300); 76 | 77 | $this->app['config']->set('image.write_image', false); 78 | $this->app['config']->set('image.write_path', null); 79 | } 80 | 81 | public function testServeNoResize() 82 | { 83 | 84 | $url = $this->image->url($this->imagePath, null, null, array( 85 | 'grayscale' 86 | )); 87 | $response = $this->call('GET', $url); 88 | 89 | $this->assertTrue($response->isOk()); 90 | 91 | $sizeManipulated = getimagesizefromstring($response->getContent()); 92 | $this->assertEquals($sizeManipulated[0], $this->imageSize[0]); 93 | $this->assertEquals($sizeManipulated[1], $this->imageSize[1]); 94 | } 95 | 96 | public function testServeResizeWidth() 97 | { 98 | $url = $this->image->url($this->imagePath, 300); 99 | $response = $this->call('GET', $url); 100 | 101 | $this->assertTrue($response->isOk()); 102 | 103 | $sizeManipulated = getimagesizefromstring($response->getContent()); 104 | $this->assertEquals($sizeManipulated[0], 300); 105 | } 106 | 107 | public function testServeResizeHeight() 108 | { 109 | $url = $this->image->url($this->imagePath, null, 300); 110 | $response = $this->call('GET', $url); 111 | 112 | $this->assertTrue($response->isOk()); 113 | 114 | $sizeManipulated = getimagesizefromstring($response->getContent()); 115 | $this->assertEquals($sizeManipulated[1], 300); 116 | } 117 | 118 | public function testServeResizeCrop() 119 | { 120 | //Both height and width with crop 121 | $url = $this->image->url($this->imagePath, 300, 300, array( 122 | 'crop' => true 123 | )); 124 | $response = $this->call('GET', $url); 125 | 126 | $this->assertTrue($response->isOk()); 127 | 128 | $sizeManipulated = getimagesizefromstring($response->getContent()); 129 | $this->assertEquals($sizeManipulated[0], 300); 130 | $this->assertEquals($sizeManipulated[1], 300); 131 | } 132 | 133 | public function testServeResizeCropSmall() 134 | { 135 | //Both height and width with crop 136 | $url = $this->image->url($this->imageSmallPath, 300, 300, array( 137 | 'crop' => true 138 | )); 139 | $response = $this->call('GET', $url); 140 | 141 | $this->assertTrue($response->isOk()); 142 | 143 | $sizeManipulated = getimagesizefromstring($response->getContent()); 144 | $this->assertEquals($sizeManipulated[0], 300); 145 | $this->assertEquals($sizeManipulated[1], 300); 146 | } 147 | 148 | public function testServeWrongParameter() 149 | { 150 | $url = $this->image->url($this->imagePath, 300, 300, array( 151 | 'crop' => true, 152 | 'wrong' => true 153 | )); 154 | 155 | try { 156 | $response = $this->call('GET', $url); 157 | $this->assertSame(404, $response->getStatusCode()); 158 | } 159 | catch(\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) 160 | { 161 | $this->assertInstanceOf('\Symfony\Component\HttpKernel\Exception\NotFoundHttpException', $e); 162 | } 163 | } 164 | 165 | public function testServeWrongFile() 166 | { 167 | $url = $this->image->url('/wrong123.jpg', 300, 300, array( 168 | 'crop' => true, 169 | 'wrong' => true 170 | )); 171 | 172 | try { 173 | $response = $this->call('GET', $url); 174 | $this->assertSame(404, $response->getStatusCode()); 175 | } 176 | catch(\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) 177 | { 178 | $this->assertInstanceOf('\Symfony\Component\HttpKernel\Exception\NotFoundHttpException', $e); 179 | } 180 | } 181 | 182 | public function testServeWrongFormat() 183 | { 184 | $url = $this->image->url('/wrong.jpg', 300, 300, array( 185 | 'crop' => true 186 | )); 187 | 188 | try { 189 | $response = $this->call('GET', $url); 190 | $this->assertSame(500, $response->getStatusCode()); 191 | } 192 | catch(\Symfony\Component\HttpKernel\Exception\HttpException $e) 193 | { 194 | $this->assertInstanceOf('\Symfony\Component\HttpKernel\Exception\HttpException', $e); 195 | } 196 | } 197 | 198 | /** 199 | * Define environment setup. 200 | * 201 | * @param \Illuminate\Foundation\Application $app 202 | * @return void 203 | */ 204 | protected function getEnvironmentSetUp($app) 205 | { 206 | $app->instance('path.public', __DIR__.'/fixture'); 207 | } 208 | 209 | protected function getPackageProviders($app) 210 | { 211 | return array('Folklore\Image\ImageServiceProvider'); 212 | } 213 | 214 | protected function getPackageAliases($app) 215 | { 216 | return array( 217 | 'Image' => 'Folklore\Image\Facades\Image' 218 | ); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /tests/ImageTestCase.php: -------------------------------------------------------------------------------- 1 | image = $this->app['image']; 19 | $this->imageSize = getimagesize(public_path().$this->imagePath); 20 | $this->imageSmallSize = getimagesize(public_path().$this->imageSmallPath); 21 | } 22 | 23 | public function tearDown() 24 | { 25 | $customPath = $this->app['path.public'].'/custom'; 26 | $this->app['config']->set('image.write_path', $customPath); 27 | 28 | $this->image->deleteManipulated($this->imagePath); 29 | 30 | parent::tearDown(); 31 | } 32 | 33 | public function testURLisValid() 34 | { 35 | 36 | $patterns = array( 37 | array( 38 | 'url_parameter' => null 39 | ), 40 | array( 41 | 'url_parameter' => '-image({options})', 42 | 'url_parameter_separator' => '-' 43 | ), 44 | array( 45 | 'url_parameter' => '-i-{options}', 46 | 'url_parameter_separator' => '-' 47 | ), 48 | array( 49 | 'url_parameter' => '/i/{options}', 50 | 'url_parameter_separator' => '/' 51 | ) 52 | ); 53 | 54 | foreach ($patterns as $pattern) { 55 | $options = array( 56 | 'grayscale', 57 | 'crop' => true, 58 | 'colorize' => 'FFCC00' 59 | ); 60 | $url = $this->image->url($this->imagePath, 300, 300, array_merge($pattern, $options)); 61 | 62 | //Check against pattern 63 | $urlMatch = preg_match('#'.$this->image->pattern($pattern['url_parameter']).'#', $url, $matches); 64 | $this->assertEquals($urlMatch, 1); 65 | 66 | //Check path 67 | $parsedPath = $this->image->parse($url, $pattern); 68 | $this->assertEquals($parsedPath['path'], $this->imagePath); 69 | 70 | //Check options 71 | foreach ($options as $key => $value) { 72 | if (is_numeric($key)) { 73 | $this->assertTrue($parsedPath['options'][$value]); 74 | } else { 75 | $this->assertEquals($parsedPath['options'][$key], $value); 76 | } 77 | } 78 | 79 | } 80 | } 81 | 82 | /** 83 | * Define environment setup. 84 | * 85 | * @param \Illuminate\Foundation\Application $app 86 | * @return void 87 | */ 88 | protected function getEnvironmentSetUp($app) 89 | { 90 | $app->instance('path.public', __DIR__.'/fixture'); 91 | } 92 | 93 | protected function getPackageProviders($app) 94 | { 95 | return array('Folklore\Image\ImageServiceProvider'); 96 | } 97 | 98 | protected function getPackageAliases($app) 99 | { 100 | return array( 101 | 'Image' => 'Folklore\Image\Facades\Image' 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/fixture/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/fixture/custom/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/fixture/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folkloreinc/laravel-image-legacy/146ba2710575c1f21f3b0eb1193af4ca19ac5f36/tests/fixture/image.jpg -------------------------------------------------------------------------------- /tests/fixture/image_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folkloreinc/laravel-image-legacy/146ba2710575c1f21f3b0eb1193af4ca19ac5f36/tests/fixture/image_small.jpg -------------------------------------------------------------------------------- /tests/fixture/wrong.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folkloreinc/laravel-image-legacy/146ba2710575c1f21f3b0eb1193af4ca19ac5f36/tests/fixture/wrong.jpg --------------------------------------------------------------------------------