diff --git a/.travis.yml b/.travis.yml index 8dcadc25..87bbb1e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: php +sudo: false + +dist: trusty + php: - 5.4 - 5.5 @@ -14,15 +18,9 @@ matrix: - php: nightly - php: hhvm -before_install: - - sudo add-apt-repository -y ppa:moti-p/cc - - sudo apt-get update - - sudo apt-get -y --reinstall install imagemagick - - yes | pecl install imagick-beta - - if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.4" ]]; then echo "extension = imagick.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi - before_script: - - composer self-update - - composer install --prefer-source --no-interaction --dev + - printf "\n" | pecl install imagick + - composer self-update || true + - composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader script: vendor/bin/phpunit diff --git a/README.md b/README.md index 40f96dc5..06b84e01 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Intervention Image is a **PHP image handling and manipulation** library providing an easier and expressive way to create, edit, and compose images. The package includes ServiceProviders and Facades for easy **Laravel** integration. +[![Latest Version](https://img.shields.io/packagist/v/intervention/image.svg)](https://packagist.org/packages/intervention/image) [![Build Status](https://travis-ci.org/Intervention/image.png?branch=master)](https://travis-ci.org/Intervention/image) +[![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image.svg)](https://packagist.org/packages/intervention/image/stats) ## Requirements @@ -18,7 +20,7 @@ Intervention Image is a **PHP image handling and manipulation** library providin - [Installation](http://image.intervention.io/getting_started/installation) - [Laravel Framework Integration](http://image.intervention.io/getting_started/installation#laravel) -- [Official Documentation](http://image.intervention.io/) +- [Basic Usage](http://image.intervention.io/use/basics) ## Code Examples @@ -36,7 +38,7 @@ $img->insert('public/watermark.png'); $img->save('public/bar.jpg'); ``` -Refer to the [documentation](http://image.intervention.io/) to learn more about Intervention Image. +Refer to the [official documentation](http://image.intervention.io/) to learn more about Intervention Image. ## Contributing @@ -50,4 +52,4 @@ Contributions to the Intervention Image library are welcome. Please note the fol Intervention Image is licensed under the [MIT License](http://opensource.org/licenses/MIT). -Copyright 2014 [Oliver Vogel](http://olivervogel.net/) +Copyright 2017 [Oliver Vogel](http://olivervogel.com/) diff --git a/composer.json b/composer.json index 3817a4b5..136d7b21 100644 --- a/composer.json +++ b/composer.json @@ -7,8 +7,8 @@ "authors": [ { "name": "Oliver Vogel", - "email": "oliver@olivervogel.net", - "homepage": "http://olivervogel.net/" + "email": "oliver@olivervogel.com", + "homepage": "http://olivervogel.com/" } ], "require": { @@ -17,7 +17,7 @@ "guzzlehttp/psr7": "~1.1" }, "require-dev": { - "phpunit/phpunit": "3.*", + "phpunit/phpunit": "^4.8 || ^5.7", "mockery/mockery": "~0.9.2" }, "suggest": { @@ -33,6 +33,14 @@ "extra": { "branch-alias": { "dev-master": "2.3-dev" + }, + "laravel": { + "providers": [ + "Intervention\\Image\\ImageServiceProvider" + ], + "aliases": { + "Image": "Intervention\\Image\\Facades\\Image" + } } }, "minimum-stability": "stable" diff --git a/src/Intervention/Image/AbstractDecoder.php b/src/Intervention/Image/AbstractDecoder.php index 419d5cfc..ac93cf0b 100644 --- a/src/Intervention/Image/AbstractDecoder.php +++ b/src/Intervention/Image/AbstractDecoder.php @@ -2,6 +2,9 @@ namespace Intervention\Image; +use GuzzleHttp\Psr7\Stream; +use Psr\Http\Message\StreamInterface; + abstract class AbstractDecoder { /** @@ -85,22 +88,35 @@ abstract class AbstractDecoder /** * Init from given stream * - * @param $stream + * @param StreamInterface|resource $stream * @return \Intervention\Image\Image */ public function initFromStream($stream) { - $offset = ftell($stream); - $shouldAndCanSeek = $offset !== 0 && $this->isStreamSeekable($stream); - - if ($shouldAndCanSeek) { - rewind($stream); + if (!$stream instanceof StreamInterface) { + $stream = new Stream($stream); } - $data = @stream_get_contents($stream); + try { + $offset = $stream->tell(); + } catch (\RuntimeException $e) { + $offset = 0; + } + + $shouldAndCanSeek = $offset !== 0 && $stream->isSeekable(); if ($shouldAndCanSeek) { - fseek($stream, $offset); + $stream->rewind(); + } + + try { + $data = $stream->getContents(); + } catch (\RuntimeException $e) { + $data = null; + } + + if ($shouldAndCanSeek) { + $stream->seek($offset); } if ($data) { @@ -112,18 +128,6 @@ abstract class AbstractDecoder ); } - /** - * Checks if we can move the pointer for this stream - * - * @param resource $stream - * @return bool - */ - private function isStreamSeekable($stream) - { - $metadata = stream_get_meta_data($stream); - return $metadata['seekable']; - } - /** * Determines if current source data is GD resource * @@ -213,6 +217,7 @@ abstract class AbstractDecoder */ public function isStream() { + if ($this->data instanceof StreamInterface) return true; if (!is_resource($this->data)) return false; if (get_resource_type($this->data) !== 'stream') return false; @@ -332,6 +337,7 @@ abstract class AbstractDecoder case $this->isFilePath(): return $this->initFromPath($this->data); + // isBase64 has to be after isFilePath to prevent false positives case $this->isBase64(): return $this->initFromBinary(base64_decode($this->data)); diff --git a/src/Intervention/Image/AbstractEncoder.php b/src/Intervention/Image/AbstractEncoder.php index 5e0cce2f..0b56db55 100644 --- a/src/Intervention/Image/AbstractEncoder.php +++ b/src/Intervention/Image/AbstractEncoder.php @@ -74,6 +74,13 @@ abstract class AbstractEncoder */ abstract protected function processIco(); + /** + * Processes and returns image as WebP encoded string + * + * @return string + */ + abstract protected function processWebp(); + /** * Process a given image * @@ -145,6 +152,12 @@ abstract class AbstractEncoder case 'image/vnd.adobe.photoshop': $this->result = $this->processPsd(); break; + + case 'webp': + case 'image/webp': + case 'image/x-webp': + $this->result = $this->processWebp(); + break; default: throw new \Intervention\Image\Exception\NotSupportedException( diff --git a/src/Intervention/Image/Constraint.php b/src/Intervention/Image/Constraint.php index a247c99c..7352354d 100644 --- a/src/Intervention/Image/Constraint.php +++ b/src/Intervention/Image/Constraint.php @@ -50,6 +50,7 @@ class Constraint /** * Fix the given argument in current constraint + * * @param integer $type * @return void */ diff --git a/src/Intervention/Image/Gd/Decoder.php b/src/Intervention/Image/Gd/Decoder.php index 313e6261..25f91a53 100644 --- a/src/Intervention/Image/Gd/Decoder.php +++ b/src/Intervention/Image/Gd/Decoder.php @@ -14,37 +14,54 @@ class Decoder extends \Intervention\Image\AbstractDecoder */ public function initFromPath($path) { - $info = @getimagesize($path); - - if ($info === false) { + if ( ! file_exists($path)) { throw new \Intervention\Image\Exception\NotReadableException( - "Unable to read image from file ({$path})." + "Unable to find file ({$path})." ); } + // get mime type of file + $mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path); + // define core - switch ($info[2]) { - case IMAGETYPE_PNG: + switch (strtolower($mime)) { + case 'image/png': + case 'image/x-png': $core = @imagecreatefrompng($path); break; - case IMAGETYPE_JPEG: - $core = @imagecreatefromjpeg($path); + case 'image/jpg': + case 'image/jpeg': + case 'image/pjpeg': + $core = @imagecreatefromjpeg($path); + if (!$core) { + $core= @imagecreatefromstring(file_get_contents($path)); + } break; - case IMAGETYPE_GIF: + case 'image/gif': $core = @imagecreatefromgif($path); break; + case 'image/webp': + case 'image/x-webp': + if ( ! function_exists('imagecreatefromwebp')) { + throw new \Intervention\Image\Exception\NotReadableException( + "Unsupported image type. GD/PHP installation does not support WebP format." + ); + } + $core = @imagecreatefromwebp($path); + break; + default: throw new \Intervention\Image\Exception\NotReadableException( - "Unable to read image type. GD driver is only able to decode JPG, PNG or GIF files." + "Unsupported image type. GD driver is only able to decode JPG, PNG, GIF or WebP files." ); } if (empty($core)) { throw new \Intervention\Image\Exception\NotReadableException( - "Unable to read image from file ({$path})." + "Unable to decode image from file ({$path})." ); } @@ -52,7 +69,7 @@ class Decoder extends \Intervention\Image\AbstractDecoder // build image $image = $this->initFromGdResource($core); - $image->mime = $info['mime']; + $image->mime = $mime; $image->setFileInfoFromPath($path); return $image; diff --git a/src/Intervention/Image/Gd/Encoder.php b/src/Intervention/Image/Gd/Encoder.php index 97a2c271..a3fd2f12 100644 --- a/src/Intervention/Image/Gd/Encoder.php +++ b/src/Intervention/Image/Gd/Encoder.php @@ -55,6 +55,23 @@ class Encoder extends \Intervention\Image\AbstractEncoder return $buffer; } + protected function processWebp() + { + if ( ! function_exists('imagewebp')) { + throw new \Intervention\Image\Exception\NotSupportedException( + "Webp format is not supported by PHP installation." + ); + } + + ob_start(); + imagewebp($this->image->getCore(), null, $this->quality); + $this->image->mime = defined('IMAGETYPE_WEBP') ? image_type_to_mime_type(IMAGETYPE_WEBP) : 'image/webp'; + $buffer = ob_get_contents(); + ob_end_clean(); + + return $buffer; + } + /** * Processes and returns encoded image as TIFF string * diff --git a/src/Intervention/Image/ImageServiceProvider.php b/src/Intervention/Image/ImageServiceProvider.php index 234e06cf..e61e6a42 100644 --- a/src/Intervention/Image/ImageServiceProvider.php +++ b/src/Intervention/Image/ImageServiceProvider.php @@ -40,7 +40,9 @@ class ImageServiceProvider extends ServiceProvider */ public function boot() { - return $this->provider->boot(); + if (method_exists($this->provider, 'boot')) { + return $this->provider->boot(); + } } /** diff --git a/src/Intervention/Image/ImageServiceProviderLaravel5.php b/src/Intervention/Image/ImageServiceProviderLaravel5.php index 9d770b87..04cfc22b 100644 --- a/src/Intervention/Image/ImageServiceProviderLaravel5.php +++ b/src/Intervention/Image/ImageServiceProviderLaravel5.php @@ -77,7 +77,7 @@ class ImageServiceProviderLaravel5 extends ServiceProvider // imagecache route if (is_string(config('imagecache.route'))) { - $filename_pattern = '[ \w\\.\\/\\-\\@]+'; + $filename_pattern = '[ \w\\.\\/\\-\\@\(\)]+'; // route to access template applied image file $app['router']->get(config('imagecache.route').'/{template}/{filename}', array( diff --git a/src/Intervention/Image/Imagick/Encoder.php b/src/Intervention/Image/Imagick/Encoder.php index d744a445..b8cad8e1 100644 --- a/src/Intervention/Image/Imagick/Encoder.php +++ b/src/Intervention/Image/Imagick/Encoder.php @@ -70,6 +70,28 @@ class Encoder extends \Intervention\Image\AbstractEncoder return $imagick->getImagesBlob(); } + protected function processWebp() + { + if ( ! \Imagick::queryFormats('WEBP')) { + throw new \Intervention\Image\Exception\NotSupportedException( + "Webp format is not supported by Imagick installation." + ); + } + + $format = 'webp'; + $compression = \Imagick::COMPRESSION_JPEG; + + $imagick = $this->image->getCore(); + $imagick = $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_MERGE); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + $imagick->setImageCompressionQuality($this->quality); + + return $imagick->getImagesBlob(); + } + /** * Processes and returns encoded image as TIFF string * diff --git a/tests/AbstractDecoderTest.php b/tests/AbstractDecoderTest.php index 4d633ce2..7db29f33 100644 --- a/tests/AbstractDecoderTest.php +++ b/tests/AbstractDecoderTest.php @@ -61,6 +61,9 @@ class AbstractDecoderTest extends PHPUnit_Framework_TestCase $source = $this->getTestDecoder(fopen(__DIR__ . '/images/test.jpg', 'r')); $this->assertTrue($source->isStream()); + $source = $this->getTestDecoder(new \GuzzleHttp\Psr7\Stream(fopen(__DIR__ . '/images/test.jpg', 'r'))); + $this->assertTrue($source->isStream()); + $source = $this->getTestDecoder(null); $this->assertFalse($source->isStream()); } diff --git a/tests/EncoderTest.php b/tests/EncoderTest.php index 0b7eba8b..7a22f98c 100644 --- a/tests/EncoderTest.php +++ b/tests/EncoderTest.php @@ -46,6 +46,20 @@ class EncoderTest extends PHPUnit_Framework_TestCase $this->assertEquals('image/gif; charset=binary', $this->getMime($encoder->result)); } + public function testProcessWebpGd() + { + if (function_exists('imagewebp')) { + $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); + $encoder = new GdEncoder; + $image = Mockery::mock('\Intervention\Image\Image'); + $image->shouldReceive('getCore')->once()->andReturn($core); + $image->shouldReceive('setEncoded')->once()->andReturn($image); + $img = $encoder->process($image, 'webp', 90); + $this->assertInstanceOf('Intervention\Image\Image', $img); + $this->assertEquals('image/webp; charset=binary', $this->getMime($encoder->result)); + } + } + /** * @expectedException \Intervention\Image\Exception\NotSupportedException */ @@ -155,6 +169,16 @@ class EncoderTest extends PHPUnit_Framework_TestCase $this->assertEquals('mock-gif', $encoder->result); } + /** + * @expectedException \Intervention\Image\Exception\NotSupportedException + */ + public function testProcessWebpImagick() + { + $encoder = new ImagickEncoder; + $image = Mockery::mock('\Intervention\Image\Image'); + $img = $encoder->process($image, 'webp', 90); + } + public function testProcessTiffImagick() { $core = $this->getImagickMock('tiff'); diff --git a/tests/GdSystemTest.php b/tests/GdSystemTest.php index 3b107411..6c5c0605 100644 --- a/tests/GdSystemTest.php +++ b/tests/GdSystemTest.php @@ -28,6 +28,14 @@ class GdSystemTest extends PHPUnit_Framework_TestCase $this->manager()->make('tests/images/broken.png'); } + /** + * @expectedException \Intervention\Image\Exception\NotReadableException + */ + public function testMakeFromNotExisting() + { + $this->manager()->make('tests/images/not_existing.png'); + } + public function testMakeFromString() { $str = file_get_contents('tests/images/circle.png'); @@ -75,6 +83,22 @@ class GdSystemTest extends PHPUnit_Framework_TestCase $this->assertEquals(10, $img->getHeight()); } + public function testMakeFromWebp() + { + if (function_exists('imagecreatefromwebp')) { + $img = $this->manager()->make('tests/images/test.webp'); + $this->assertInstanceOf('Intervention\Image\Image', $img); + $this->assertInternalType('resource', $img->getCore()); + $this->assertEquals(16, $img->getWidth()); + $this->assertEquals(16, $img->getHeight()); + $this->assertEquals('image/webp', $img->mime); + $this->assertEquals('tests/images', $img->dirname); + $this->assertEquals('test.webp', $img->basename); + $this->assertEquals('webp', $img->extension); + $this->assertEquals('test', $img->filename); + } + } + public function testCanvas() { $img = $this->manager()->canvas(30, 20); @@ -1474,6 +1498,15 @@ class GdSystemTest extends PHPUnit_Framework_TestCase $this->assertInternalType('resource', imagecreatefromstring($img->encoded)); } + public function testEncodeWebp() + { + if (function_exists('imagewebp')) { + $img = $this->manager()->make('tests/images/trim.png'); + $data = (string) $img->encode('webp'); + $this->assertEquals('image/webp; charset=binary', $this->getMime($data)); + } + } + public function testEncodeDataUrl() { $img = $this->manager()->make('tests/images/trim.png'); @@ -1643,4 +1676,10 @@ class GdSystemTest extends PHPUnit_Framework_TestCase 'driver' => 'gd' )); } + + private function getMime($data) + { + $finfo = new finfo(FILEINFO_MIME); + return $finfo->buffer($data); + } } diff --git a/tests/images/test.webp b/tests/images/test.webp new file mode 100755 index 00000000..ecd28246 Binary files /dev/null and b/tests/images/test.webp differ