├── examples ├── output │ └── .gitkeep ├── files │ ├── peke.gif │ ├── peke.jpg │ ├── test.png │ ├── tripi.jpg │ ├── watermark.png │ └── LICENSE ├── config.php ├── Watermark.php ├── Image.php └── Watimage.php ├── tests ├── visual │ ├── files │ │ ├── image.gif │ │ ├── image.jpg │ │ ├── image.png │ │ ├── watermark.gif │ │ └── watermark.png │ ├── run_them_all.php │ ├── flip.php │ ├── rotate.php │ ├── watermark.php │ └── resize.php ├── bootstrap.php └── TestCase │ ├── WatimageTest.php │ ├── TestCaseBase.php │ ├── WatermarkTest.php │ ├── NormalizeTest.php │ └── ImageTest.php ├── .gitignore ├── .editorconfig ├── src ├── Exception │ ├── InvalidExtensionException.php │ ├── FileNotExistException.php │ ├── InvalidMimeException.php │ ├── ExtensionNotLoadedException.php │ └── InvalidArgumentException.php ├── Watermark.php ├── Watimage.php ├── Normalize.php └── Image.php ├── phpunit.xml ├── .codeclimate.yml ├── generate-api.sh ├── composer.json ├── phpmd.xml ├── .travis.yml ├── LICENSE.md └── README.md /examples/output/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/files/peke.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/examples/files/peke.gif -------------------------------------------------------------------------------- /examples/files/peke.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/examples/files/peke.jpg -------------------------------------------------------------------------------- /examples/files/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/examples/files/test.png -------------------------------------------------------------------------------- /examples/files/tripi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/examples/files/tripi.jpg -------------------------------------------------------------------------------- /examples/files/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/examples/files/watermark.png -------------------------------------------------------------------------------- /tests/visual/files/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/tests/visual/files/image.gif -------------------------------------------------------------------------------- /tests/visual/files/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/tests/visual/files/image.jpg -------------------------------------------------------------------------------- /tests/visual/files/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/tests/visual/files/image.png -------------------------------------------------------------------------------- /tests/visual/files/watermark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/tests/visual/files/watermark.gif -------------------------------------------------------------------------------- /tests/visual/files/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elboletaire/Watimage/HEAD/tests/visual/files/watermark.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | /vendor/ 3 | composer.lock 4 | 5 | # Tests and examples 6 | /tests/visual/results/ 7 | /examples/output/ 8 | /build/ 9 | 10 | -------------------------------------------------------------------------------- /examples/files/LICENSE: -------------------------------------------------------------------------------- 1 | All pictures included in this project are licensed under a Creative Commons 2 | by-nc-sa 4.0 License. 3 | 4 | Image Credits: Òscar Casajuana 5 | -------------------------------------------------------------------------------- /tests/visual/run_them_all.php: -------------------------------------------------------------------------------- 1 | testClass = new Watimage; 11 | 12 | parent::setUp(); 13 | } 14 | 15 | public function testSetWatermark() 16 | { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exception/FileNotExistException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ./src/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | phpmd: 3 | enabled: true 4 | config: 5 | rulesets: "phpmd.xml" 6 | fixme: 7 | enabled: true 8 | config: 9 | strings: 10 | - TODO 11 | - FIXME 12 | - BUG 13 | duplication: 14 | enabled: true 15 | config: 16 | languages: 17 | - php 18 | phpcodesniffer: 19 | enabled: true 20 | config: 21 | standard: "PSR1,PSR2" 22 | ratings: 23 | paths: 24 | - "src" 25 | exclude_paths: 26 | - ".codeclimate.yml" 27 | - "**/tests/**/*" 28 | - "**/examples/**/*" 29 | - "**/vendor/**/*" 30 | - "**/build/**/*" 31 | -------------------------------------------------------------------------------- /generate-api.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get ApiGen.phar 4 | wget http://www.apigen.org/apigen.phar 5 | 6 | # Set identity 7 | git config --global user.email "travis@travis-ci.org" 8 | git config --global user.name "Travis" 9 | 10 | # Init project 11 | git clone --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/elboletaire/Watimage.git ../gh-pages 12 | rm -fr ../gh-pages/api 13 | 14 | # Generate Api 15 | php apigen.phar generate \ 16 | --source src \ 17 | --destination ../gh-pages/api \ 18 | --template-theme bootstrap \ 19 | --title "Watimage API" \ 20 | --google-analytics "UA-70920203-3" 21 | 22 | cd ../gh-pages/ 23 | 24 | # Push generated files 25 | git add . 26 | git commit -m "API updated" 27 | git push origin gh-pages -q > /dev/null 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elboletaire/watimage", 3 | "description": "Watermark and Image PHP class", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Òscar Casajuana", 8 | "email": "elboletaire@underave.net" 9 | } 10 | ], 11 | "minimum-stability": "stable", 12 | "require": { 13 | "php": ">=5.4.0", 14 | "ext-gd": "*" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Elboletaire\\Watimage\\": "src" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Elboletaire\\Watimage\\Test\\": "tests/" 24 | } 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "*", 28 | "squizlabs/php_codesniffer": "*" 29 | }, 30 | "suggest": { 31 | "thephpleague/color-extractor": "Extract colors from an image like a human would do." 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | Custom rule set for Watimage. Basically adds exceptions for the 11 | naming ShortVariable ruleset. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/visual/flip.php: -------------------------------------------------------------------------------- 1 | setImage(array('file' => "files/{$image}", 'quality' => 90)); // file to use and export quality 17 | $wm->flip($mode); 18 | if (!$wm->generate("results/{$output_image}")) { 19 | // handle errors... 20 | print_r($wm->errors); 21 | } 22 | } 23 | 24 | // Flip horizontally 25 | flipImage('image.jpg', 'horizontal', 'flip_jpg_horizontally.jpg'); 26 | // Flip vertically 27 | flipImage('image.jpg', 'vertical', 'flip_jpg_vertically.jpg'); 28 | // Flip on both axis 29 | flipImage('image.jpg', 'both', 'flip_jpg_both.jpg'); 30 | 31 | ############################ 32 | ### Check transparencies ### 33 | ############################ 34 | 35 | // Flip a png image 36 | flipImage('image.png', 'horizontal', 'flip_png_horizontally.png'); 37 | // Flip a gif image. 38 | flipImage('image.gif', 'horizontal', 'flip_gif_horizontally.gif'); 39 | 40 | echo "All images have been flipped.\n"; 41 | -------------------------------------------------------------------------------- /tests/visual/rotate.php: -------------------------------------------------------------------------------- 1 | setImage(array('file' => "files/{$image}", 'quality' => 90)); // file to use and export quality 18 | $wm->rotate($deg); 19 | if (!$wm->generate("results/{$output_image}")) { 20 | // handle errors... 21 | print_r($wm->errors); 22 | } 23 | } 24 | 25 | rotateImage('image.jpg', 45, 'rotate_jpg_45.jpg'); 26 | rotateImage('image.jpg', 90, 'rotate_jpg_90.jpg'); 27 | rotateImage('image.jpg', 180, 'rotate_jpg_180.jpg'); 28 | rotateImage('image.jpg', 270, 'rotate_jpg_270.jpg'); 29 | 30 | ############################ 31 | ### Check transparencies ### 32 | ############################ 33 | 34 | // Rotate png 45 deg 35 | rotateImage('image.png', 45, 'rotate_png_45.png'); 36 | 37 | // Rotate gif 45 deg [currently failing] 38 | rotateImage('image.gif', 45, 'rotate_gif_45.gif'); 39 | rotateImage('image.gif', 90, 'rotate_gif_90.gif'); 40 | 41 | echo "All images have been rotated.\n"; 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | 8 | env: 9 | global: 10 | - DEFAULT=1 11 | - secure: YIIpFV5hgzQf8uUzbb8KdS2u7gOh80i0tVJbg+nRAVLF83tF2fA7wMFFSTpTUGAJyaoQQt40pkCPqLTUAR1hj/P0EltAdeSSlKRy3od9z668wW/9SemZHhpxQ3kDHEHT4xetzsw9o5T3xvHCH3pRh8dPa23arhvJ60X0URW+KyU= 12 | 13 | matrix: 14 | include: 15 | - php: 5.6 16 | env: PHPCS=1 DEFAULT=0 17 | - php: 5.6 18 | env: COVERALLS=1 DEFAULT=0 19 | 20 | install: 21 | - composer self-update 22 | - composer install --prefer-dist --no-interaction --dev 23 | 24 | before_script: 25 | - sh -c "if [ '$COVERALLS' = '1' ]; then composer require --dev satooshi/php-coveralls:dev-master; fi" 26 | - sh -c "if [ '$COVERALLS' = '1' ]; then mkdir -p build/logs; fi" 27 | 28 | script: 29 | - sh -c "if [ '$DEFAULT' = '1' ]; then phpunit -d memory_limit=512M; fi" 30 | - sh -c "if [ '$PHPCS' = '1' ]; then ./vendor/bin/phpcs -p --extensions=php --standard=psr1,psr2 ./src; fi" 31 | - sh -c "if [ '$COVERALLS' = '1' ]; then phpunit -d memory_limit=512M --coverage-clover build/logs/clover.xml; fi" 32 | - sh -c "if [ '$COVERALLS' = '1' ]; then vendor/bin/coveralls -c .coveralls.yml -v; fi" 33 | 34 | after_success: 35 | - "if [ $TRAVIS_PHP_VERSION = '5.6' ] && [ $TRAVIS_BRANCH = 'master' ] && [ $TRAVIS_PULL_REQUEST = 'false' ]; then sh generate-api.sh; fi" 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | WATIMAGE LICENSE 2 | ================ 3 | 4 | All the images given in this repository (for examples and testing) have a 5 | Creative Commons by-nc-sa 4.0 License. 6 | 7 | Since version 2.0 all the other non-image-files are licensed under the MIT 8 | license: 9 | 10 | The MIT License (MIT) 11 | --------------------- 12 | 13 | Copyright © 2015-2016 Òscar Casajuana `` 14 | 15 | Permission is hereby granted, free of charge, to any person 16 | obtaining a copy of this software and associated documentation 17 | files (the “Software”), to deal in the Software without 18 | restriction, including without limitation the rights to use, 19 | copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the 21 | Software is furnished to do so, subject to the following 22 | conditions: 23 | 24 | The above copyright notice and this permission notice shall be 25 | included in all copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 34 | OTHER DEALINGS IN THE SOFTWARE. 35 | -------------------------------------------------------------------------------- /tests/visual/watermark.php: -------------------------------------------------------------------------------- 1 | setImage(array('file' => "files/{$image}", 'quality' => 90)); // file to use and export quality 18 | $wm->setWatermark(array('file' => "files/{$watermark}", 'position' => 'center')); // watermark to use and its position 19 | $wm->applyWatermark(); // apply watermark to the canvas 20 | if (!$wm->generate("results/{$output_image}")) { 21 | // handle errors... 22 | print_r($wm->errors); 23 | } 24 | } 25 | 26 | // Watermark a jpg file with a png 27 | applyWatermark('image.jpg', 'watermark.png', 'watermark_jpg_with_png.jpg'); 28 | // Watermark a jpg file with a gif 29 | applyWatermark('image.jpg', 'watermark.gif', 'watermark_jpg_with_gif.jpg'); 30 | 31 | // Watermark a png file with a png 32 | applyWatermark('image.png', 'watermark.png', 'watermark_png_with_png.png'); 33 | // Watermark a png file with a gif 34 | applyWatermark('image.png', 'watermark.gif', 'watermark_png_with_gif.png'); 35 | 36 | // Watermark a gif file with a png 37 | applyWatermark('image.gif', 'watermark.png', 'watermark_gif_with_png.gif'); 38 | // Watermark a gif file with a gif 39 | applyWatermark('image.gif', 'watermark.gif', 'watermark_gif_with_gif.gif'); 40 | 41 | echo "All images have been watermarked.\n"; 42 | -------------------------------------------------------------------------------- /tests/TestCase/TestCaseBase.php: -------------------------------------------------------------------------------- 1 | reflection = new ReflectionClass($this->testClass); 18 | 19 | $this->files_path = realpath(dirname(__FILE__) . '/../../examples/files'); 20 | $this->files_path .= DIRECTORY_SEPARATOR; 21 | 22 | $this->output_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR; 23 | } 24 | 25 | public function getMethod($method) 26 | { 27 | $method = $this->reflection->getMethod($method); 28 | $method->setAccessible(true); 29 | 30 | return $method; 31 | } 32 | 33 | public function getProperty($property) 34 | { 35 | $property = $this->reflection->getProperty($property); 36 | $property->setAccessible(true); 37 | 38 | return $property->getValue($this->testClass); 39 | } 40 | 41 | public function setProperty($property, $value) 42 | { 43 | $property = $this->reflection->getProperty($property); 44 | $property->setAccessible(true); 45 | 46 | return $property->setValue($this->testClass, $value); 47 | } 48 | 49 | public function getOutputFilename($filename) 50 | { 51 | $filename = pathinfo($filename); 52 | $random = substr(md5(time()), 0, 10); 53 | return "{$this->output_path}watimage-{$filename['filename']}-{$random}.{$filename['extension']}"; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/visual/resize.php: -------------------------------------------------------------------------------- 1 | setImage(array('file' => "files/{$image}", 'quality' => 90)); // file to use and export quality 18 | $wm->resize(array('type' => $crop_type, 'size' => 400)); 19 | if (!$wm->generate("results/{$output_image}")) { 20 | // handle errors... 21 | print_r($wm->errors); 22 | } 23 | } 24 | 25 | function resizeAndWatermarkImage($image, $output_image) 26 | { 27 | $wm = new Watimage(); 28 | $wm->setImage(array('file' => "files/{$image}", 'quality' => 90)); // file to use and export quality 29 | $wm->setWatermark(array('file' => "files/watermark.png", 'position' => 'center')); // watermark to use and its position 30 | $wm->resize(array('type' => 'resizemin', 'size' => 400)); 31 | $wm->applyWatermark(); // apply watermark to the canvas 32 | if (!$wm->generate("results/{$output_image}")) { 33 | // handle errors... 34 | print_r($wm->errors); 35 | } 36 | } 37 | 38 | // Reizing a jpg file with resizecrop 39 | resizeImage('image.jpg', 'resizecrop', 'resize_jpg_with_resizecrop.jpg'); 40 | // Reizing a jpg file with resizemin 41 | resizeImage('image.jpg', 'resizemin', 'resize_jpg_with_resizemin.jpg'); 42 | // Reizing a jpg file with resize 43 | resizeImage('image.jpg', 'resize', 'resize_jpg_with_resize.jpg'); 44 | // Reizing a jpg file with reduce 45 | resizeImage('image.jpg', 'reduce', 'resize_jpg_with_reduce.jpg'); 46 | // Reizing a jpg file with crop 47 | resizeImage('image.jpg', 'crop', 'resize_jpg_with_crop.jpg'); 48 | 49 | // Resizing a png file 50 | resizeImage('image.png', 'resizemin', 'resize_png.png'); 51 | // Resizing a gif file [currently failing] 52 | resizeImage('image.gif', 'resizemin', 'resize_gif.gif'); 53 | 54 | echo "Images have been rotated.\n"; 55 | 56 | // Resize a png image and apply watermark 57 | resizeAndWatermarkImage('image.png', 'resize_and_watermark_png.png'); 58 | // Resize a gif image and apply watermark [currently failing due to resize issue] 59 | resizeAndWatermarkImage('image.gif', 'resize_and_watermark_gif.gif'); 60 | 61 | echo "Resizing and applying watermarks ended as well.\n"; 62 | -------------------------------------------------------------------------------- /examples/Watermark.php: -------------------------------------------------------------------------------- 1 | setPosition('bottom right') 18 | ->apply($image) 19 | // apply method returns the Image instance (not the Watermark) 20 | // that's why we can directly generate 21 | ->generate(OUTPUT . 'watermark1-apply.png'); 22 | 23 | /************************** 24 | *** ALTERING WATERMARK *** 25 | **************************/ 26 | $image = new Image($image_file); 27 | $watermark = new Watermark($watermark_file); 28 | $watermark 29 | // Watermark extends Image, so we can use any method from there to 30 | // modify the watermark 31 | ->rotate(90) 32 | ->negate() 33 | ->setPosition('bottom right') 34 | ->apply($image) 35 | ->generate(OUTPUT . 'watermark2-rotate.png'); 36 | 37 | /*************************** 38 | *** EVERYTHING TOGETHER *** 39 | ***************************/ 40 | $image = new Image($image_file); 41 | $watermark = new Watermark($watermark_file); 42 | $watermark 43 | // let's rotate the watermark just 45 deg. 44 | ->rotate(45) 45 | ->setPosition('bottom right') 46 | // change its size 47 | ->setSize(250, 90) 48 | // set a margin 49 | ->setMargin(-20) 50 | // and apply to the image 51 | ->apply($image) 52 | ; 53 | 54 | // Apply a second watermark 55 | $watermark 56 | // but this one top left 57 | ->setPosition('top left') 58 | // and adjust its margin to the new position 59 | ->setMargin(20) 60 | // then apply it 61 | ->apply($image) 62 | ; 63 | 64 | $image 65 | // Then rotate the image 66 | ->rotate(45) 67 | // and resize it 68 | ->resize('min', 400) 69 | ; 70 | 71 | $watermark 72 | // Rotate the watermark 73 | ->rotate(-45) 74 | // resize it 75 | ->resize('resize', 400) 76 | // change its position 77 | ->setPosition('centered') 78 | // and apply it 79 | ->apply($image); 80 | ; 81 | 82 | // Create a new Watermark object to watermark the resulting file 83 | $watermark = new Watermark($watermark_file); 84 | $watermark->setPosition('bottom right')->apply($image); 85 | 86 | // generate the resulting image 87 | $image->generate(OUTPUT . 'watermark3-all-together.png'); 88 | 89 | echo "All examples are now available under the 'output' folder\n"; 90 | // END OF FILE 91 | -------------------------------------------------------------------------------- /examples/Image.php: -------------------------------------------------------------------------------- 1 | resize('resizecrop', 400, 200) 15 | ->generate(OUTPUT . 'image1-resizecrop.jpg'); 16 | 17 | /********************* 18 | *** ROTATE IMAGES *** 19 | *********************/ 20 | $image = new Image($image_file); 21 | // check out Normalize::color to see all the allowed possibilities about how 22 | // to set colors. Angle must be specified in degrees (positive is clockwise) 23 | $image->rotate(90, '#fff') 24 | ->generate(OUTPUT . 'image2-rotate.jpg'); 25 | // Images are automatically orientated by default when using the constructor 26 | // instead of load. You can skip it and you can also auto-orientate images later: 27 | 28 | // disable auto-orientate on load 29 | $image = new Image($orientate, false); 30 | // we can later use the autoOrientate method if we not did it previously: 31 | $image->autoOrientate() 32 | ->generate(OUTPUT . 'image3-auto-orientate.jpg'); 33 | 34 | /********************************** 35 | *** EXPORTING TO OTHER FORMATS *** 36 | **********************************/ 37 | $image = new Image($image_file); 38 | $image->generate(OUTPUT . 'image4-formats.png', 'image/png'); 39 | 40 | /******************* 41 | *** FLIP IMAGES *** 42 | *******************/ 43 | $image = new Image($image_file); 44 | // vertical [or y, or v], horizontal [or x, or h] 45 | // check out Normalize::flip to see all the allowed possibilities 46 | $image->flip('vertical') 47 | ->generate(OUTPUT . 'image5-flip.jpg'); 48 | 49 | /*********************** 50 | *** CROPPING IMAGES *** 51 | ***********************/ 52 | // Usefull for cropping plugins like https://github.com/tapmodo/Jcrop 53 | $image = new Image($image_file); 54 | // Values from the cropper 55 | // check out Normalize::crop to see all the allowed possibilities 56 | $image->crop([ 57 | 'width' => 500, // the cropped width 58 | 'height' => 500, // " " height 59 | 'x' => 50, 60 | 'y' => 80 61 | ]) 62 | ->generate(OUTPUT . 'image6-crop.jpg'); 63 | 64 | /************************ 65 | *** APPLYING FILTERS *** 66 | ************************/ 67 | 68 | $image = new Image($image_file); 69 | $image 70 | // ->edgeDetection() 71 | ->blur() 72 | ->sepia() 73 | ->pixelate(3, true) 74 | // ->brightness(10) 75 | // ->contrast(10) 76 | // ->colorize('#f00') 77 | // ->emboss() 78 | // ->meanRemove() 79 | // ->negate() 80 | ->vignette() 81 | ->generate(OUTPUT . 'image7-effects.jpg'); 82 | 83 | /******************************** 84 | *** DIRECTLY TREATING IMAGES *** 85 | ********************************/ 86 | 87 | $image = new Image($image_file); 88 | // Get the resource image 89 | $resource = $image->getImage(); 90 | // Add a string as a note in the top left side 91 | $color = imagecolorallocate($resource, 0, 0, 0); 92 | imagestring($resource, 5, 10, 10, "My cat, peke", $color); 93 | // Return the image resource to the Image instance 94 | $image->setImage($resource) 95 | // and save 96 | ->generate(OUTPUT . 'image8-treating-images.jpg'); 97 | 98 | echo "All examples are now available under the 'output' folder\n"; 99 | // END OF FILE 100 | -------------------------------------------------------------------------------- /examples/Watimage.php: -------------------------------------------------------------------------------- 1 | setImage(array('file' => $image, 'quality' => 70)); // file to use and export quality 14 | $wm->setWatermark(array('file' => $watermark, 'position' => 'top right')); // watermark to use and its position 15 | $wm->applyWatermark(); // apply watermark to the canvas 16 | if ( !$wm->generate(OUTPUT . 'test1.png') ) { 17 | // handle errors... 18 | print_r($wm->errors); 19 | } 20 | 21 | /********************* 22 | *** RESIZE IMAGES *** 23 | *********************/ 24 | $wm = new Watimage($image); 25 | // allowed types: resize, resizecrop, resizemin, crop and reduce 26 | $wm->resize(array('type' => 'resizecrop', 'size' => array(400, 200))); 27 | if ( !$wm->generate(OUTPUT . 'test2.png') ) { 28 | // handle errors... 29 | print_r($wm->errors); 30 | } 31 | 32 | 33 | /********************* 34 | *** ROTATE IMAGES *** 35 | *********************/ 36 | $wm = new Watimage($image); 37 | $wm->rotate(90); 38 | if ( !$wm->generate(OUTPUT . 'test3.png') ) { 39 | // handle errors... 40 | print_r($wm->errors); 41 | } 42 | 43 | /********************************** 44 | *** EXPORTING TO OTHER FORMATS *** 45 | **********************************/ 46 | $wm = new Watimage($image); 47 | if ( !$wm->generate(OUTPUT . 'test4.jpg', 'image/jpeg') ) { 48 | // handle errors... 49 | print_r($wm->errors); 50 | } 51 | 52 | /******************* 53 | *** FLIP IMAGES *** 54 | *******************/ 55 | $wm = new Watimage($image); 56 | $wm->flip('vertical'); // or "horizontal" 57 | if ( !$wm->generate(OUTPUT . 'test5.png') ) { 58 | // handle errors... 59 | print_r($wm->errors); 60 | } 61 | 62 | 63 | /*********************** 64 | *** CROPPING IMAGES *** 65 | ***********************/ 66 | // Usefull for cropping plugins like https://github.com/tapmodo/Jcrop 67 | $wm = new Watimage($image); 68 | $wm->crop(array( // values from the cropper 69 | 'width' => 500, // the cropped width 70 | 'height' => 500, // " " height 71 | 'x' => 50, 72 | 'y' => 80 73 | )); 74 | if ( !$wm->generate(OUTPUT . 'test6.png') ) { 75 | // handle errors... 76 | print_r($wm->errors); 77 | } 78 | 79 | 80 | /*************************** 81 | *** EVERYTHING TOGETHER *** 82 | ***************************/ 83 | 84 | $wm = new Watimage(); 85 | 86 | // Set the image 87 | $wm->setImage($image); // you can also set the quality with setImage, you only need to change it with an array: array('file' => $image, 'quality' => 70) 88 | 89 | // Set the export quality 90 | $wm->setQuality(80); 91 | 92 | // Set a watermark 93 | $wm->setWatermark(array( 94 | 'file' => $watermark, // the watermark file 95 | 'position' => 'center center', // the watermark position works like CSS backgrounds positioning 96 | 'margin' => array('x' => -20, 'y' => 10), // you can set some 'margins' to the watermark for better positioning 97 | 'size' => 'full' // you can set the size of the watermark using a percentage or the word "full" for getting a full width/height watermark 98 | )); 99 | 100 | // Resize the image 101 | $wm->resize(array('type' => 'resize', 'size' => 400)); 102 | 103 | // Flip it 104 | $wm->flip('horizontal'); 105 | 106 | // Now rotate it 30deg 107 | $wm->rotate(30); 108 | 109 | // It's time to apply the watermark 110 | $wm->applyWatermark(); 111 | 112 | // Export the file 113 | if ( !$wm->generate(OUTPUT . 'test7.png') ) { 114 | // handle errors... 115 | print_r($wm->errors); 116 | } 117 | 118 | 119 | echo "All examples are now available under the 'output' folder\n"; 120 | // END OF FILE 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Watimage: GD Image Helper PHP Class 2 | =================================== 3 | 4 | [![Build status](https://img.shields.io/travis/elboletaire/Watimage.svg?style=flat-square)](https://travis-ci.org/elboletaire/Watimage) 5 | [![Code coverage](https://img.shields.io/coveralls/elboletaire/Watimage.svg?style=flat-square)](https://coveralls.io/github/elboletaire/Watimage) 6 | [![License](https://img.shields.io/packagist/l/elboletaire/Watimage.svg?style=flat-square)](https://github.com/elboletaire/Watimage/blob/master/LICENSE.md) 7 | [![Latest Stable Version](https://img.shields.io/github/release/elboletaire/Watimage.svg?style=flat-square)](https://github.com/elboletaire/Watimage/releases) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/elboletaire/Watimage.svg?style=flat-square)](https://packagist.org/packages/elboletaire/Watimage) 9 | [![Code Climate](https://img.shields.io/codeclimate/github/elboletaire/Watimage.svg?style=flat-square)](https://codeclimate.com/github/elboletaire/Watimage) 10 | 11 | Watimage is a group of PHP classes to help you resize, rotate, apply filters, 12 | watermarks and a lot more of things to your images using the PHP's GD library. 13 | 14 | It was initially a CakePHP component, later became a simple Vendor class and now 15 | is a powerful set of classes to alter images. 16 | 17 | And it maintains the transparencies in every scenario. 18 | 19 | Requirements 20 | ------------ 21 | 22 | You need php > 5.4 and the [php GD](http://php.net/manual/book.image.php) 23 | package installed. 24 | 25 | With aptitude: 26 | 27 | ```bash 28 | sudo apt-get install php5-gd 29 | ``` 30 | 31 | With yum: 32 | 33 | ```bash 34 | sudo yum install php-gd 35 | ``` 36 | 37 | Installing 38 | ---------- 39 | 40 | With composer: 41 | 42 | ```bash 43 | composer require elboletaire/watimage:~2.0.0 44 | ``` 45 | 46 | Check out the Watimage pages for 47 | [more information about installation](https://elboletaire.github.io/Watimage/usage/setup). 48 | 49 | Usage 50 | ----- 51 | 52 | See all the information you need to know 53 | [at the Watimage pages](https://elboletaire.github.io/Watimage/usage) 54 | 55 | API 56 | --- 57 | 58 | Check out [the API](https://elboletaire.github.io/Watimage/api) 59 | [at the Watimage pages](https://elboletaire.github.io/Watimage/usage). 60 | 61 | Examples 62 | -------- 63 | 64 | There are lot of examples at the `examples` folder, plus a lot more of examples 65 | and tutorials [in the Watimage pages](https://elboletaire.github.io/Watimage). 66 | 67 | Before running the given examples, you need to `composer install` from 68 | `Watimage` root folder in order to get composer's autoloader downloaded into 69 | `vendor` folder. 70 | 71 | ```bash 72 | composer install 73 | cd examples 74 | php Image.php 75 | php Watermark.php 76 | php Watimage.php 77 | ``` 78 | 79 | You can also put the `Watimage` folder in your local webhost webroot dir and 80 | [point there](http://localhost/Watimage/examples/Image.php) your browser. 81 | 82 | Testing 83 | ------- 84 | 85 | Please, see the information about testing in 86 | [the Watimage pages](https://elboletaire.github.io/Watimage/usage/testing). 87 | 88 | Patches & Features 89 | ------------------ 90 | 91 | + Fork 92 | + Mod, fix 93 | + Test - this is important, so it's not unintentionally broken 94 | + Commit - do not mess with license, todo, version, etc. (if you do change any, bump them into commits of 95 | their own that I can ignore when I pull) 96 | + Pull request - bonus point for topic branches 97 | 98 | Bugs & Feedback 99 | --------------- 100 | 101 | See the [issues section](https://github.com/elboletaire/Watimage/issues). 102 | 103 | Changelog 104 | --------- 105 | 106 | Check out the [releases on github](https://github.com/elboletaire/Watimage/releases). 107 | They have different order than expected because I've created tags recently to 108 | not loose them and to have the whole changelog there. 109 | 110 | LICENSE 111 | ------- 112 | 113 | See [LICENSE.md](./LICENSE.md). 114 | -------------------------------------------------------------------------------- /src/Watermark.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2015 Òscar Casajuana 10 | * @link https://github.com/elboletaire/Watimage 11 | * @license https://opensource.org/licenses/MIT MIT 12 | */ 13 | class Watermark extends Image 14 | { 15 | /** 16 | * Size param, used to resize the watermark. 17 | * 18 | * @var mixed Can either be string (`full` or a %) or the exact size (as integer or array) 19 | */ 20 | protected $size; 21 | 22 | /** 23 | * Watermark margin. 24 | * 25 | * @var array 26 | */ 27 | protected $margin = [0, 0]; 28 | 29 | /** 30 | * Position for the watermark. 31 | * 32 | * @var mixed Can either be a string ala CSS or the exact position (as integer or array) 33 | */ 34 | protected $position; 35 | 36 | /** 37 | * {@inheritdoc} 38 | * 39 | * @param string $file Filepath of the watermark to be loaded. 40 | * @param array $options Array of options to be set, with keys: size, 41 | * position and/or margin. 42 | */ 43 | public function __construct($file = null, $options = []) 44 | { 45 | if (!empty($options)) { 46 | foreach ($options as $option => $values) { 47 | $method = 'set' . ucfirst($option); 48 | if (!method_exists($this, $method)) { 49 | continue; 50 | } 51 | 52 | $this->$method($values); 53 | } 54 | } 55 | 56 | return parent::__construct($file); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | * 62 | * @return Watermark 63 | */ 64 | public function destroy() 65 | { 66 | $this->size = $this->position = null; 67 | $this->margin = [0, 0]; 68 | 69 | return parent::destroy(); 70 | } 71 | 72 | /** 73 | * Sets the position of the watermark. 74 | * 75 | * @param mixed $x Can be a position ala CSS, just position X or an array 76 | * containing both params. 77 | * @param int $y Position Y. 78 | * @return Watermark 79 | */ 80 | public function setPosition($x, $y = null) 81 | { 82 | $this->position = Normalize::cssPosition($x, $y); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Sets the size of the watermark. 89 | * 90 | * This method has been added for backwards compatibility. If you wanna resize 91 | * the watermark you can directly call ->resize from Watermark object. 92 | * 93 | * @param mixed $width Can be just width or an array containing both params. 94 | * @param int $height Height. 95 | * @return Watermark 96 | */ 97 | public function setSize($width, $height = null) 98 | { 99 | $this->size = Normalize::watermarkSize($width, $height); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Sets a margin for the watermark. Useful if you're using positioning ala CSS. 106 | * 107 | * @param mixed $x Can be just x position or an array containing both params. 108 | * @param int $y Y position. 109 | * @return Watermark 110 | */ 111 | public function setMargin($x, $y = null) 112 | { 113 | $this->margin = Normalize::margin($x, $y); 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Applies the watermark to the given image. 120 | * 121 | * @param Image $image The image where apply the watermark. 122 | * @return Image The resulting watermarked Image, so you can 123 | * do $watermark->apply($image)->generate(). 124 | */ 125 | public function apply(Image $image) 126 | { 127 | $metadata = $image->getMetadata(); 128 | $this->calculateSize($metadata); 129 | list($x, $y) = $this->calculatePosition($metadata); 130 | 131 | $resource = $this->imagecreate($metadata['width'], $metadata['height']); 132 | 133 | // @codingStandardsIgnoreStart 134 | imagecopyresampled( 135 | $resource, $image->getImage(), 136 | 0, 0, 0, 0, 137 | $metadata['width'], $metadata['height'], 138 | $metadata['width'], $metadata['height'] 139 | ); 140 | // @codingStandardsIgnoreEnd 141 | 142 | imagealphablending($resource, true); 143 | imagesavealpha($resource, false); 144 | 145 | // @codingStandardsIgnoreStart 146 | imagecopy( 147 | $resource, $this->image, 148 | $x, $y, 0, 0, 149 | $this->width, $this->height 150 | ); 151 | // @codingStandardsIgnoreEnd 152 | 153 | $image->setImage($resource); 154 | 155 | return $image; 156 | } 157 | 158 | /** 159 | * Calculates the position of the watermark. 160 | * 161 | * @param array $metadata Image to be watermarked metadata. 162 | * @return array Position in array x,y 163 | */ 164 | protected function calculatePosition($metadata) 165 | { 166 | // Force center alignement if 'full' size has been set 167 | if ($this->size == 'full') { 168 | $this->position = 'center center'; 169 | } 170 | 171 | if (is_array($this->position)) { 172 | return $this->position; 173 | } 174 | 175 | if (empty($this->position)) { 176 | $this->position = 'center center'; 177 | } 178 | 179 | $x = $y = 0; 180 | 181 | // Horizontal 182 | if (preg_match('/right/', $this->position)) { 183 | $x = $metadata['width'] - $this->width + $this->margin[0]; 184 | } elseif (preg_match('/left/', $this->position)) { 185 | $x = 0 + $this->margin[0]; 186 | } elseif (preg_match('/center/', $this->position)) { 187 | $x = $metadata['width'] / 2 - $this->width / 2 + $this->margin[0]; 188 | } 189 | 190 | // Vertical 191 | if (preg_match('/bottom/', $this->position)) { 192 | $y = $metadata['height'] - $this->height + $this->margin[1]; 193 | } elseif (preg_match('/top/', $this->position)) { 194 | $y = 0 + $this->margin[1]; 195 | } elseif (preg_match('/center/', $this->position)) { 196 | $y = $metadata['height'] / 2 - $this->height / 2 + $this->margin[1]; 197 | } 198 | 199 | return [$x, $y]; 200 | } 201 | 202 | /** 203 | * Calculates the required size for the watermark from $this->size. 204 | * 205 | * @param array $metadata Image metadata 206 | * @return void 207 | */ 208 | protected function calculateSize($metadata) 209 | { 210 | if (!isset($this->size)) { 211 | return; 212 | } 213 | 214 | if (is_array($this->size)) { 215 | list($width, $height) = $this->size; 216 | if ($width == $this->width && $height == $this->height) { 217 | return; 218 | } 219 | } elseif (preg_match('/[0-9]{1,3}%$/', $this->size)) { 220 | $ratio = $this->size / 100; 221 | 222 | $width = $this->width * $ratio; 223 | $height = $this->height * $ratio; 224 | } else { 225 | // size == 'full' 226 | $width = $this->width; 227 | $height = $this->height; 228 | 229 | if ($this->width > $metadata['width'] * 1.05 && $this->height > $metadata['height'] * 1.05) { 230 | // both are already larger than the original by at least 5%... 231 | // we need to make the watermark *smaller* for this one. 232 | // where is the largest difference? 233 | $wdiff = $width - $metadata['width']; 234 | $hdiff = $height - $metadata['height']; 235 | if ($wdiff > $hdiff) { 236 | // the width has the largest difference - get percentage 237 | $ratio = ($wdiff / $width) - 0.05; 238 | } else { 239 | $ratio = ($hdiff / $height) - 0.05; 240 | } 241 | $width -= $width * $ratio; 242 | $height -= $height * $ratio; 243 | } else { 244 | // the watermark will need to be enlarged for this one 245 | // where is the largest difference? 246 | $wdiff = $metadata['width'] - $width; 247 | $hdiff = $metadata['height'] - $height; 248 | if ($wdiff > $hdiff) { 249 | // the width has the largest difference - get percentage 250 | $ratio = ($wdiff / $width) + 0.05; 251 | } else { 252 | $ratio = ($hdiff / $height) + 0.05; 253 | } 254 | $width += $width * $ratio; 255 | $height += $height * $ratio; 256 | } 257 | } 258 | 259 | $this->size = [$width, $height]; 260 | // Resize watermark to desired size 261 | $this->classicResize($width, $height); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/Watimage.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright 2015 Òscar Casajuana 13 | * @link https://github.com/elboletaire/Watimage 14 | * @license https://opensource.org/licenses/MIT MIT 15 | */ 16 | class Watimage 17 | { 18 | /** 19 | * Image handler. 20 | * 21 | * @var Image 22 | */ 23 | protected $image; 24 | 25 | /** 26 | * Watermark handler. 27 | * 28 | * @var Watermark False if no watermark set, Watermark otherwise. 29 | */ 30 | protected $watermark = false; 31 | 32 | /** 33 | * Any error returned by the class will be stored here 34 | * @public array $errors 35 | */ 36 | public $errors = []; 37 | 38 | /** 39 | * Construct method. Accepts file and watermark as parameters so you 40 | * can avoid the setImage and setWatermark methods. 41 | * 42 | * @param mixed $file Image details if is an array, image file 43 | * location otherwise 44 | * @param mixed $watermark Watermark details if is an array, watermark file 45 | * location otherwise. 46 | */ 47 | public function __construct($file = null, $watermark = null) 48 | { 49 | $this->image = new Image($file); 50 | 51 | if (!is_null($watermark)) { 52 | $this->watermark = new Watermark(); 53 | // This setWatermark method is backwards compatible! 54 | $this->setWatermark($watermark); 55 | } 56 | } 57 | 58 | /** 59 | * Sets image and (optionally) its options 60 | * 61 | * @param mixed $filename Filename string or array containing both filename and quality 62 | * @return Watimage 63 | * @throws Exception 64 | */ 65 | public function setImage($filename) 66 | { 67 | try { 68 | $this->image->load($filename); 69 | 70 | return true; 71 | } catch (Exception $e) { 72 | array_push($this->errors, $e->getMessage()); 73 | 74 | return false; 75 | } 76 | } 77 | 78 | /** 79 | * Sets quality for gif and jpg files. 80 | * 81 | * @param int $quality A value from 0 (zero quality) to 100 (max quality). 82 | */ 83 | public function setQuality($quality) 84 | { 85 | try { 86 | $this->image->setQuality($quality); 87 | 88 | return true; 89 | } catch (Exception $e) { 90 | array_push($this->errors, $e->getMessage()); 91 | 92 | return false; 93 | } 94 | } 95 | 96 | /** 97 | * Sets compression for png files. 98 | * 99 | * @param int $compression A value from 0 (no compression, not recommended) to 9. 100 | */ 101 | public function setCompression($compression) 102 | { 103 | try { 104 | $this->image->setQuality($compression); 105 | 106 | return true; 107 | } catch (Exception $e) { 108 | array_push($this->errors, $e->getMessage()); 109 | 110 | return false; 111 | } 112 | } 113 | 114 | /** 115 | * Set watermark and (optionally) its options. 116 | * 117 | * @param mixed $options You can set the watermark without options or you can 118 | * set an array with any of these $options = [ 119 | * 'file' => 'watermark.png', 120 | * 'position' => 'bottom right', // default 121 | * 'margin' => ['20', '10'] // 0 by default, 122 | * 'size' => 'full' // 100% by default 123 | * ]; 124 | * @return true on success; false on failure 125 | */ 126 | public function setWatermark($options = []) 127 | { 128 | try { 129 | if (!$this->watermark) { 130 | $this->watermark = new Watermark(); 131 | } 132 | 133 | if (!is_array($options)) { 134 | $this->watermark->load($options); 135 | 136 | return true; 137 | } 138 | 139 | if (!isset($options['file'])) { 140 | throw new InvalidArgumentException("Watermark \"file\" param not specified"); 141 | } 142 | 143 | $this->watermark->load($options['file']); 144 | 145 | foreach (['position', 'margin', 'size'] as $option) { 146 | if (!array_key_exists($option, $options)) { 147 | continue; 148 | } 149 | 150 | $method = 'set' . ucfirst($option); 151 | $this->watermark->$method($options[$option]); 152 | } 153 | 154 | return true; 155 | } catch (Exception $e) { 156 | array_push($this->errors, $e->getMessage()); 157 | 158 | return false; 159 | } 160 | } 161 | 162 | /** 163 | * Resizes the image. 164 | * 165 | * @param array $options = [ 166 | * 'type' => 'resizemin|resizecrop|resize|crop|reduce', 167 | * 'size' => ['x' => 2000, 'y' => 500] 168 | * ] 169 | * You can also set the size without specifying x and y: 170 | * [2000, 500]. Or directly 'size' => 2000 (takes 2000x2000) 171 | * @return bool true on success; otherwise false 172 | */ 173 | public function resize($options = []) 174 | { 175 | try { 176 | $this->image->resize($options['type'], $options['size']); 177 | 178 | return true; 179 | } catch (Exception $e) { 180 | array_push($this->errors, $e->getMessage()); 181 | 182 | return false; 183 | } 184 | } 185 | 186 | /** 187 | * Crops an image based on specified coords and size. 188 | * 189 | * @param mixed $options Specifying x & y position and width & height, like 190 | * so [ 191 | * 'x' => 23, 192 | * 'y' => 23, 193 | * 'width' => 230, 194 | * 'height' => 230 195 | * ] 196 | * @return bool success 197 | */ 198 | public function crop($options = []) 199 | { 200 | try { 201 | $this->image->crop($options); 202 | 203 | return true; 204 | } catch (Exception $e) { 205 | array_push($this->errors, $e->getMessage()); 206 | 207 | return false; 208 | } 209 | } 210 | 211 | /** 212 | * Rotates an image. 213 | * 214 | * @param mixed $options Can either be an integer with the degrees or an array with 215 | * keys `bgcolor` for the rotation bgcolor and `degrees` for 216 | * the angle. 217 | * @return bool 218 | */ 219 | public function rotateImage($options = []) 220 | { 221 | try { 222 | if (is_array($options)) { 223 | if (empty($options['bgcolor'])) { 224 | $options['bgcolor'] = -1; 225 | } 226 | 227 | $this->image->rotate($options['degrees'], $options['bgcolor']); 228 | return true; 229 | } 230 | 231 | $this->image->rotate($options); 232 | return true; 233 | } catch (Exception $e) { 234 | array_push($this->errors, $e->getMessage()); 235 | 236 | return false; 237 | } 238 | } 239 | 240 | /** 241 | * rotateImage alias. 242 | * 243 | * @see self::rotateImage() 244 | */ 245 | public function rotate($options = []) 246 | { 247 | return $this->rotateImage($options); 248 | } 249 | 250 | /** 251 | * Applies a watermark to the image. Needs to be initialized with $this->setWatermark() 252 | * 253 | * @return true on success, otherwise false 254 | */ 255 | public function applyWatermark() 256 | { 257 | try { 258 | $this->watermark->apply($this->image); 259 | 260 | return true; 261 | } catch (Exception $e) { 262 | array_push($this->errors, $e->getMessage()); 263 | 264 | return false; 265 | } 266 | } 267 | 268 | /** 269 | * Flips an image. 270 | * 271 | * @param string $type type of flip: horizontal / vertical / both 272 | * @return true on success. Otherwise false 273 | */ 274 | public function flip($type = 'horizontal') 275 | { 276 | try { 277 | $this->image->flip($type); 278 | 279 | return true; 280 | } catch (Exception $e) { 281 | array_push($this->errors, $e->getMessage()); 282 | 283 | return false; 284 | } 285 | } 286 | 287 | /** 288 | * Generates the image file. 289 | * 290 | * @param string $path if not specified image will be printed on screen 291 | * @param string $output mime type for output image (image/png, image/gif, image/jpeg) 292 | * @return true on success. Otherwise false 293 | */ 294 | public function generate($path = null, $output = null) 295 | { 296 | try { 297 | $this->image->generate($path, $output); 298 | 299 | return true; 300 | } catch (Exception $e) { 301 | array_push($this->errors, $e->getMessage()); 302 | 303 | return false; 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /tests/TestCase/WatermarkTest.php: -------------------------------------------------------------------------------- 1 | testClass = new Watermark(); 12 | 13 | parent::setUp(); 14 | 15 | $this->testClass->load("{$this->files_path}/watermark.png"); 16 | } 17 | 18 | /** 19 | * @covers Elboletaire\Watimage\Watermark::destroy 20 | */ 21 | public function testDestroy() 22 | { 23 | $instance = $this->testClass 24 | ->setSize("150%") 25 | ->setPosition("centered") 26 | ->setMargin(20) 27 | ->destroy() 28 | ; 29 | 30 | $this->assertInstanceOf('Elboletaire\Watimage\Watermark', $instance); 31 | 32 | $this->assertNull($this->getProperty('position')); 33 | $this->assertNull($this->getProperty('size')); 34 | $this->assertArraySubset([0, 0], $this->getProperty('margin')); 35 | } 36 | 37 | /** 38 | * @covers Elboletaire\Watimage\Watermark::setPosition 39 | */ 40 | public function testSetPosition() 41 | { 42 | $instance = $this->testClass->setPosition(23); 43 | $this->assertNotNull($this->getProperty('position')); 44 | $this->assertInstanceOf('Elboletaire\Watimage\Watermark', $instance); 45 | } 46 | 47 | /** 48 | * @covers Elboletaire\Watimage\Watermark::setSize 49 | */ 50 | public function testSetSize() 51 | { 52 | $instance = $this->testClass->setSize(23); 53 | $this->assertNotNull($this->getProperty('size')); 54 | $this->assertInstanceOf('Elboletaire\Watimage\Watermark', $instance); 55 | } 56 | 57 | /** 58 | * @covers Elboletaire\Watimage\Watermark::setSize 59 | */ 60 | public function testSetMargin() 61 | { 62 | $instance = $this->testClass->setMargin(23); 63 | $this->assertArraySubset([23, 23], $this->getProperty('margin')); 64 | $this->assertInstanceOf('Elboletaire\Watimage\Watermark', $instance); 65 | } 66 | 67 | /** 68 | * @covers Elboletaire\Watimage\Watermark::calculatePosition 69 | * @uses Elboletaire\Watimage\Image 70 | */ 71 | public function testApply() 72 | { 73 | // Check values 74 | $red = [ 75 | 'red' => 255, 76 | 'green' => 0, 77 | 'blue' => 0, 78 | 'alpha' => 0 79 | ]; 80 | $white = [ 81 | 'red' => 255, 82 | 'green' => 255, 83 | 'blue' => 255, 84 | 'alpha' => 0 85 | ]; 86 | $transparent = [ 87 | 'red' => 0, 88 | 'green' => 0, 89 | 'blue' => 0, 90 | 'alpha' => 127 91 | ]; 92 | 93 | // Init classes 94 | $image = new Image(); 95 | $watermark = $this->testClass; 96 | 97 | $image->create(200)->fill($white); 98 | $instance = $watermark->create(10) 99 | ->fill($red) 100 | ->setPosition('top left') 101 | ->apply($image) 102 | ; 103 | 104 | // Check instances are the expected ones 105 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $image); 106 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 107 | $this->assertInstanceOf('Elboletaire\Watimage\Watermark', $watermark); 108 | 109 | $resource = $image->getImage(); 110 | 111 | // Ensure whatermark is there 112 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 0, 0)); 113 | $this->assertArraySubset($red, $pixel); 114 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 9, 9)); 115 | $this->assertArraySubset($red, $pixel); 116 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 9, 0)); 117 | $this->assertArraySubset($red, $pixel); 118 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 0, 9)); 119 | $this->assertArraySubset($red, $pixel); 120 | 121 | // Check image is just after the image 122 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 10, 10)); 123 | $this->assertArraySubset($white, $pixel); 124 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 0, 10)); 125 | $this->assertArraySubset($white, $pixel); 126 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 10, 0)); 127 | $this->assertArraySubset($white, $pixel); 128 | 129 | // Check transparencies 130 | $image->create(400); 131 | $watermark 132 | ->load("{$this->files_path}/watermark.png") 133 | ->setPosition('top left') 134 | ->apply($image) 135 | ; 136 | 137 | $resource = $image->getImage(); 138 | // Ensure it's still transparent 139 | $pixel = imagecolorsforindex($resource, imagecolorat($resource, 0, 0)); 140 | $this->assertEquals(127, $pixel['alpha']); 141 | } 142 | 143 | /** 144 | * @covers Elboletaire\Watimage\Watermark::calculatePosition 145 | * @uses Elboletaire\Watimage\Image 146 | */ 147 | public function testCalculatePosition() 148 | { 149 | $size_image = 200; 150 | $size_watermark = 10; 151 | 152 | $image = new Image(); 153 | $image->create($size_image); 154 | 155 | $this->testClass->create($size_watermark); 156 | $this->testClass->fill('#f00'); 157 | 158 | $instance = $this->testClass->setPosition('bottom right')->apply($image); 159 | 160 | $metadata_img = $image->getMetadata(); 161 | $calculatePosition = $this->getMethod('calculatePosition'); 162 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 163 | 164 | $this->assertArraySubset([190, 190], $position); 165 | // Setting exact position should return array 166 | $this->setProperty('position', [200, 200]); 167 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 168 | $this->assertArraySubset([200, 200], $position); 169 | // When position is null or size is `full` should set position to `center center` 170 | $this->setProperty('position', null); 171 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 172 | $this->assertEquals('center center', $this->getProperty('position')); 173 | $this->setProperty('position', null); 174 | $this->testClass->setSize('full'); 175 | $calculatePosition->invoke($this->testClass, $metadata_img); 176 | $this->assertEquals('center center', $this->getProperty('position')); 177 | 178 | // Position calculated without margin 179 | $this->testClass 180 | ->destroy() 181 | ->create($size_watermark) 182 | ->fill('#f00') 183 | ->setPosition('top left') 184 | ; 185 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 186 | $this->assertArraySubset([0, 0], $position); 187 | $this->testClass 188 | ->destroy() 189 | ->create($size_watermark) 190 | ->fill('#f00') 191 | ->setPosition('top center') 192 | ; 193 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 194 | $this->assertArraySubset([95, 0], $position); 195 | $this->testClass 196 | ->destroy() 197 | ->create($size_watermark) 198 | ->fill('#f00') 199 | ->setPosition('top right') 200 | ; 201 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 202 | $this->assertArraySubset([190, 0], $position); 203 | $this->testClass 204 | ->destroy() 205 | ->create($size_watermark) 206 | ->fill('#f00') 207 | ->setPosition('bottom left') 208 | ; 209 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 210 | $this->assertArraySubset([0, 190], $position); 211 | $this->testClass 212 | ->destroy() 213 | ->create($size_watermark) 214 | ->fill('#f00') 215 | ->setPosition('bottom center') 216 | ; 217 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 218 | $this->assertArraySubset([95, 190], $position); 219 | $this->testClass 220 | ->destroy() 221 | ->create($size_watermark) 222 | ->fill('#f00') 223 | ->setPosition('bottom right') 224 | ; 225 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 226 | $this->assertArraySubset([190, 190], $position); 227 | 228 | // Position calculated with margin 229 | $this->testClass 230 | ->destroy() 231 | ->create($size_watermark) 232 | ->fill('#f00') 233 | ->setPosition('center center') 234 | ->setMargin(10) 235 | ; 236 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 237 | $this->assertArraySubset([105, 105], $position); 238 | $this->testClass 239 | ->destroy() 240 | ->create($size_watermark) 241 | ->fill('#f00') 242 | ->setPosition('center left') 243 | ->setMargin(10, 0) 244 | ; 245 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 246 | $this->assertArraySubset([10, 95], $position); 247 | $this->testClass 248 | ->destroy() 249 | ->create($size_watermark) 250 | ->fill('#f00') 251 | ->setPosition('center right') 252 | ->setMargin(-10, 10) 253 | ; 254 | $position = $calculatePosition->invoke($this->testClass, $metadata_img); 255 | $this->assertArraySubset([180, 105], $position); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /tests/TestCase/NormalizeTest.php: -------------------------------------------------------------------------------- 1 | 0, 'g' => 0, 'b' => 0, 'a' => 127]; 11 | $this->assertArraySubset($expected, Normalize::color(-1)); 12 | $this->assertArraySubset($expected, Normalize::color([0, 0, 0, 127])); 13 | $this->assertArraySubset($expected, Normalize::color([ 14 | 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 127 15 | ])); 16 | $this->assertArraySubset($expected, Normalize::color([ 17 | 'r' => 0, 'g' => 0, 'b' => 0, 'a' => 127 18 | ])); 19 | $this->assertArraySubset($expected, Normalize::color('#0000007F')); 20 | 21 | $expected['a'] = 119; 22 | $this->assertArraySubset($expected, Normalize::color('#0007')); 23 | } 24 | 25 | /** 26 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 27 | */ 28 | public function testColorFail() 29 | { 30 | Normalize::color('#33333'); 31 | } 32 | 33 | public function testCrop() 34 | { 35 | $expected = [23, 32, 200, 150]; 36 | 37 | // Passing multiple arguments 38 | $this->assertArraySubset( 39 | $expected, 40 | // x, y, width, height 41 | Normalize::crop(23, 32, 200, 150) 42 | ); 43 | // Passing an array 44 | $this->assertArraySubset( 45 | $expected, 46 | Normalize::crop([23, 32, 200, 150]) 47 | ); 48 | // Passing an associative array 49 | $this->assertArraySubset( 50 | $expected, 51 | Normalize::crop([ 52 | 'x' => 23, 53 | 'y' => 32, 54 | 'width' => 200, 55 | 'height' => 150 56 | ]) 57 | ); 58 | // Passing a simplified associative array 59 | $this->assertArraySubset( 60 | $expected, 61 | Normalize::crop([ 62 | 'x' => 23, 63 | 'y' => 32, 64 | 'w' => 200, 65 | 'h' => 150 66 | ]) 67 | ); 68 | } 69 | 70 | /** 71 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 72 | */ 73 | public function testCropFail() 74 | { 75 | Normalize::crop(23); 76 | } 77 | 78 | public function testFitInRange() 79 | { 80 | // Value between range 81 | $this->assertEquals(5, Normalize::fitInRange(5, 0, 23)); 82 | $this->assertEquals(23, Normalize::fitInRange(23, 0, 23)); 83 | 84 | // Value out of range 85 | $this->assertEquals(23, Normalize::fitInRange(121, 0, 23)); 86 | $this->assertEquals(0, Normalize::fitInRange(-121, 0, 23)); 87 | 88 | // Just force min value 89 | $this->assertEquals(121, Normalize::fitInRange(121, 0)); 90 | $this->assertEquals(0, Normalize::fitInRange(-121, 0)); 91 | 92 | // Just force max value 93 | $this->assertEquals(0, Normalize::fitInRange(121, false, 0)); 94 | $this->assertEquals(-121, Normalize::fitInRange(-121, false, 0)); 95 | } 96 | 97 | public function testFlip() 98 | { 99 | $this->assertEquals(IMG_FLIP_HORIZONTAL, Normalize::flip('x')); 100 | $this->assertEquals(IMG_FLIP_HORIZONTAL, Normalize::flip('h')); 101 | $this->assertEquals(IMG_FLIP_HORIZONTAL, Normalize::flip('horizontal')); 102 | $this->assertEquals(IMG_FLIP_HORIZONTAL, Normalize::flip(IMG_FLIP_HORIZONTAL)); 103 | 104 | $this->assertEquals(IMG_FLIP_VERTICAL, Normalize::flip('y')); 105 | $this->assertEquals(IMG_FLIP_VERTICAL, Normalize::flip('v')); 106 | $this->assertEquals(IMG_FLIP_VERTICAL, Normalize::flip('vertical')); 107 | $this->assertEquals(IMG_FLIP_VERTICAL, Normalize::flip(IMG_FLIP_VERTICAL)); 108 | 109 | $this->assertEquals(IMG_FLIP_BOTH, Normalize::flip('b')); 110 | $this->assertEquals(IMG_FLIP_BOTH, Normalize::flip('both')); 111 | $this->assertEquals(IMG_FLIP_BOTH, Normalize::flip(IMG_FLIP_BOTH)); 112 | } 113 | 114 | /** 115 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 116 | */ 117 | public function testFlipFail() 118 | { 119 | Normalize::flip('fail'); 120 | } 121 | 122 | public function testPosition() 123 | { 124 | $expected = [23, 23]; 125 | $this->assertArraySubset($expected, Normalize::cssPosition(23)); 126 | $this->assertArraySubset($expected, Normalize::cssPosition(['x' => 23])); 127 | 128 | $expected = [23, 32]; 129 | $this->assertArraySubset($expected, Normalize::cssPosition($expected)); 130 | $this->assertArraySubset($expected, Normalize::cssPosition(['x' => 23, 'y' => 32])); 131 | } 132 | 133 | public function testCropMeasures() 134 | { 135 | $expected = [23, 32, 46, 64]; 136 | $this->assertArraySubset($expected, Normalize::cropMeasures($expected)); 137 | $this->assertArraySubset($expected, Normalize::cropMeasures(23, 32, 46, 64)); 138 | $this->assertArraySubset($expected, Normalize::cropMeasures([ 139 | 'ox' => 23, 140 | 'oy' => 32, 141 | 'dx' => 46, 142 | 'dy' => 64 143 | ])); 144 | $this->assertArraySubset($expected, Normalize::cropMeasures([ 145 | 'ox' => 23, 146 | 'oy' => 32, 147 | 'dx' => 46, 148 | 'dy' => 64, 149 | ])); 150 | } 151 | 152 | /** 153 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 154 | */ 155 | public function testCropMeasuresThrowsException() 156 | { 157 | Normalize::cropMeasures([ 158 | 'ox' => 46, 159 | 'oy' => 56, 160 | 'dx' => 40, 161 | ]); 162 | } 163 | 164 | public function testMargin() 165 | { 166 | try { 167 | Normalize::margin('fail'); 168 | } catch (\Exception $e) { 169 | $this->assertEquals('Invalid margin {"x":"fail","y":null}.', $e->getMessage()); 170 | } 171 | } 172 | 173 | /** 174 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 175 | */ 176 | public function testPositionFail() 177 | { 178 | Normalize::position(23, 'fail'); 179 | } 180 | 181 | public function testSize() 182 | { 183 | $expected = [250, 320]; 184 | 185 | // Passing multiple arguments 186 | $this->assertArraySubset( 187 | $expected, 188 | Normalize::size( 189 | // width & height 190 | 250, 191 | 320 192 | ) 193 | ); 194 | // Passing an array 195 | $this->assertArraySubset( 196 | $expected, 197 | Normalize::size([ 198 | // width & height 199 | 250, 200 | 320 201 | ]) 202 | ); 203 | // Passing an associative array 204 | $this->assertArraySubset( 205 | $expected, 206 | Normalize::size([ 207 | 'width' => 250, 208 | 'height' => 320 209 | ]) 210 | ); 211 | // Passing simplified associative arrays 212 | $this->assertArraySubset( 213 | $expected, 214 | Normalize::size([ 215 | 'w' => 250, 216 | 'h' => 320 217 | ]) 218 | ); 219 | $this->assertArraySubset( 220 | $expected, 221 | Normalize::size([ 222 | 'x' => 250, 223 | 'y' => 320 224 | ]) 225 | ); 226 | // Passing just width (should return same height) 227 | $this->assertArraySubset( 228 | [250, 250], 229 | Normalize::size(250) 230 | ); 231 | $this->assertArraySubset( 232 | [250, 250], 233 | Normalize::size(['width' => 250]) 234 | ); 235 | 236 | // Passing negative values should return zero 237 | $this->assertArraySubset( 238 | [0, 0], 239 | Normalize::size(-200) 240 | ); 241 | } 242 | 243 | /** 244 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 245 | */ 246 | public function testSizeFail() 247 | { 248 | Normalize::size(null); 249 | } 250 | 251 | /** 252 | * @covers Elboletaire\Watimage\Normalize::cssPosition 253 | */ 254 | public function testCssPosition() 255 | { 256 | // Test string 257 | $expected = 'center center'; 258 | 259 | $this->assertEquals($expected, Normalize::cssPosition('center')); 260 | $this->assertEquals($expected, Normalize::cssPosition('centered')); 261 | 262 | try { 263 | Normalize::cssPosition('fail'); 264 | } catch (\Exception $e) { 265 | $this->assertEquals('Invalid watermark position fail.', $e->getMessage()); 266 | } 267 | } 268 | 269 | /** 270 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 271 | */ 272 | public function testCssPositionFail() 273 | { 274 | Normalize::cssPosition('not valid'); 275 | } 276 | 277 | public function testWatermarkSize() 278 | { 279 | $this->assertEquals('50%', Normalize::watermarkSize('50%')); 280 | $this->assertEquals('full', Normalize::watermarkSize('full')); 281 | $this->assertArraySubset([23, 42], Normalize::watermarkSize(23, 42)); 282 | 283 | try { 284 | Normalize::watermarkSize('fail'); 285 | } catch (\Exception $e) { 286 | $this->assertEquals('Invalid size arguments {"width":"fail","height":null}', $e->getMessage()); 287 | } 288 | } 289 | 290 | /** 291 | * @expectedException Elboletaire\Watimage\Exception\InvalidArgumentException 292 | */ 293 | public function testWatermarkSizeFail() 294 | { 295 | Normalize::watermarkSize('not valid'); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/Normalize.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright 2015 Òscar Casajuana 12 | * @license https://opensource.org/licenses/MIT MIT 13 | * @link https://github.com/elboletaire/Watimage 14 | */ 15 | class Normalize 16 | { 17 | /** 18 | * Returns the proper color array for the given color. 19 | * 20 | * It accepts any (or almost any) imaginable type. 21 | * 22 | * @param mixed $color Can be an array (sequential or associative) or 23 | * hexadecimal. In hexadecimal allows 3 and 6 characters 24 | * for rgb and 4 or 8 characters for rgba. 25 | * @return array Containing all 4 color channels. 26 | * @throws InvalidArgumentException 27 | */ 28 | public static function color($color) 29 | { 30 | if ($color === Image::COLOR_TRANSPARENT) { 31 | return [ 32 | 'r' => 0, 33 | 'g' => 0, 34 | 'b' => 0, 35 | 'a' => 127 36 | ]; 37 | } 38 | 39 | // rgb(a) arrays 40 | if (is_array($color) && in_array(count($color), [3, 4])) { 41 | $allowedKeys = [ 42 | 'associative' => ['red', 'green', 'blue', 'alpha'], 43 | 'reduced' => ['r', 'g', 'b', 'a'], 44 | 'numeric' => [0, 1, 2, 3] 45 | ]; 46 | 47 | foreach ($allowedKeys as $keys) { 48 | list($r, $g, $b, $a) = $keys; 49 | 50 | if (!isset($color[$r], $color[$g], $color[$b])) { 51 | continue; 52 | } 53 | 54 | return [ 55 | 'r' => self::fitInRange($color[$r], 0, 255), 56 | 'g' => self::fitInRange($color[$g], 0, 255), 57 | 'b' => self::fitInRange($color[$b], 0, 255), 58 | 'a' => self::fitInRange(isset($color[$a]) ? $color[$a] : 0, 0, 127), 59 | ]; 60 | } 61 | 62 | throw new InvalidArgumentException("Invalid array color value %s.", $color); 63 | } 64 | 65 | // hexadecimal 66 | if (!is_string($color)) { 67 | throw new InvalidArgumentException("Invalid color value \"%s\"", $color); 68 | } 69 | 70 | $color = ltrim($color, '#'); 71 | if (in_array(strlen($color), [3, 4])) { 72 | $color = str_split($color); 73 | $color = array_map(function ($item) { 74 | return str_repeat($item, 2); 75 | }, $color); 76 | $color = implode($color); 77 | } 78 | if (strlen($color) == 6) { 79 | list($r, $g, $b) = [ 80 | $color[0] . $color[1], 81 | $color[2] .$color[3], 82 | $color[4] . $color[5] 83 | ]; 84 | } elseif (strlen($color) == 8) { 85 | list($r, $g, $b, $a) = [ 86 | $color[0] . $color[1], 87 | $color[2] . $color[3], 88 | $color[4] . $color[5], 89 | $color[6] . $color[7] 90 | ]; 91 | } else { 92 | throw new InvalidArgumentException("Invalid hexadecimal color value \"%s\"", $color); 93 | } 94 | 95 | return [ 96 | 'r' => hexdec($r), 97 | 'g' => hexdec($g), 98 | 'b' => hexdec($b), 99 | 'a' => isset($a) ? hexdec($a) : 0 100 | ]; 101 | } 102 | 103 | /** 104 | * Normalizes crop arguments returning an array with them. 105 | * 106 | * You can pass arguments one by one or an array passing arguments 107 | * however you like. 108 | * 109 | * @param int $x X position where start to crop. 110 | * @param int $y Y position where start to crop. 111 | * @param int $width New width of the image. 112 | * @param int $height New height of the image. 113 | * @return array Array with numeric keys for x, y, width & height 114 | * @throws InvalidArgumentException 115 | */ 116 | public static function crop($x, $y = null, $width = null, $height = null) 117 | { 118 | if (!isset($y, $width, $height) && is_array($x)) { 119 | $values = $x; 120 | $allowedKeys = [ 121 | 'associative' => ['x', 'y', 'width', 'height'], 122 | 'reduced' => ['x', 'y', 'w', 'h'], 123 | 'numeric' => [0, 1, 2, 3] 124 | ]; 125 | 126 | foreach ($allowedKeys as $keys) { 127 | list($x, $y, $width, $height) = $keys; 128 | if (isset($values[$x], $values[$y], $values[$width], $values[$height])) { 129 | return [ 130 | $values[$x], 131 | $values[$y], 132 | $values[$width], 133 | $values[$height] 134 | ]; 135 | } 136 | } 137 | } 138 | 139 | if (!isset($x, $y, $width, $height)) { 140 | throw new InvalidArgumentException( 141 | "Invalid options for crop %s.", 142 | compact('x', 'y', 'width', 'height') 143 | ); 144 | } 145 | 146 | return [$x, $y, $width, $height]; 147 | } 148 | 149 | /** 150 | * Normalizes flip type from any of the allowed values. 151 | * 152 | * @param mixed $type Can be either: 153 | * v, y, vertical or IMG_FLIP_VERTICAL 154 | * h, x, horizontal or IMG_FLIP_HORIZONTAL 155 | * b, xy, yx, both or IMG_FLIP_BOTH 156 | * @return int 157 | * @throws InvalidArgumentException 158 | */ 159 | public static function flip($type) 160 | { 161 | switch (strtolower($type)) { 162 | case 'x': 163 | case 'h': 164 | case 'horizontal': 165 | case IMG_FLIP_HORIZONTAL: 166 | return IMG_FLIP_HORIZONTAL; 167 | break; 168 | 169 | case 'y': 170 | case 'v': 171 | case 'vertical': 172 | case IMG_FLIP_VERTICAL: 173 | return IMG_FLIP_VERTICAL; 174 | break; 175 | 176 | case 'b': 177 | case 'both': 178 | case IMG_FLIP_BOTH: 179 | return IMG_FLIP_BOTH; 180 | break; 181 | 182 | default: 183 | throw new InvalidArgumentException("Incorrect flip type \"%s\"", $type); 184 | break; 185 | } 186 | } 187 | 188 | /** 189 | * An alias of self::position but returning a customized message for Watermark. 190 | * 191 | * @param mixed $x Can be just x or an array containing both params. 192 | * @param int $y Can only be y. 193 | * @return array With x and y in a sequential array. 194 | * @throws InvalidArgumentException 195 | */ 196 | public static function margin($x, $y = null) 197 | { 198 | try { 199 | list($x, $y) = self::position($x, $y); 200 | } catch (InvalidArgumentException $e) { 201 | throw new InvalidArgumentException("Invalid margin %s.", compact('x', 'y')); 202 | } 203 | 204 | return [$x, $y]; 205 | } 206 | 207 | /** 208 | * Normalizes position (x and y). 209 | * 210 | * @param mixed $x Can be just x or an array containing both params. 211 | * @param int $y Can only be y. 212 | * @return array With x and y in a sequential array. 213 | * @throws InvalidArgumentException 214 | */ 215 | public static function position($x, $y = null) 216 | { 217 | if (is_array($x)) { 218 | if (isset($x['x']) || isset($x['y'])) { 219 | extract($x); 220 | } else { 221 | @list($x, $y) = $x; 222 | } 223 | } 224 | 225 | if (is_numeric($x) && !isset($y)) { 226 | $y = $x; 227 | } 228 | 229 | if (!isset($x, $y) || !(is_numeric($x) && is_numeric($y))) { 230 | throw new InvalidArgumentException("Invalid position %s.", compact('x', 'y')); 231 | } 232 | 233 | return [$x, $y]; 234 | } 235 | 236 | /** 237 | * Normalizes cropMeasures (origin X, origin Y, destiny X & destiny Y) 238 | * 239 | * @param mixed $ox Can be just ox or an array containing all the params. 240 | * @param int $oy Origin Y. 241 | * @param int $dx Destiny X. 242 | * @param int $dy Destiny Y. 243 | * @return array 244 | */ 245 | public static function cropMeasures($ox, $oy = null, $dx = null, $dy = null) 246 | { 247 | if (!isset($oy, $dx, $dy, $width, $height) && is_array($ox)) { 248 | $values = $ox; 249 | $allowedKeys = [ 250 | 'associative' => ['ox', 'oy', 'dx', 'dy'], 251 | 'numeric' => [0, 1, 2, 3] 252 | ]; 253 | 254 | foreach ($allowedKeys as $keys) { 255 | list($oxk, $oyk, $dxk, $dyk) = $keys; 256 | $isset = isset( 257 | $values[$oxk], 258 | $values[$oyk], 259 | $values[$dxk], 260 | $values[$dyk] 261 | ); 262 | if (!$isset) { 263 | continue; 264 | } 265 | return [ 266 | $values[$oxk], 267 | $values[$oyk], 268 | $values[$dxk], 269 | $values[$dyk] 270 | ]; 271 | } 272 | } 273 | 274 | if (!isset($ox, $oy, $dx, $dy)) { 275 | throw new InvalidArgumentException( 276 | "Invalid options for cropMeasures %s.", 277 | compact('ox', 'oy', 'dx', 'dy') 278 | ); 279 | } 280 | 281 | return [$ox, $oy, $dx, $dy]; 282 | } 283 | 284 | /** 285 | * Normalizes size (width and height). 286 | * 287 | * @param mixed $width Can be just width or an array containing both params. 288 | * @param int $height Can only be height. 289 | * @return array With width and height in a sequential array. 290 | * @throws InvalidArgumentException 291 | */ 292 | public static function size($width, $height = null) 293 | { 294 | if (!isset($height) && is_array($width)) { 295 | $allowedKeys = [ 296 | [0, 1], 297 | ['x', 'y'], 298 | ['w', 'h'], 299 | ['width', 'height'], 300 | ]; 301 | 302 | foreach ($allowedKeys as $keys) { 303 | list($x, $y) = $keys; 304 | 305 | 306 | if (isset($width[$x])) { 307 | if (isset($width[$y])) { 308 | $height = $width[$y]; 309 | } 310 | $width = $width[$x]; 311 | break; 312 | } 313 | } 314 | } 315 | 316 | if (isset($width) && !isset($height)) { 317 | $height = $width; 318 | } 319 | 320 | if (!isset($width, $height) || !(is_numeric($width) && is_numeric($height))) { 321 | throw new InvalidArgumentException( 322 | "Invalid resize arguments %s", 323 | compact('width', 'height') 324 | ); 325 | } 326 | 327 | return [ 328 | self::fitInRange($width, 0), 329 | self::fitInRange($height, 0) 330 | ]; 331 | } 332 | 333 | /** 334 | * Checks that the given value is between our defined range. 335 | * 336 | * Can check just for min or max if setting the other value to false. 337 | * 338 | * @param int $value Value to be checked, 339 | * @param bool $min Minimum value. False to just use max. 340 | * @param bool $max Maximum value. False to just use min. 341 | * @return int The value itself. 342 | */ 343 | public static function fitInRange($value, $min = false, $max = false) 344 | { 345 | if ($min !== false && $value < $min) { 346 | $value = $min; 347 | } 348 | 349 | if ($max !== false && $value > $max) { 350 | $value = $max; 351 | } 352 | 353 | return $value; 354 | } 355 | 356 | /** 357 | * Normalizes position + position ala css. 358 | * 359 | * @param mixed $position Array with x,y or string ala CSS. 360 | * @return mixed Returns what you pass (array or string). 361 | * @throws InvalidArgumentException 362 | */ 363 | public static function cssPosition($position) 364 | { 365 | try { 366 | $position = self::position($position); 367 | } catch (InvalidArgumentException $e) { 368 | if (!is_string($position)) { 369 | throw new InvalidArgumentException("Invalid watermark position %s.", $position); 370 | } 371 | 372 | if (in_array($position, ['center', 'centered'])) { 373 | $position = 'center center'; 374 | } 375 | 376 | if (!preg_match('/((center|top|bottom|right|left) ?){2}/', $position)) { 377 | throw new InvalidArgumentException("Invalid watermark position %s.", $position); 378 | } 379 | } 380 | 381 | return $position; 382 | } 383 | 384 | /** 385 | * Returns proper size argument for Watermark. 386 | * 387 | * @param mixed $width Can be a percentage, just width or an array containing both params. 388 | * @param int $height Can only be height. 389 | * @return mixed 390 | */ 391 | public static function watermarkSize($width, $height = null) 392 | { 393 | try { 394 | $width = self::size($width, $height); 395 | } catch (InvalidArgumentException $e) { 396 | if (!is_string($width) || !preg_match('/([0-9]{1,3}%|full)$/', $width)) { 397 | throw new InvalidArgumentException( 398 | "Invalid size arguments %s", 399 | compact('width', 'height') 400 | ); 401 | } 402 | } 403 | 404 | return $width; 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/Image.php: -------------------------------------------------------------------------------- 1 | 15 | * @copyright 2015 Òscar Casajuana 16 | * @license https://opensource.org/licenses/MIT MIT 17 | * @link https://github.com/elboletaire/Watimage 18 | */ 19 | class Image 20 | { 21 | /** 22 | * Constant for the (deprecated) transparent color 23 | */ 24 | const COLOR_TRANSPARENT = -1; 25 | 26 | /** 27 | * Current image location. 28 | * 29 | * @var string 30 | */ 31 | protected $filename; 32 | 33 | /** 34 | * Image GD resource. 35 | * 36 | * @var resource 37 | */ 38 | protected $image; 39 | 40 | /** 41 | * Image metadata. 42 | * 43 | * @var array 44 | */ 45 | protected $metadata = []; 46 | 47 | /** 48 | * Current image width 49 | * 50 | * @var float 51 | */ 52 | protected $width; 53 | 54 | /** 55 | * Current image height 56 | * 57 | * @var float 58 | */ 59 | protected $height; 60 | 61 | /** 62 | * Image export quality for gif and jpg files. 63 | * 64 | * You can set it with setQuality or setImage methods. 65 | * 66 | * @var integer 67 | */ 68 | protected $quality = 80; 69 | 70 | /** 71 | * Image compression value for png files. 72 | * 73 | * You can set it with setCompression method. 74 | * 75 | * @var integer 76 | */ 77 | protected $compression = 9; 78 | 79 | /** 80 | * Constructor method. You can pass a filename to be loaded by default 81 | * or load it later with load('filename.ext') 82 | * 83 | * @param string $file Filepath of the image to be loaded. 84 | */ 85 | public function __construct($file = null, $autoOrientate = true) 86 | { 87 | if (!extension_loaded('gd')) { 88 | throw new ExtensionNotLoadedException("GD"); 89 | } 90 | 91 | if (!empty($file)) { 92 | $this->load($file); 93 | 94 | if ($autoOrientate) { 95 | $this->autoOrientate(); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Ensure everything gets emptied on object destruction. 102 | * 103 | * @codeCoverageIgnore 104 | */ 105 | public function __destruct() 106 | { 107 | $this->destroy(); 108 | } 109 | 110 | /** 111 | * Creates a resource image. 112 | * 113 | * This method was using imagecreatefromstring but I decided to switch after 114 | * reading this: https://thenewphalls.wordpress.com/2012/12/27/imagecreatefromstring-vs-imagecreatefromformat 115 | * 116 | * @param string $filename Image file path/name. 117 | * @param string $mime Image mime or `string` if creating from string 118 | * (no base64 encoded). 119 | * @return resource 120 | * @throws InvalidMimeException 121 | */ 122 | public function createResourceImage($filename, $mime) 123 | { 124 | switch ($mime) { 125 | case 'image/gif': 126 | $image = imagecreatefromgif($filename); 127 | break; 128 | 129 | case 'image/png': 130 | $image = imagecreatefrompng($filename); 131 | break; 132 | 133 | case 'image/jpeg': 134 | $image = imagecreatefromjpeg($filename); 135 | break; 136 | 137 | case 'string': 138 | $image = imagecreatefromstring($filename); 139 | break; 140 | 141 | default: 142 | throw new InvalidMimeException($mime); 143 | } 144 | 145 | // Handle transparencies 146 | imagesavealpha($image, true); 147 | imagealphablending($image, true); 148 | 149 | return $image; 150 | } 151 | 152 | /** 153 | * Cleans up everything to start again. 154 | * 155 | * @return Image 156 | */ 157 | public function destroy() 158 | { 159 | if (!empty($this->image) 160 | && is_resource($this->image) 161 | && get_resource_type($this->image) == 'gd' 162 | ) { 163 | imagedestroy($this->image); 164 | } 165 | $this->metadata = []; 166 | $this->filename = $this->width = $this->height = null; 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Outputs or saves the image. 173 | * 174 | * @param string $filename Filename to be saved. Empty to directly print on screen. 175 | * @param string $output Use it to overwrite the output format when no $filename is passed. 176 | * @param bool $header Wheather or not generate the output header. 177 | * @return Image 178 | * @throws InvalidArgumentException If output format is not recognised. 179 | */ 180 | public function generate($filename = null, $output = null, $header = true) 181 | { 182 | $output = $output ?: $this->metadata['mime']; 183 | if (!empty($filename)) { 184 | $output = $this->getMimeFromExtension($filename); 185 | } elseif ($header) { 186 | header("Content-type: {$output}"); 187 | } 188 | 189 | switch ($output) { 190 | case 'image/gif': 191 | imagegif($this->image, $filename, $this->quality); 192 | break; 193 | case 'image/png': 194 | imagesavealpha($this->image, true); 195 | imagepng($this->image, $filename, $this->compression); 196 | break; 197 | case 'image/jpeg': 198 | imageinterlace($this->image, true); 199 | imagejpeg($this->image, $filename, $this->quality); 200 | break; 201 | default: 202 | throw new InvalidArgumentException("Invalid output format \"%s\"", $output); 203 | } 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Similar to generate, except that passing an empty $filename here will 210 | * overwrite the original file. 211 | * 212 | * @param string $filename Filename to be saved. Empty to overwrite original file. 213 | * @return bool 214 | */ 215 | public function save($filename = null) 216 | { 217 | $filename = $filename ?: $this->filename; 218 | 219 | return $this->generate($filename); 220 | } 221 | 222 | /** 223 | * Returns the base64 version for the current Image. 224 | * 225 | * @param bool $prefix Whether or not prefix the string 226 | * with `data:{mime};base64,`. 227 | * @return string 228 | */ 229 | public function toString($prefix = false) 230 | { 231 | ob_start(); 232 | $this->generate(null, null, false); 233 | $image = ob_get_contents(); 234 | ob_end_clean(); 235 | 236 | $string = base64_encode($image); 237 | 238 | if ($prefix) { 239 | $prefix = "data:{$this->metadata['mime']};base64,"; 240 | $string = $prefix . $string; 241 | } 242 | 243 | return $string; 244 | } 245 | 246 | /** 247 | * Loads image and (optionally) its options. 248 | * 249 | * @param mixed $filename Filename string or array containing both filename and quality 250 | * @return Watimage 251 | * @throws FileNotExistException 252 | * @throws InvalidArgumentException 253 | */ 254 | public function load($filename) 255 | { 256 | if (empty($filename)) { 257 | throw new InvalidArgumentException("Image file has not been set."); 258 | } 259 | 260 | if (is_array($filename)) { 261 | if (isset($filename['quality'])) { 262 | $this->setQuality($filename['quality']); 263 | } 264 | $filename = $filename['file']; 265 | } 266 | 267 | if (!file_exists($filename)) { 268 | throw new FileNotExistException($filename); 269 | } 270 | 271 | $this->destroy(); 272 | 273 | $this->filename = $filename; 274 | $this->getMetadataForImage(); 275 | $this->image = $this->createResourceImage($filename, $this->metadata['mime']); 276 | 277 | return $this; 278 | } 279 | 280 | /** 281 | * Loads an image from string. Can be either base64 encoded or not. 282 | * 283 | * @param string $string The image string to be loaded. 284 | * @return Image 285 | */ 286 | public function fromString($string) 287 | { 288 | if (strpos($string, 'data:image') === 0) { 289 | preg_match('/^data:(image\/[a-z]+);base64,(.+)/', $string, $matches); 290 | array_shift($matches); 291 | list($this->metadata['mime'], $string) = $matches; 292 | } 293 | 294 | if (!$string = base64_decode($string)) { 295 | throw new InvalidArgumentException( 296 | 'The given value does not seem a valid base64 string' 297 | ); 298 | } 299 | 300 | $this->image = $this->createResourceImage($string, 'string'); 301 | $this->updateSize(); 302 | 303 | if (function_exists('finfo_buffer') && !isset($this->metadata['mime'])) { 304 | $finfo = finfo_open(); 305 | $this->metadata['mime'] = finfo_buffer($finfo, $string, FILEINFO_MIME_TYPE); 306 | finfo_close($finfo); 307 | } 308 | 309 | return $this; 310 | } 311 | 312 | /** 313 | * Auto-orients an image based on its exif Orientation information. 314 | * 315 | * @return Image 316 | */ 317 | public function autoOrientate() 318 | { 319 | if (empty($this->metadata['exif']['Orientation'])) { 320 | return $this; 321 | } 322 | 323 | switch ((int)$this->metadata['exif']['Orientation']) { 324 | case 2: 325 | return $this->flip('horizontal'); 326 | case 3: 327 | return $this->flip('both'); 328 | case 4: 329 | return $this->flip('vertical'); 330 | case 5: 331 | $this->flip('horizontal'); 332 | return $this->rotate(-90); 333 | case 6: 334 | return $this->rotate(-90); 335 | case 7: 336 | $this->flip('horizontal'); 337 | return $this->rotate(90); 338 | case 8: 339 | return $this->rotate(90); 340 | default: 341 | return $this; 342 | } 343 | } 344 | 345 | /** 346 | * Rotates an image. 347 | * 348 | * Will rotate clockwise when using positive degrees. 349 | * 350 | * @param int $degrees Rotation angle in degrees. 351 | * @param mixed $bgcolor Background to be used for the background, transparent by default. 352 | * @return Image 353 | */ 354 | public function rotate($degrees, $bgcolor = self::COLOR_TRANSPARENT) 355 | { 356 | $bgcolor = $this->color($bgcolor); 357 | 358 | $this->image = imagerotate($this->image, $degrees, $bgcolor); 359 | 360 | $this->updateSize(); 361 | 362 | return $this; 363 | } 364 | 365 | /** 366 | * All in one method for all resize methods. 367 | * 368 | * @param string $type Type of resize: resize, resizemin, reduce, crop & resizecrop. 369 | * @param mixed $width Can be just max width or an array containing both params. 370 | * @param int $height Max height. 371 | * @return Image 372 | */ 373 | public function resize($type, $width, $height = null) 374 | { 375 | $types = [ 376 | 'classic' => 'classicResize', 377 | 'resize' => 'classicResize', 378 | 'reduce' => 'reduce', 379 | 'resizemin' => 'reduce', 380 | 'min' => 'reduce', 381 | 'crop' => 'classicCrop', 382 | 'resizecrop' => 'resizeCrop' 383 | ]; 384 | 385 | $lowertype = strtolower($type); 386 | 387 | if (!array_key_exists($lowertype, $types)) { 388 | throw new InvalidArgumentException("Invalid resize type %s.", $type); 389 | } 390 | 391 | return $this->{$types[$lowertype]}($width, $height); 392 | } 393 | 394 | /** 395 | * Resizes maintaining aspect ratio. 396 | * 397 | * Maintains the aspect ratio of the image and makes sure that it fits 398 | * within the max width and max height (thus some side will be smaller). 399 | * 400 | * @param mixed $width Can be just max width or an array containing both params. 401 | * @param int $height Max height. 402 | * @return Image 403 | */ 404 | public function classicResize($width, $height = null) 405 | { 406 | list($width, $height) = Normalize::size($width, $height); 407 | 408 | if ($this->width == $width && $this->height == $height) { 409 | return $this; 410 | } 411 | 412 | if ($this->width > $this->height) { 413 | $height = ($this->height * $width) / $this->width; 414 | } elseif ($this->width < $this->height) { 415 | $width = ($this->width * $height) / $this->height; 416 | } elseif ($this->width == $this->height) { 417 | $width = $height; 418 | } 419 | 420 | $this->image = $this->imagecopy($width, $height); 421 | 422 | $this->updateSize(); 423 | 424 | return $this; 425 | } 426 | 427 | /** 428 | * Backwards compatibility alias for reduce (which has the same logic). 429 | * 430 | * @param mixed $width Can be just max width or an array containing both params. 431 | * @param int $height Max height. 432 | * @return Image 433 | * @deprecated 434 | * @codeCoverageIgnore 435 | */ 436 | public function resizeMin($width, $height = null) 437 | { 438 | return $this->reduce($width, $height); 439 | } 440 | 441 | /** 442 | * A straight centered crop. 443 | * 444 | * @param mixed $width Can be just max width or an array containing both params. 445 | * @param int $height Max height. 446 | * @return Image 447 | */ 448 | public function classicCrop($width, $height = null) 449 | { 450 | list($width, $height) = Normalize::size($width, $height); 451 | 452 | $startY = ($this->height - $height) / 2; 453 | $startX = ($this->width - $width) / 2; 454 | 455 | $this->image = $this->imagecopy($width, $height, $startX, $startY, $width, $height); 456 | 457 | $this->updateSize(); 458 | 459 | return $this; 460 | } 461 | 462 | /** 463 | * Resizes to max, then crops to center. 464 | * 465 | * @param mixed $width Can be just max width or an array containing both params. 466 | * @param int $height Max height. 467 | * @return Image 468 | */ 469 | public function resizeCrop($width, $height = null) 470 | { 471 | list($width, $height) = Normalize::size($width, $height); 472 | 473 | $ratioX = $width / $this->width; 474 | $ratioY = $height / $this->height; 475 | $srcW = $this->width; 476 | $srcH = $this->height; 477 | 478 | if ($ratioX < $ratioY) { 479 | $startX = round(($this->width - ($width / $ratioY)) / 2); 480 | $startY = 0; 481 | $srcW = round($width / $ratioY); 482 | } else { 483 | $startX = 0; 484 | $startY = round(($this->height - ($height / $ratioX)) / 2); 485 | $srcH = round($height / $ratioX); 486 | } 487 | 488 | $this->image = $this->imagecopy($width, $height, $startX, $startY, $srcW, $srcH); 489 | 490 | $this->updateSize(); 491 | 492 | return $this; 493 | } 494 | 495 | /** 496 | * Resizes maintaining aspect ratio but not exceeding width / height. 497 | * 498 | * @param mixed $width Can be just max width or an array containing both params. 499 | * @param int $height Max height. 500 | * @return Image 501 | */ 502 | public function reduce($width, $height = null) 503 | { 504 | list($width, $height) = Normalize::size($width, $height); 505 | 506 | if ($this->width < $width && $this->height < $height) { 507 | return $this; 508 | } 509 | 510 | $ratioX = $this->width / $width; 511 | $ratioY = $this->height / $height; 512 | 513 | $ratio = $ratioX > $ratioY ? $ratioX : $ratioY; 514 | 515 | if ($ratio === 1) { 516 | return $this; 517 | } 518 | 519 | // Getting the new image size 520 | $width = (int)($this->width / $ratio); 521 | $height = (int)($this->height / $ratio); 522 | 523 | $this->image = $this->imagecopy($width, $height); 524 | 525 | $this->updateSize(); 526 | 527 | return $this; 528 | } 529 | 530 | /** 531 | * Flips an image. If PHP version is 5.5.0 or greater will use 532 | * proper php gd imageflip method. Otherwise will fallback to 533 | * convenienceflip. 534 | * 535 | * @param string $type Type of flip, can be any of: horizontal, vertical, both 536 | * @return Image 537 | */ 538 | public function flip($type = 'horizontal') 539 | { 540 | if (version_compare(PHP_VERSION, '5.5.0', '<')) { 541 | return $this->convenienceFlip($type); 542 | } 543 | 544 | imageflip($this->image, Normalize::flip($type)); 545 | 546 | return $this; 547 | } 548 | 549 | /** 550 | * Flip method for PHP versions < 5.5.0 551 | * 552 | * @param string $type Type of flip, can be any of: horizontal, vertical, both 553 | * @return Image 554 | */ 555 | public function convenienceFlip($type = 'horizontal') 556 | { 557 | $type = Normalize::flip($type); 558 | 559 | $resampled = $this->imagecreate($this->width, $this->height); 560 | 561 | // @codingStandardsIgnoreStart 562 | switch ($type) { 563 | case IMG_FLIP_VERTICAL: 564 | imagecopyresampled( 565 | $resampled, $this->image, 566 | 0, 0, 0, ($this->height - 1), 567 | $this->width, $this->height, $this->width, 0 - $this->height 568 | ); 569 | break; 570 | case IMG_FLIP_HORIZONTAL: 571 | imagecopyresampled( 572 | $resampled, $this->image, 573 | 0, 0, ($this->width - 1), 0, 574 | $this->width, $this->height, 0 - $this->width, $this->height 575 | ); 576 | break; 577 | // same as $this->rotate(180) 578 | case IMG_FLIP_BOTH: 579 | imagecopyresampled( 580 | $resampled, $this->image, 581 | 0, 0, ($this->width - 1), ($this->height - 1), 582 | $this->width, $this->height, 0 - $this->width, 0 - $this->height 583 | ); 584 | break; 585 | } 586 | // @codingStandardsIgnoreEnd 587 | 588 | $this->image = $resampled; 589 | 590 | return $this; 591 | } 592 | 593 | /** 594 | * Creates an empty canvas. 595 | * 596 | * If no arguments are passed and we have previously created an 597 | * image it will create a new canvas with the previous canvas size. 598 | * Due to this, you can use this method to "empty" the current canvas. 599 | * 600 | * @param int $width Canvas width. 601 | * @param int $height Canvas height. 602 | * @return Image 603 | */ 604 | public function create($width = null, $height = null) 605 | { 606 | if (!isset($width)) { 607 | if (!isset($this->width, $this->height)) { 608 | throw new InvalidArgumentException("You must set the canvas size."); 609 | } 610 | $width = $this->width; 611 | $height = $this->height; 612 | } 613 | 614 | if (!isset($height)) { 615 | $height = $width; 616 | } 617 | 618 | $this->image = $this->imagecreate($width, $height); 619 | $exif = null; 620 | $this->metadata = compact('width', 'height', 'exif'); 621 | 622 | $this->updateSize(); 623 | 624 | return $this; 625 | } 626 | 627 | /** 628 | * Creates an empty canvas. 629 | * 630 | * @param int $width Canvas width. 631 | * @param int $height Canvas height. 632 | * @param bool $transparency Whether or not to set transparency values. 633 | * @return resource Image resource with the canvas. 634 | */ 635 | protected function imagecreate($width, $height, $transparency = true) 636 | { 637 | $image = imagecreatetruecolor($width, $height); 638 | 639 | if ($transparency) { 640 | // Required for transparencies 641 | $bgcolor = imagecolortransparent( 642 | $image, 643 | imagecolorallocatealpha($image, 255, 255, 255, 127) 644 | ); 645 | imagefill($image, 0, 0, $bgcolor); 646 | imagesavealpha($image, true); 647 | imagealphablending($image, true); 648 | } 649 | 650 | return $image; 651 | } 652 | 653 | /** 654 | * Helper method for all resize methods and others that require 655 | * imagecopyresampled method. 656 | * 657 | * @param int $dstW New width. 658 | * @param int $dstH New height. 659 | * @param int $srcX Starting source point X. 660 | * @param int $srcY Starting source point Y. 661 | * @return resource GD image resource containing the resized image. 662 | */ 663 | protected function imagecopy($dstW, $dstH, $srcX = 0, $srcY = 0, $srcW = false, $srcH = false) 664 | { 665 | $destImage = $this->imagecreate($dstW, $dstH); 666 | 667 | if ($srcW === false) { 668 | $srcW = $this->width; 669 | } 670 | 671 | if ($srcH === false) { 672 | $srcH = $this->height; 673 | } 674 | 675 | // @codingStandardsIgnoreStart 676 | imagecopyresampled( 677 | $destImage, $this->image, 678 | 0, 0, $srcX, $srcY, 679 | $dstW, $dstH, $srcW, $srcH 680 | ); 681 | // @codingStandardsIgnoreEnd 682 | 683 | return $destImage; 684 | } 685 | 686 | /** 687 | * Fills current canvas with specified color. 688 | * 689 | * It works with newly created canvas. If you want to overwrite the current 690 | * canvas you must first call `create` method to empty current canvas. 691 | * 692 | * @param mixed $color The color. Check out getColorArray for allowed formats. 693 | * @return Image 694 | */ 695 | public function fill($color = '#fff') 696 | { 697 | imagefill($this->image, 0, 0, $this->color($color)); 698 | 699 | return $this; 700 | } 701 | 702 | /** 703 | * Allocates a color for the current image resource and returns it. 704 | * 705 | * Useful for directly treating images. 706 | * 707 | * @param mixed $color The color. Check out getColorArray for allowed formats. 708 | * @return int 709 | * @codeCoverageIgnore 710 | */ 711 | public function color($color) 712 | { 713 | $color = Normalize::color($color); 714 | 715 | if ($color['a'] !== 0) { 716 | return imagecolorallocatealpha($this->image, $color['r'], $color['g'], $color['b'], $color['a']); 717 | } 718 | 719 | return imagecolorallocate($this->image, $color['r'], $color['g'], $color['b']); 720 | } 721 | 722 | /** 723 | * Crops an image based on specified coords and size. 724 | * 725 | * You can pass arguments one by one or an array passing arguments 726 | * however you like. 727 | * 728 | * @param int $x X position where start to crop. 729 | * @param int $y Y position where start to crop. 730 | * @param int $width New width of the image. 731 | * @param int $height New height of the image. 732 | * @return Image 733 | */ 734 | public function crop($x, $y = null, $width = null, $height = null) 735 | { 736 | list($x, $y, $width, $height) = Normalize::crop($x, $y, $width, $height); 737 | 738 | $crop = $this->imagecreate($width, $height); 739 | 740 | // @codingStandardsIgnoreStart 741 | imagecopyresampled( 742 | $crop, $this->image, 743 | 0, 0, $x, $y, 744 | $width, $height, $width, $height 745 | ); 746 | // @codingStandardsIgnoreEnd 747 | 748 | $this->image = $crop; 749 | 750 | $this->updateSize(); 751 | 752 | return $this; 753 | } 754 | 755 | /** 756 | * Blurs the image. 757 | * 758 | * @param mixed $type Type of blur to be used between: gaussian, selective. 759 | * @param integer $passes Number of times to apply the filter. 760 | * @return Image 761 | * @throws InvalidArgumentException 762 | */ 763 | public function blur($type = null, $passes = 1) 764 | { 765 | switch (strtolower($type)) { 766 | case IMG_FILTER_GAUSSIAN_BLUR: 767 | case 'selective': 768 | $type = IMG_FILTER_GAUSSIAN_BLUR; 769 | break; 770 | 771 | // gaussian by default (just because I like it more) 772 | case null: 773 | case 'gaussian': 774 | case IMG_FILTER_SELECTIVE_BLUR: 775 | $type = IMG_FILTER_SELECTIVE_BLUR; 776 | break; 777 | 778 | default: 779 | throw new InvalidArgumentException("Incorrect blur type \"%s\"", $type); 780 | } 781 | 782 | for ($i = 0; $i < Normalize::fitInRange($passes, 1); $i++) { 783 | imagefilter($this->image, $type); 784 | } 785 | 786 | return $this; 787 | } 788 | 789 | /** 790 | * Changes the brightness of the image. 791 | * 792 | * @param integer $level Brightness value; range between -255 & 255. 793 | * @return Image 794 | */ 795 | public function brightness($level) 796 | { 797 | imagefilter( 798 | $this->image, 799 | IMG_FILTER_BRIGHTNESS, 800 | Normalize::fitInRange($level, -255, 255) 801 | ); 802 | 803 | return $this; 804 | } 805 | 806 | /** 807 | * Like grayscale, except you can specify the color. 808 | * 809 | * @param mixed $color Color in any format accepted by Normalize::color 810 | * @return Image 811 | */ 812 | public function colorize($color) 813 | { 814 | $color = Normalize::color($color); 815 | 816 | imagefilter( 817 | $this->image, 818 | IMG_FILTER_COLORIZE, 819 | $color['r'], 820 | $color['g'], 821 | $color['b'], 822 | $color['a'] 823 | ); 824 | 825 | return $this; 826 | } 827 | 828 | /** 829 | * Changes the contrast of the image. 830 | * 831 | * @param integer $level Use for adjunting level of contrast (-100 to 100) 832 | * @return Image 833 | */ 834 | public function contrast($level) 835 | { 836 | imagefilter( 837 | $this->image, 838 | IMG_FILTER_CONTRAST, 839 | Normalize::fitInRange($level, -100, 100) 840 | ); 841 | 842 | return $this; 843 | } 844 | 845 | /** 846 | * Uses edge detection to highlight the edges in the image. 847 | * 848 | * @return Image 849 | */ 850 | public function edgeDetection() 851 | { 852 | imagefilter($this->image, IMG_FILTER_EDGEDETECT); 853 | 854 | return $this; 855 | } 856 | 857 | /** 858 | * Embosses the image. 859 | * 860 | * @return Image 861 | */ 862 | public function emboss() 863 | { 864 | imagefilter($this->image, IMG_FILTER_EMBOSS); 865 | 866 | return $this; 867 | } 868 | 869 | /** 870 | * Applies grayscale filter. 871 | * 872 | * @return Image 873 | */ 874 | public function grayscale() 875 | { 876 | imagefilter($this->image, IMG_FILTER_GRAYSCALE); 877 | 878 | return $this; 879 | } 880 | 881 | /** 882 | * Uses mean removal to achieve a "sketchy" effect. 883 | * 884 | * @return Image 885 | */ 886 | public function meanRemove() 887 | { 888 | imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); 889 | 890 | return $this; 891 | } 892 | 893 | /** 894 | * Reverses all colors of the image. 895 | * 896 | * @return Image 897 | */ 898 | public function negate() 899 | { 900 | imagefilter($this->image, IMG_FILTER_NEGATE); 901 | 902 | return $this; 903 | } 904 | 905 | /** 906 | * Pixelates the image. 907 | * 908 | * @param int $blockSize Block size in pixels. 909 | * @param bool $advanced Set to true to enable advanced pixelation. 910 | * @return Image 911 | */ 912 | public function pixelate($blockSize = 3, $advanced = false) 913 | { 914 | imagefilter( 915 | $this->image, 916 | IMG_FILTER_PIXELATE, 917 | Normalize::fitInRange($blockSize, 1), 918 | $advanced 919 | ); 920 | 921 | return $this; 922 | } 923 | 924 | /** 925 | * A combination of various effects to achieve a sepia like effect. 926 | * 927 | * TODO: Create an additional class with instagram-like effects and move it there. 928 | * 929 | * @param int $alpha Defines the transparency of the effect: from 0 to 100 930 | * @return Image 931 | */ 932 | public function sepia($alpha = 0) 933 | { 934 | return $this 935 | ->grayscale() 936 | ->contrast(-3) 937 | ->brightness(-15) 938 | ->colorize([ 939 | 'r' => 100, 940 | 'g' => 70, 941 | 'b' => 50, 942 | 'a' => Normalize::fitInRange($alpha, 0, 100) 943 | ]) 944 | ; 945 | } 946 | 947 | /** 948 | * Makes the image smoother. 949 | * 950 | * @param int $level Level of smoothness, between -15 and 15. 951 | * @return Image 952 | */ 953 | public function smooth($level) 954 | { 955 | imagefilter( 956 | $this->image, 957 | IMG_FILTER_SMOOTH, 958 | Normalize::fitInRange($level, -15, 15) 959 | ); 960 | 961 | return $this; 962 | } 963 | 964 | /** 965 | * Adds a vignette to image. 966 | * 967 | * @param float $size Size of the vignette, between 0 and 10. Low is sharper. 968 | * @param float $level Vignete transparency, between 0 and 1 969 | * @return Image 970 | * @link http://php.net/manual/en/function.imagefilter.php#109809 971 | */ 972 | public function vignette($size = 0.7, $level = 0.8) 973 | { 974 | for ($x = 0; $x < $this->width; ++$x) { 975 | for ($y = 0; $y < $this->height; ++$y) { 976 | $index = imagecolorat($this->image, $x, $y); 977 | $rgb = imagecolorsforindex($this->image, $index); 978 | 979 | $this->vignetteEffect($size, $level, $x, $y, $rgb); 980 | $color = imagecolorallocate($this->image, $rgb['red'], $rgb['green'], $rgb['blue']); 981 | 982 | imagesetpixel($this->image, $x, $y, $color); 983 | } 984 | } 985 | 986 | return $this; 987 | } 988 | 989 | /** 990 | * Sets quality for gif and jpg files. 991 | * 992 | * @param int $quality A value from 0 (zero quality) to 100 (max quality). 993 | * @return Image 994 | * @codeCoverageIgnore 995 | */ 996 | public function setQuality($quality) 997 | { 998 | $this->quality = $quality; 999 | 1000 | return $this; 1001 | } 1002 | 1003 | /** 1004 | * Sets compression for png files. 1005 | * 1006 | * @param int $compression A value from 0 (no compression, not recommended) to 9. 1007 | * @return Image 1008 | * @codeCoverageIgnore 1009 | */ 1010 | public function setCompression($compression) 1011 | { 1012 | $this->compression = $compression; 1013 | 1014 | return $this; 1015 | } 1016 | 1017 | /** 1018 | * Allows you to set the current image resource. 1019 | * 1020 | * This is intented for use it in conjuntion with getImage. 1021 | * 1022 | * @param resource $image Image resource to be set. 1023 | * @throws Exception If given image is not a GD resource. 1024 | * @return Image 1025 | */ 1026 | public function setImage($image) 1027 | { 1028 | if (!is_resource($image) || !get_resource_type($image) == 'gd') { 1029 | throw new Exception("Given image is not a GD image resource"); 1030 | } 1031 | 1032 | $this->image = $image; 1033 | $this->updateSize(); 1034 | 1035 | return $this; 1036 | } 1037 | 1038 | /** 1039 | * Useful method to calculate real crop measures. Used when you crop an image 1040 | * which is smaller than the original one. In those cases you can call 1041 | * calculateCropMeasures to retrieve the real $ox, $oy, $dx & $dy of the 1042 | * image to be cropped. 1043 | * 1044 | * Note that you need to set the destiny image and pass the smaller (cropped) 1045 | * image to this function. 1046 | * 1047 | * @param string|Image $croppedFile The cropped image. 1048 | * @param mixed $ox Origin X. 1049 | * @param int $oy Origin Y. 1050 | * @param int $dx Destiny X. 1051 | * @param int $dy Destiny Y. 1052 | * @return array 1053 | */ 1054 | public function calculateCropMeasures($croppedFile, $ox, $oy = null, $dx = null, $dy = null) 1055 | { 1056 | list($ox, $oy, $dx, $dy) = Normalize::cropMeasures($ox, $oy, $dx, $dy); 1057 | 1058 | if (!($croppedFile instanceof self)) { 1059 | $croppedFile = new self($croppedFile); 1060 | } 1061 | 1062 | $meta = $croppedFile->getMetadata(); 1063 | 1064 | $rateWidth = $this->width / $meta['width']; 1065 | $rateHeight = $this->height / $meta['height']; 1066 | 1067 | $ox = round($ox * $rateWidth); 1068 | $oy = round($oy * $rateHeight); 1069 | $dx = round($dx * $rateHeight); 1070 | $dy = round($dy * $rateHeight); 1071 | 1072 | $width = $dx - $ox; 1073 | $height = $dy - $oy; 1074 | 1075 | return [$ox, $oy, $dx, $dy, $width, $height]; 1076 | } 1077 | 1078 | /** 1079 | * Returns image resource, so you can use it however you wan. 1080 | * 1081 | * @return resource 1082 | * @codeCoverageIgnore 1083 | */ 1084 | public function getImage() 1085 | { 1086 | return $this->image; 1087 | } 1088 | 1089 | /** 1090 | * Returns metadata for current image. 1091 | * 1092 | * @return array 1093 | * @codeCoverageIgnore 1094 | */ 1095 | public function getMetadata() 1096 | { 1097 | return $this->metadata; 1098 | } 1099 | 1100 | /** 1101 | * Gets metadata information from given $filename. 1102 | * 1103 | * @param string $filename File path 1104 | * @return array 1105 | */ 1106 | public static function getMetadataFromFile($filename) 1107 | { 1108 | $info = getimagesize($filename); 1109 | 1110 | $metadata = [ 1111 | 'width' => $info[0], 1112 | 'height' => $info[1], 1113 | 'mime' => $info['mime'], 1114 | 'exif' => null // set later, if necessary 1115 | ]; 1116 | 1117 | if (function_exists('exif_read_data') && $metadata['mime'] == 'image/jpeg') { 1118 | $metadata['exif'] = @exif_read_data($filename); 1119 | } 1120 | 1121 | return $metadata; 1122 | } 1123 | 1124 | /** 1125 | * Loads metadata to internal variables. 1126 | * 1127 | * @return void 1128 | * @codeCoverageIgnore 1129 | */ 1130 | protected function getMetadataForImage() 1131 | { 1132 | $this->metadata = $this->getMetadataFromFile($this->filename); 1133 | 1134 | $this->width = $this->metadata['width']; 1135 | $this->height = $this->metadata['height']; 1136 | } 1137 | 1138 | /** 1139 | * Gets mime for an image from its extension. 1140 | * 1141 | * @param string $filename Filename to be checked. 1142 | * @return string Mime for the filename given. 1143 | * @throws InvalidExtensionException 1144 | */ 1145 | protected function getMimeFromExtension($filename) 1146 | { 1147 | $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); 1148 | 1149 | switch ($extension) { 1150 | case 'jpg': 1151 | case 'jpeg': 1152 | return 'image/jpeg'; 1153 | case 'png': 1154 | return 'image/png'; 1155 | case 'gif': 1156 | return 'image/gif'; 1157 | default: 1158 | throw new InvalidExtensionException($extension); 1159 | } 1160 | } 1161 | 1162 | /** 1163 | * Updates current image metadata. 1164 | * 1165 | * @return void 1166 | * @codeCoverageIgnore 1167 | */ 1168 | protected function updateMetadata() 1169 | { 1170 | $this->metadata['width'] = $this->width; 1171 | $this->metadata['height'] = $this->height; 1172 | } 1173 | 1174 | /** 1175 | * Resets width and height of the current image. 1176 | * 1177 | * @return void 1178 | * @codeCoverageIgnore 1179 | */ 1180 | protected function updateSize() 1181 | { 1182 | $this->width = imagesx($this->image); 1183 | $this->height = imagesy($this->image); 1184 | 1185 | $this->updateMetadata(); 1186 | } 1187 | 1188 | /** 1189 | * Required by vignette to generate the propper colors. 1190 | * 1191 | * @param float $size Size of the vignette, between 0 and 10. Low is sharper. 1192 | * @param float $level Vignete transparency, between 0 and 1 1193 | * @param int $x X position of the pixel. 1194 | * @param int $y Y position of the pixel. 1195 | * @param array &$rgb Current pixel olor information. 1196 | * @return void 1197 | * @codeCoverageIgnore 1198 | */ 1199 | protected function vignetteEffect($size, $level, $x, $y, &$rgb) 1200 | { 1201 | $l = sin(M_PI / $this->width * $x) * sin(M_PI / $this->height * $y); 1202 | $l = pow($l, Normalize::fitInRange($size, 0, 10)); 1203 | 1204 | $l = 1 - Normalize::fitInRange($level, 0, 1) * (1 - $l); 1205 | 1206 | $rgb['red'] *= $l; 1207 | $rgb['green'] *= $l; 1208 | $rgb['blue'] *= $l; 1209 | } 1210 | } 1211 | -------------------------------------------------------------------------------- /tests/TestCase/ImageTest.php: -------------------------------------------------------------------------------- 1 | testClass = new Image; 14 | 15 | parent::setUp(); 16 | } 17 | 18 | /** 19 | * @return void 20 | */ 21 | public function testConstruct() 22 | { 23 | $image = "{$this->files_path}peke.jpg"; 24 | $instance = new Image($image); 25 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 26 | } 27 | 28 | /** 29 | * @expectedException \Elboletaire\Watimage\Exception\InvalidMimeException 30 | * @return void 31 | */ 32 | public function testLoad() 33 | { 34 | $image = "{$this->files_path}peke.jpg"; 35 | $instance = $this->testClass->load($image); 36 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 37 | // Check filename has been properly loaded 38 | $this->assertEquals($image, $this->getProperty('filename')); 39 | // Check gd resource has been created 40 | $this->assertEquals('gd', get_resource_type($this->getProperty('image'))); 41 | // Check for metadata 42 | $this->assertNotEmpty($this->testClass->getMetadata()); 43 | 44 | // Check loading passing quality 45 | $this->testClass->load([ 46 | 'file' => $image, 47 | 'quality' => 20 48 | ]); 49 | $this->assertEquals(20, $this->getProperty('quality')); 50 | 51 | // Check gif load 52 | $gif = "{$this->files_path}peke.gif"; 53 | $instance = $this->testClass->load($gif); 54 | // Check gd resource has been created 55 | $this->assertEquals('gd', get_resource_type($this->getProperty('image'))); 56 | // Check for metadata 57 | $metadata = $this->testClass->getMetadata(); 58 | $this->assertNotEmpty($metadata); 59 | $this->assertEquals('image/gif', $metadata['mime']); 60 | 61 | // Check InvalidMimeException 62 | $file = "{$this->files_path}LICENSE"; 63 | $this->testClass->load($file); 64 | } 65 | 66 | /** 67 | * @expectedException \Elboletaire\Watimage\Exception\InvalidArgumentException 68 | * @return void 69 | */ 70 | public function testLoadArgumentsFail() 71 | { 72 | $this->testClass->load(null); 73 | } 74 | 75 | /** 76 | * @expectedException \Elboletaire\Watimage\Exception\FileNotExistException 77 | * @return void 78 | */ 79 | public function testLoadFileNotExistFail() 80 | { 81 | $this->testClass->load('a-non-existant-file.png'); 82 | } 83 | 84 | /** 85 | * @return void 86 | */ 87 | public function testCreate() 88 | { 89 | $image = $this->testClass->create(250, 400); 90 | // Check chaining 91 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $image); 92 | // Check size 93 | $this->assertEquals(250, $this->getProperty('width')); 94 | $this->assertEquals(400, $this->getProperty('height')); 95 | // Check it again 96 | $resource = $this->testClass->getImage(); 97 | $this->assertEquals(250, imagesx($resource)); 98 | $this->assertEquals(400, imagesy($resource)); 99 | 100 | // Check that height is set to width when there's no height specified 101 | $this->testClass->create(350); 102 | $this->assertEquals(350, $this->getProperty('width')); 103 | $this->assertEquals(350, $this->getProperty('height')); 104 | 105 | $this->assertEquals('gd', get_resource_type($resource)); 106 | 107 | $metadata = $this->testClass->getMetadata(); 108 | $this->assertNull($metadata['exif']); 109 | } 110 | 111 | /** 112 | * @expectedException \Elboletaire\Watimage\Exception\InvalidArgumentException 113 | * @return void 114 | */ 115 | public function testCreateArgumentsFail() 116 | { 117 | $this->testClass->create(null); 118 | } 119 | 120 | /** 121 | * @runInSeparateProcess 122 | * @return void 123 | */ 124 | public function testGenerate() 125 | { 126 | $input = "{$this->files_path}test.png"; 127 | $output = $this->getOutputFilename("image-generate.png"); 128 | 129 | // Check that image can be generated and printed to screen 130 | ob_start(); 131 | $this->testClass->load($input)->generate(); 132 | $buffer = ob_get_contents(); 133 | ob_end_clean(); 134 | $this->assertNotEmpty($buffer); 135 | 136 | // Check output 137 | $this->assertFileNotExists($output); 138 | 139 | // Generate saving to file 140 | $image = $this->testClass->load($input)->generate($output); 141 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $image); 142 | $this->assertFileExists($output); 143 | $this->assertGreaterThan(0, filesize($output)); 144 | 145 | // Check gif export 146 | $output = $this->getOutputFilename('image-generate.gif'); 147 | $image = $this->testClass->load($input)->generate($output); 148 | $this->assertFileExists($output); 149 | $this->assertGreaterThan(0, filesize($output)); 150 | } 151 | 152 | /** 153 | * @runInSeparateProcess 154 | * @expectedException \Elboletaire\Watimage\Exception\InvalidArgumentException 155 | * @return void 156 | */ 157 | public function testGenerateThrowsInvalidArgumentException() 158 | { 159 | $input = "{$this->files_path}test.png"; 160 | 161 | $image = $this->testClass->load($input); 162 | $image->generate(null, 'invented'); 163 | } 164 | 165 | /** 166 | * Test save 167 | * 168 | * @return void 169 | */ 170 | public function testSave() 171 | { 172 | $image = "{$this->files_path}test.png"; 173 | $output = $this->getOutputFilename("image-save.png"); 174 | 175 | $this->assertFileNotExists($output); 176 | $image = $this->testClass->load($image)->flip()->save($output); 177 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $image); 178 | $this->assertFileExists($output); 179 | $this->assertGreaterThan(0, filesize($output)); 180 | // Check that it overrides original file 181 | $original_size = filesize($output); 182 | $this->testClass->load($output)->resize('min', 50)->save(); 183 | // We need to clear file status cache to get correct sizes 184 | clearstatcache(); 185 | $this->assertFileExists($output); 186 | $this->assertGreaterThan(0, filesize($output)); 187 | $this->assertNotEquals($original_size, filesize($output)); 188 | } 189 | 190 | public function testToString() 191 | { 192 | $image = "{$this->files_path}test.png"; 193 | $this->testClass->load($image); 194 | $string = $this->testClass->toString(); 195 | $this->assertTrue(is_string($string)); 196 | 197 | $string = $this->testClass->toString(true); 198 | $this->assertRegExp('@^data:image/png;base64,@', $string); 199 | } 200 | 201 | public function testFromString() 202 | { 203 | $image = $this->testClass->fromString('data:image/jpeg;base64,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AAA4QklNA+0AAAAAABAAR///AAIAAgBH//8AAgACOEJJTQQmAAAAAAAOAAAAAAAAAAAAAD+AAAA4QklNBA0AAAAAAAQAAAB4OEJJTQQZAAAAAAAEAAAAHjhCSU0D8wAAAAAACQAAAAAAAAAAAQA4QklNJxAAAAAAAAoAAQAAAAAAAAACOEJJTQP1AAAAAABIAC9mZgABAGxmZgAGAAAAAAABAC9mZgABAKGZmgAGAAAAAAABADIAAAABAFoAAAAGAAAAAAABADUAAAABAC0AAAAGAAAAAAABOEJJTQP4AAAAAABwAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAADhCSU0ECAAAAAAAEAAAAAEAAAJAAAACQAAAAAA4QklNBB4AAAAAAAQAAAAAOEJJTQQaAAAAAANJAAAABgAAAAAAAAAAAAAAAQAAAAEAAAAKAFUAbgB0AGkAdABsAGUAZAAtADEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAEAAAAAAABudWxsAAAAAgAAAAZib3VuZHNPYmpjAAAAAQAAAAAAAFJjdDEAAAAEAAAAAFRvcCBsb25nAAAAAAAAAABMZWZ0bG9uZwAAAAAAAAAAQnRvbWxvbmcAAAABAAAAAFJnaHRsb25nAAAAAQAAAAZzbGljZXNWbExzAAAAAU9iamMAAAABAAAAAAAFc2xpY2UAAAASAAAAB3NsaWNlSURsb25nAAAAAAAAAAdncm91cElEbG9uZwAAAAAAAAAGb3JpZ2luZW51bQAAAAxFU2xpY2VPcmlnaW4AAAANYXV0b0dlbmVyYXRlZAAAAABUeXBlZW51bQAAAApFU2xpY2VUeXBlAAAAAEltZyAAAAAGYm91bmRzT2JqYwAAAAEAAAAAAABSY3QxAAAABAAAAABUb3AgbG9uZwAAAAAAAAAATGVmdGxvbmcAAAAAAAAAAEJ0b21sb25nAAAAAQAAAABSZ2h0bG9uZwAAAAEAAAADdXJsVEVYVAAA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cm1hdD0iaW1hZ2UvanBlZyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCRTdEQjM2ODcyMERFNjExQjY4N0REREE0M0MxRUQzQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCRTdEQjM2ODcyMERFNjExQjY4N0REREE0M0MxRUQzQyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOkJFN0RCMzY4NzIwREU2MTFCNjg3REREQTQzQzFFRDNDIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiPg0KCQkJPHhtcE1NOkhpc3Rvcnk+DQoJCQkJPHJkZjpTZXE+DQoJCQkJCTxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOkJFN0RCMzY4NzIwREU2MTFCNjg3REREQTQzQzFFRDNDIiBzdEV2dDp3aGVuPSIyMDE2LTA0LTI4VDIwOjUyOjUxKzAyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSIvPg0KCQkJCTwvcmRmOlNlcT4NCgkJCTwveG1wTU06SGlzdG9yeT4NCgkJPC9yZGY6RGVzY3JpcHRpb24+DQoJPC9yZGY6UkRGPg0KPC94OnhtcG1ldGE+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0ndyc/Pv/iDFhJQ0NfUFJPRklMRQABAQAADEhMaW5vAhAAAG1udHJSR0IgWFlaIAfOAAIACQAGADEAAGFjc3BNU0ZUAAAAAElFQyBzUkdCAAAAAAAAAAAAAAAB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'); 204 | $resource = $image->getImage(); 205 | $index = imagecolorat($resource, 0, 0); 206 | $color = imagecolorsforindex($resource, $index); 207 | $expected = ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0]; 208 | $this->assertArraySubset($expected, $color); 209 | $image = $this->testClass->fromString('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AAA4QklNA+0AAAAAABAAR///AAIAAgBH//8AAgACOEJJTQQmAAAAAAAOAAAAAAAAAAAAAD+AAAA4QklNBA0AAAAAAAQAAAB4OEJJTQQZAAAAAAAEAAAAHjhCSU0D8wAAAAAACQAAAAAAAAAAAQA4QklNJxAAAAAAAAoAAQAAAAAAAAACOEJJTQP1AAAAAABIAC9mZgABAGxmZgAGAAAAAAABAC9mZgABAKGZmgAGAAAAAAABADIAAAABAFoAAAAGAAAAAAABADUAAAABAC0AAAAGAAAAAAABOEJJTQP4AAAAAABwAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAADhCSU0ECAAAAAAAEAAAAAEAAAJAAAACQAAAAAA4QklNBB4AAAAAAAQAAAAAOEJJTQQaAAAAAANJAAAABgAAAAAAAAAAAAAAAQAAAAEAAAAKAFUAbgB0AGkAdABsAGUAZAAtADEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAEAAAAAAABudWxsAAAAAgAAAAZib3VuZHNPYmpjAAAAAQAAAAAAAFJjdDEAAAAEAAAAAFRvcCBsb25nAAAAAAAAAABMZWZ0bG9uZwAAAAAAAAAAQnRvbWxvbmcAAAABAAAAAFJnaHRsb25nAAAAAQAAAAZzbGljZXNWbExzAAAAAU9iamMAAAABAAAAAAAFc2xpY2UAAAASAAAAB3NsaWNlSURsb25nAAAAAAAAAAdncm91cElEbG9uZwAAAAAAAAAGb3JpZ2luZW51bQAAAAxFU2xpY2VPcmlnaW4AAAANYXV0b0dlbmVyYXRlZAAAAABUeXBlZW51bQAAAApFU2xpY2VUeXBlAAAAAEltZyAAAAAGYm91bmRzT2JqYwAAAAEAAAAAAABSY3QxAAAABAAAAABUb3AgbG9uZwAAAAAAAAAATGVmdGxvbmcAAAAAAAAAAEJ0b21sb25nAAAAAQAAAABSZ2h0bG9uZwAAAAEAAAADdXJsVEVYVAAA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cm1hdD0iaW1hZ2UvanBlZyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCRTdEQjM2ODcyMERFNjExQjY4N0REREE0M0MxRUQzQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCRTdEQjM2ODcyMERFNjExQjY4N0REREE0M0MxRUQzQyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOkJFN0RCMzY4NzIwREU2MTFCNjg3REREQTQzQzFFRDNDIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiPg0KCQkJPHhtcE1NOkhpc3Rvcnk+DQoJCQkJPHJkZjpTZXE+DQoJCQkJCTxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOkJFN0RCMzY4NzIwREU2MTFCNjg3REREQTQzQzFFRDNDIiBzdEV2dDp3aGVuPSIyMDE2LTA0LTI4VDIwOjUyOjUxKzAyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSIvPg0KCQkJCTwvcmRmOlNlcT4NCgkJCTwveG1wTU06SGlzdG9yeT4NCgkJPC9yZGY6RGVzY3JpcHRpb24+DQoJPC9yZGY6UkRGPg0KPC94OnhtcG1ldGE+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0ndyc/Pv/iDFhJQ0NfUFJPRklMRQABAQAADEhMaW5vAhAAAG1udHJSR0IgWFlaIAfOAAIACQAGADEAAGFjc3BNU0ZUAAAAAElFQyBzUkdCAAAAAAAAAAAAAAAB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'); 210 | $resource = $image->getImage(); 211 | $index = imagecolorat($resource, 0, 0); 212 | $color = imagecolorsforindex($resource, $index); 213 | $expected = ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0]; 214 | $this->assertArraySubset($expected, $color); 215 | } 216 | 217 | /** 218 | * @return void 219 | * @group orientation 220 | */ 221 | public function testAutoOrientate() 222 | { 223 | $image = "{$this->files_path}tripi.jpg"; 224 | $output = $this->getOutputFilename("image-auto-orientate.jpg"); 225 | 226 | // I know that image must be rotated, so... 227 | // disable auto orientate to manually do it.. 228 | $this->testClass->load($image, false); 229 | $original_metadata = $this->testClass->getMetadata(); 230 | // auto orientate 231 | $instance = $this->testClass->autoOrientate(); 232 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 233 | // and save 234 | $instance->save($output); 235 | // get new image size 236 | list($width, $height) = getimagesize($output); 237 | $this->assertEquals($original_metadata['width'], $height); 238 | $this->assertEquals($original_metadata['height'], $width); 239 | } 240 | 241 | /** 242 | * @return void 243 | * @group orientation 244 | */ 245 | public function testRotate() 246 | { 247 | // We know this image has portrait orientation 248 | $image = "{$this->files_path}test.png"; 249 | $output = $this->getOutputFilename("image-rotate.png"); 250 | 251 | // Check rotation image size 252 | $image = $this->testClass->load($image); 253 | // Get current width and height 254 | $old_width = $this->getProperty('width'); 255 | $old_height = $this->getProperty('height'); 256 | // Rotate it 257 | $instance = $image->rotate(90); 258 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 259 | $instance->generate($output); 260 | list($width, $height) = getimagesize($output); 261 | // Knowing rotation, check width according to it 262 | $this->assertLessThanOrEqual($old_width, $height); 263 | $this->assertGreaterThan($old_width, $width); 264 | // Knowing rotation, check height according to it 265 | $this->assertLessThanOrEqual($old_height, $width); 266 | $this->assertLessThan($old_height, $height); 267 | } 268 | 269 | /** 270 | * @return void 271 | * @group resize 272 | */ 273 | public function testResize() 274 | { 275 | $image = "{$this->files_path}peke.jpg"; 276 | $output = $this->getOutputFilename("image-resize.jpg"); 277 | 278 | $types = [ 279 | 'classic', 280 | 'resize', 281 | 'reduce', 282 | 'resizemin', 283 | 'min', 284 | 'crop', 285 | 'resizecrop' 286 | ]; 287 | 288 | // Test types fallback 289 | $this->testClass->load($image); 290 | 291 | // We're just gonna check that it does not crash. 292 | // Every method is tested in its proper test method. 293 | foreach ($types as $type) { 294 | $instance = $this->testClass->resize($type, 200); 295 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 296 | } 297 | 298 | $this->testClass->generate($output); 299 | } 300 | 301 | /** 302 | * @expectedException \Elboletaire\Watimage\Exception\InvalidArgumentException 303 | * @group resize 304 | */ 305 | public function testResizeFail() 306 | { 307 | $image = "{$this->files_path}test.png"; 308 | 309 | $this->testClass->load($image)->resize('fail', 'fail'); 310 | } 311 | 312 | /** 313 | * @return void 314 | * @group resize 315 | */ 316 | public function testClassicResize() 317 | { 318 | $image = "{$this->files_path}test.png"; 319 | $output = $this->getOutputFilename("image-classic-resize.png"); 320 | 321 | $this->testClass->load($image); 322 | $instance = $this->testClass->classicResize(200, 300); 323 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 324 | $instance->generate($output); 325 | 326 | // Check that the current size corresponds to the defined one 327 | list($width, $height) = getimagesize($output); 328 | $this->assertEquals(182, $width); 329 | $this->assertEquals(300, $height); 330 | 331 | // Check that the size of the image has been updated 332 | $this->assertEquals(182, $this->getProperty('width')); 333 | $this->assertEquals(300, $this->getProperty('height')); 334 | 335 | $this->testClass->load($image); 336 | $metadata = $this->testClass->getMetadata(); 337 | $this->testClass 338 | ->classicResize($metadata['width'], $metadata['height']) 339 | ->rotate(90)->classicResize(300, 200) 340 | ; 341 | } 342 | 343 | /** 344 | * @return void 345 | * @group resize 346 | */ 347 | public function testReduce() 348 | { 349 | $image = "{$this->files_path}test.png"; 350 | $output = $this->getOutputFilename("image-resizemin.png"); 351 | 352 | $this->testClass->load($image); 353 | $instance = $this->testClass->reduce(200, 300); 354 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 355 | $instance->generate($output); 356 | 357 | // Check that the current size corresponds to the defined one 358 | list($width, $height) = getimagesize($output); 359 | $this->assertEquals(182, $width); 360 | $this->assertEquals(300, $height); 361 | 362 | // Check that the size of the image has been updated 363 | $this->assertEquals(182, $this->getProperty('width')); 364 | $this->assertEquals(300, $this->getProperty('height')); 365 | } 366 | 367 | /** 368 | * @return void 369 | * @group resize 370 | */ 371 | public function testClassicCrop() 372 | { 373 | $image = "{$this->files_path}test.png"; 374 | $output = $this->getOutputFilename("image-classic-crop.png"); 375 | 376 | $this->testClass->load($image); 377 | $instance = $this->testClass->classicCrop(200, 250); 378 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 379 | $instance->generate($output); 380 | 381 | // Check that the current size corresponds to the defined one 382 | list($width, $height) = getimagesize($output); 383 | $this->assertEquals(200, $width); 384 | $this->assertEquals(250, $height); 385 | 386 | // Check that the size of the image has been updated 387 | $this->assertEquals(200, $this->getProperty('width')); 388 | $this->assertEquals(250, $this->getProperty('height')); 389 | } 390 | 391 | /** 392 | * @return void 393 | * @group resize 394 | */ 395 | public function testResizeCrop() 396 | { 397 | $image = "{$this->files_path}test.png"; 398 | $output = $this->getOutputFilename("image-resize-crop.png"); 399 | 400 | $this->testClass->load($image); 401 | $instance = $this->testClass->resizeCrop(200, 250); 402 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 403 | $instance->generate($output); 404 | 405 | // Check that the current size corresponds to the defined one 406 | list($width, $height) = getimagesize($output); 407 | $this->assertEquals(200, $width); 408 | $this->assertEquals(250, $height); 409 | 410 | // Check that the size of the image has been updated 411 | $this->assertEquals(200, $width); 412 | $this->assertEquals(250, $height); 413 | } 414 | 415 | /** 416 | * @return void 417 | * @group effects 418 | */ 419 | public function testFlip() 420 | { 421 | $image = "{$this->files_path}test.png"; 422 | $output = $this->getOutputFilename("image-flip.png"); 423 | 424 | $this->testClass->load($image); 425 | $metadata = $this->testClass->getMetadata(); 426 | $instance = $this->testClass->flip('horizontal'); 427 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 428 | $instance->generate($output); 429 | list($width, $height) = getimagesize($output); 430 | $this->assertEquals($metadata['width'], $width); 431 | $this->assertEquals($metadata['height'], $height); 432 | 433 | $instance = $this->testClass->flip('vertical'); 434 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 435 | $instance->generate($output); 436 | list($width, $height) = getimagesize($output); 437 | $this->assertEquals($metadata['width'], $width); 438 | $this->assertEquals($metadata['height'], $height); 439 | 440 | $instance = $this->testClass->flip('both'); 441 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 442 | $instance->generate($output); 443 | list($width, $height) = getimagesize($output); 444 | $this->assertEquals($metadata['width'], $width); 445 | $this->assertEquals($metadata['height'], $height); 446 | } 447 | 448 | /** 449 | * @return void 450 | * @group orientation 451 | */ 452 | public function testConvenienceFlip() 453 | { 454 | $image = "{$this->files_path}test.png"; 455 | $output = $this->getOutputFilename("image-convenience-flip.png"); 456 | 457 | $this->testClass->load($image); 458 | $metadata = $this->testClass->getMetadata(); 459 | $instance = $this->testClass->convenienceFlip('horizontal'); 460 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 461 | $instance->generate($output); 462 | list($width, $height) = getimagesize($output); 463 | $this->assertEquals($metadata['width'], $width); 464 | $this->assertEquals($metadata['height'], $height); 465 | 466 | $instance = $this->testClass->convenienceFlip('vertical'); 467 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 468 | $instance->generate($output); 469 | list($width, $height) = getimagesize($output); 470 | $this->assertEquals($metadata['width'], $width); 471 | $this->assertEquals($metadata['height'], $height); 472 | 473 | $instance = $this->testClass->convenienceFlip('both'); 474 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 475 | $instance->generate($output); 476 | list($width, $height) = getimagesize($output); 477 | $this->assertEquals($metadata['width'], $width); 478 | $this->assertEquals($metadata['height'], $height); 479 | } 480 | 481 | /** 482 | * @return void 483 | * @group draw 484 | */ 485 | public function testFill() 486 | { 487 | // Create a 1px x 1px canvas 488 | $image = $this->testClass->create(1, 1); 489 | // Fill it with red 490 | $instance = $image->fill('#f00'); 491 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 492 | // Get color at that unique pixel 493 | $resource = $image->getImage(); 494 | $color = imagecolorsforindex($resource, imagecolorat($resource, 0, 0)); 495 | // Assert is red 496 | $this->assertArraySubset([ 497 | 'red' => 255, 498 | 'green' => 0, 499 | 'blue' => 0, 500 | 'alpha' => 0 501 | ], $color); 502 | } 503 | 504 | /** 505 | * @return void 506 | * @group resize 507 | */ 508 | public function testCrop() 509 | { 510 | $image = "{$this->files_path}peke.jpg"; 511 | $output = $this->getOutputFilename("image-crop.jpg"); 512 | 513 | $this->testClass->load($image); 514 | $metadata = $this->testClass->getMetadata(); 515 | // Get color index at crop position 516 | $resource = $this->testClass->getImage(); 517 | $color = imagecolorsforindex($resource, imagecolorat($resource, 500, 500)); 518 | // Crop 519 | $instance = $this->testClass->crop(500, 500, 100, 150); 520 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 521 | $instance->generate($output); 522 | // Get color index at cropped position and compare 523 | $resource = $this->testClass->getImage(); 524 | $new_color = imagecolorsforindex($resource, imagecolorat($resource, 0, 0)); 525 | $this->assertArraySubset($color, $new_color); 526 | // Compare size 527 | list($width, $height) = getimagesize($output); 528 | $this->assertNotEquals($metadata['width'], $width); 529 | $this->assertNotEquals($metadata['height'], $height); 530 | $this->assertEquals(100, $width); 531 | $this->assertEquals(150, $height); 532 | } 533 | 534 | /** 535 | * The blur method test. 536 | * 537 | * @return void 538 | * @group effects 539 | */ 540 | public function testBlur() 541 | { 542 | $image = "{$this->files_path}peke.jpg"; 543 | 544 | $image = $this->testClass->load($image); 545 | $instance = $image->blur(); 546 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 547 | 548 | $instance = $image->blur('selective'); 549 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 550 | } 551 | 552 | /** 553 | * @expectedException \Elboletaire\Watimage\Exception\InvalidArgumentException 554 | * @group effects 555 | */ 556 | public function testBlurFail() 557 | { 558 | $this->testClass->create(250, 250)->fill('#f00')->blur('fail'); 559 | } 560 | 561 | /** 562 | * The brightness method test. 563 | * 564 | * @return void 565 | * @group effects 566 | */ 567 | public function testBrightness() 568 | { 569 | $image = "{$this->files_path}peke.jpg"; 570 | 571 | $image = $this->testClass->load($image); 572 | $instance = $image->brightness(23); 573 | 574 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 575 | } 576 | 577 | /** 578 | * The colorize method test. 579 | * 580 | * @return void 581 | * @group effects 582 | */ 583 | public function testColorize() 584 | { 585 | $image = "{$this->files_path}peke.jpg"; 586 | 587 | $image = $this->testClass->load($image); 588 | $instance = $image->brightness(23); 589 | 590 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 591 | } 592 | 593 | /** 594 | * The contrast method test. 595 | * 596 | * @return void 597 | * @group effects 598 | */ 599 | public function testContrast() 600 | { 601 | $image = "{$this->files_path}peke.jpg"; 602 | 603 | $image = $this->testClass->load($image); 604 | $instance = $image->contrast(23); 605 | 606 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 607 | } 608 | 609 | /** 610 | * The edgeDetection method test. 611 | * 612 | * @return void 613 | * @group effects 614 | */ 615 | public function testEdgeDetection() 616 | { 617 | $image = "{$this->files_path}peke.jpg"; 618 | 619 | $image = $this->testClass->load($image); 620 | $instance = $image->edgeDetection(); 621 | 622 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 623 | } 624 | 625 | /** 626 | * The emboss method test. 627 | * 628 | * @return void 629 | * @group effects 630 | */ 631 | public function testEmboss() 632 | { 633 | $image = "{$this->files_path}peke.jpg"; 634 | 635 | $image = $this->testClass->load($image); 636 | $instance = $image->emboss(); 637 | 638 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 639 | } 640 | 641 | /** 642 | * The grayscale method test. 643 | * 644 | * @return void 645 | * @group effects 646 | */ 647 | public function testGrayscale() 648 | { 649 | $image = "{$this->files_path}peke.jpg"; 650 | 651 | $image = $this->testClass->load($image); 652 | $instance = $image->grayscale(); 653 | 654 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 655 | } 656 | 657 | /** 658 | * The meanRemove method test. 659 | * 660 | * @return void 661 | * @group effects 662 | */ 663 | public function testMeanRemove() 664 | { 665 | $image = "{$this->files_path}peke.jpg"; 666 | 667 | $image = $this->testClass->load($image); 668 | $instance = $image->meanRemove(); 669 | 670 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 671 | } 672 | 673 | /** 674 | * The negate method test. 675 | * 676 | * @return void 677 | * @group effects 678 | */ 679 | public function testNegate() 680 | { 681 | $image = "{$this->files_path}peke.jpg"; 682 | 683 | $image = $this->testClass->load($image); 684 | $instance = $image->negate(); 685 | 686 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 687 | } 688 | 689 | /** 690 | * The pixelate method test. 691 | * 692 | * @return void 693 | * @group effects 694 | */ 695 | public function testPixelate() 696 | { 697 | $image = "{$this->files_path}peke.jpg"; 698 | 699 | $image = $this->testClass->load($image); 700 | $instance = $image->pixelate(); 701 | 702 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 703 | } 704 | 705 | /** 706 | * The sepia method test. 707 | * 708 | * @return void 709 | * @group effects 710 | */ 711 | public function testSepia() 712 | { 713 | $image = "{$this->files_path}peke.jpg"; 714 | 715 | $image = $this->testClass->load($image); 716 | $instance = $image->sepia(); 717 | 718 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 719 | } 720 | 721 | /** 722 | * The smooth method test. 723 | * 724 | * @return void 725 | * @group effects 726 | */ 727 | public function testSmooth() 728 | { 729 | $image = "{$this->files_path}peke.jpg"; 730 | 731 | $image = $this->testClass->load($image); 732 | $instance = $image->smooth(5); 733 | 734 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 735 | } 736 | 737 | /** 738 | * The vignette method test. 739 | * 740 | * @return void 741 | * @group effects 742 | * @group slow 743 | */ 744 | public function testVignette() 745 | { 746 | $image = "{$this->files_path}peke.jpg"; 747 | 748 | $instance = $this->testClass->load($image); 749 | // Let's create a very dark vignette to check if borders are black 750 | $instance = $instance->vignette(10, 1); 751 | 752 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 753 | 754 | $resource = $instance->getImage(); 755 | $color = imagecolorsforindex($resource, imagecolorat($resource, 0, 0)); 756 | 757 | $this->assertArraySubset([ 758 | 'red' => 0, 759 | 'green' => 0, 760 | 'blue' => 0, 761 | 'alpha' => 0 762 | ], $color); 763 | } 764 | 765 | public function testSetImage() 766 | { 767 | $this->testClass->load("{$this->files_path}test.png"); 768 | 769 | $rsc_image = "{$this->files_path}peke.jpg"; 770 | $resource = imagecreatefromjpeg($rsc_image); 771 | 772 | $instance = $this->testClass->setImage($resource); 773 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 774 | 775 | // Ensure has updated the size 776 | $this->assertEquals(1024, $this->getProperty('width')); 777 | $this->assertEquals(682, $this->getProperty('height')); 778 | 779 | $this->setExpectedException('Exception'); 780 | $this->testClass->setImage("{$this->files_path}peke.jpg"); 781 | } 782 | 783 | public function testCalculateCropMeasures() 784 | { 785 | $this->testClass->load("{$this->files_path}peke.jpg"); 786 | $cropped = new Image("{$this->files_path}peke.jpg"); 787 | $cropped->resizeMin(500, 500); 788 | $this->assertArraySubset( 789 | [41, 41, 205, 205, 164, 164], 790 | $this->testClass->calculateCropMeasures($cropped, 20, 20, 100, 100) 791 | ); 792 | 793 | $this->testClass->load("{$this->files_path}peke.jpg"); 794 | $this->assertArraySubset( 795 | [26, 26, 128, 128, 102, 102], 796 | $this->testClass->calculateCropMeasures( 797 | "{$this->files_path}peke.gif", 798 | [ 799 | 'ox' => 20, 800 | 'oy' => 20, 801 | 'dx' => 100, 802 | 'dy' => 100 803 | ] 804 | ) 805 | ); 806 | } 807 | 808 | public function testGetMetadataFromFile() 809 | { 810 | $image = "{$this->files_path}peke.jpg"; 811 | 812 | $expected = [ 813 | 'width' => 1024, 814 | 'height' => 682, 815 | 'mime' => 'image/jpeg', 816 | 'exif' => null // we're not testing exif functions 817 | ]; 818 | 819 | $metadata = $this->testClass->getMetadataFromFile($image); 820 | // unset exif 821 | $metadata['exif'] = null; 822 | $this->assertArraySubset($expected, $metadata); 823 | } 824 | 825 | public function testGetMimeFromExtension() 826 | { 827 | $method = $this->getMethod('getMimeFromExtension'); 828 | 829 | $this->assertEquals('image/jpeg', $method->invoke($this->testClass, 'image.jpg')); 830 | $this->assertEquals('image/jpeg', $method->invoke($this->testClass, 'image.jpeg')); 831 | $this->assertEquals('image/gif', $method->invoke($this->testClass, 'image.gif')); 832 | $this->assertEquals('image/png', $method->invoke($this->testClass, 'image.png')); 833 | 834 | $this->setExpectedException('Elboletaire\Watimage\Exception\InvalidExtensionException'); 835 | $method->invoke($this->testClass, 'image.bmp'); 836 | } 837 | 838 | /** 839 | * @return void 840 | */ 841 | public function testDestroy() 842 | { 843 | $image = "{$this->files_path}peke.jpg"; 844 | $instance = $this->testClass->load($image); 845 | 846 | $this->assertNotNull($this->getProperty('filename')); 847 | $this->assertNotNull($this->getProperty('width')); 848 | $this->assertNotNull($this->getProperty('height')); 849 | 850 | $instance = $instance->destroy(); 851 | $this->assertInstanceOf('Elboletaire\Watimage\Image', $instance); 852 | 853 | $this->assertArraySubset([], $this->getProperty('metadata')); 854 | $this->assertNull($this->getProperty('filename')); 855 | $this->assertNull($this->getProperty('width')); 856 | $this->assertNull($this->getProperty('height')); 857 | } 858 | } 859 | --------------------------------------------------------------------------------