diff --git a/composer.json b/composer.json index 4dc8d9b..2c807aa 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "yiisoft/yii2-codeception": "self.version", "yiisoft/yii2-debug": "self.version", "yiisoft/yii2-elasticsearch": "self.version", + "yiisoft/yii2-imagine": "self.version", "yiisoft/yii2-gii": "self.version", "yiisoft/yii2-jui": "self.version", "yiisoft/yii2-mongodb": "self.version", @@ -76,10 +77,6 @@ "michelf/php-markdown": "1.3.*" }, "require-dev": { - "twbs/bootstrap": "3.0.*", - "smarty/smarty": "*", - "swiftmailer/swiftmailer": "*", - "twig/twig": "*", "phpunit/phpunit": "3.7.*" }, "suggest": { @@ -88,6 +85,7 @@ "ext-mongo": "required by yii2-mongo extension", "ext-pdo": "required by yii2-sphinx extension", "ext-pdo_mysql": "required by yii2-sphinx extension", + "imagine/imagine": "required by yii2-imagine extension", "smarty/smarty": "required by yii2-smarty extension", "swiftmailer/swiftmailer": "required by yii2-swiftmailer extension", "twig/twig": "required by yii2-twig extension" @@ -100,6 +98,7 @@ "yii\\debug\\": "extensions/", "yii\\elasticsearch\\": "extensions/", "yii\\gii\\": "extensions/", + "yii\\imagine\\" : "extensions/", "yii\\jui\\": "extensions/", "yii\\mongodb\\": "extensions/", "yii\\redis\\": "extensions/", diff --git a/extensions/yii/imagine/CHANGELOG.md b/extensions/yii/imagine/CHANGELOG.md new file mode 100644 index 0000000..dffb353 --- /dev/null +++ b/extensions/yii/imagine/CHANGELOG.md @@ -0,0 +1,12 @@ +Yii Framework 2 imagine extension Change Log +================================================ + +2.0.0 beta under development +---------------------------- + +- no changes in this release. + +2.0.0 alpha, December 1, 2013 +----------------------------- + +- Initial release. diff --git a/extensions/yii/imagine/Image.php b/extensions/yii/imagine/Image.php new file mode 100644 index 0000000..6703b59 --- /dev/null +++ b/extensions/yii/imagine/Image.php @@ -0,0 +1,327 @@ + [ + * ... + * 'image' => [ + * 'class' => 'yii\imagine\Image', + * 'driver' => \yii\imagine\Image::DRIVER_GD2, + * ], + * ... + * ], + * ~~~ + * + * But you can also use it directly, + * + * ~~~ + * use yii\imagine\Image; + * + * $img = new Image(); + * ~~~ + * + * Example of use: + * + * ~~~ + * // thumb - saved on runtime path + * $imagePath = Yii::$app->getBasePath() . '/web/img/test-image.jpg'; + * $runtimePath = Yii::$app->getRuntimePath(); + * Yii::$app->image + * ->thumb($imagePath, 120, 120) + * ->save($runtime . '/thumb-test-image.jpg', ['quality' => 50]); + * ~~~ + * + * + * @see http://imagine.readthedocs.org/ + * + * @author Antonio Ramirez + * @since 2.0 + */ +class Image extends Component +{ + /** + * GD2 driver definition for Imagine implementation using the GD library. + */ + const DRIVER_GD2 = 'gd2'; + /** + * imagick driver definition. + */ + const DRIVER_IMAGICK = 'imagick'; + /** + * gmagick driver definition. + */ + const DRIVER_GMAGICK = 'gmagick'; + /** + * @var \Imagine\Image\ImagineInterface instance. + */ + private $_imagine; + /** + * @var string the driver to use. These can be: + * - [[DRIVER_GD2]] + * - [[DRIVER_IMAGICK]] + * - [[DRIVER_GMAGICK]] + */ + private $_driver = self::DRIVER_GD2; + + /** + * Sets the driver. + * @param $driver + * @throws \yii\base\InvalidConfigException + */ + public function setDriver($driver) + { + if (!is_string($driver) || !in_array($driver, $this->getAvailableDrivers(), true)) { + throw new InvalidConfigException( + strtr('"{class}::driver" should be string of these possible options "{drivers}", "{driver}" given.', [ + '{class}' => get_class($this), + '{drivers}' => implode('", "', $this->getAvailableDrivers()), + '{driver}' => $driver + ])); + } + $this->_driver = $driver; + } + + /** + * Returns the driver which is going to be used for \Imagine\Image\ImagineInterface instance creation. + * @return string the driver used. + */ + public function getDriver() + { + return $this->_driver; + } + + /** + * @return array of available drivers. + */ + public function getAvailableDrivers() + { + static $drivers; + if ($drivers === null) { + $drivers = [static::DRIVER_GD2, static::DRIVER_GMAGICK, static::DRIVER_IMAGICK]; + } + return $drivers; + } + + /** + * @return \Imagine\Image\ImagineInterface instance + */ + public function getImagine() + { + if ($this->_imagine === null) { + switch ($this->_driver) { + case static::DRIVER_GD2: + $this->_imagine = new \Imagine\Gd\Imagine(); + break; + case static::DRIVER_IMAGICK: + $this->_imagine = new \Imagine\Imagick\Imagine(); + break; + case static::DRIVER_GMAGICK: + $this->_imagine = new \Imagine\Gmagick\Imagine(); + break; + } + } + return $this->_imagine; + } + + /** + * Crops an image + * @param string $filename the full path to the image file + * @param integer $width the crop width + * @param integer $height the crop height + * @param mixed $point. This argument can be both an array or an \Imagine\Image\Point type class, containing both + * `x` and `y` coordinates. For example: + * ~~~ + * // as array + * $obj->crop('path\to\image.jpg', 200, 200, [5, 5]); + * // as \Imagine\Image\Point + * $point = new \Imagine\Image\Point(5, 5); + * $obj->crop('path\to\image.jpg', 200, 200, $point); + * ~~~ + * If null, it will crop from 0,0 pixel position + * @return \Imagine\Image\ManipulatorInterface + * @throws \InvalidArgumentException + */ + public function crop($filename, $width, $height, $point = null) + { + if(is_array($point)) { + list($x, $y) = $point; + $point = new Point($x, $y); + } elseif ($point === null) { + $point = new Point(0, 0); + } elseif (!$point instanceof Point ) { + throw new \InvalidArgumentException( + strtr('"{class}::crop()" "$point" if not null, should be an "array" or a "{type}" class type, containing both "x" and "y" coordinates.', [ + '{class}' => get_class($this), + '{type}' => 'Imagine\\Image\\Point' + ])); + } + return $this->getImagine() + ->open($filename) + ->copy() + ->crop($point, new Box($width, $height)); + } + + /** + * Creates a thumbnail image. The function differs from [[\Imagine\Image\ImageInterface::thumbnail()]] function that + * it keeps the aspect ratio of the image. + * @param string $filename the full path to the image file + * @param integer $width the width to create the thumbnail + * @param integer $height the height in pixels to create the thumbnail + * @param string $mode + * @return \Imagine\Image\ImageInterface|ManipulatorInterface + */ + public function thumbnail($filename, $width, $height, $mode = ManipulatorInterface::THUMBNAIL_OUTBOUND) + { + $box = new Box($width, $height); + $img = $this->getImagine() + ->open($filename); + + if(($img->getSize()->getWidth() <= $box->getWidth() && $img->getSize()->getHeight() <= $box->getHeight()) + || (!$box->getWidth() && !$box->getHeight())) { + return $img->copy(); + } + $img = $img->thumbnail($box, $mode); + + // create empty image to preserve aspect ratio of thumbnail + $thumb = $this->getImagine() + ->create($box); + + // calculate points + $size = $img->getSize(); + + $startX = 0; + $startY = 0; + if ($size->getWidth() < $width) { + $startX = ceil($width - $size->getWidth()) / 2; + } + if ($size->getHeight() < $height) { + $startY = ceil($height - $size->getHeight()) / 2; + } + + $thumb->paste($img, new Point($startX, $startY)); + + return $thumb; + } + + /** + * Paste a watermark image onto another. + * Note: If any of `$x` or `$y` parameters are null, bottom right position will be default. + * @param string $filename the full path to the image file to apply the watermark to + * @param string $watermarkFilename the full path to the image file to apply as watermark + * @param mixed $point. This argument can be both an array or an \Imagine\Image\Point type class, containing both + * `x` and `y` coordinates. For example: + * ~~~ + * // as array + * $obj->watermark('path\to\image.jpg', 'path\to\watermark.jpg', [5, 5]); + * // as \Imagine\Image\Point + * $point = new \Imagine\Image\Point(5, 5); + * $obj->watermark('path\to\image.jpg', 'path\to\watermark.jpg', $point); + * ~~~ + * @return ManipulatorInterface + * @throws \InvalidArgumentException + */ + public function watermark($filename, $watermarkFilename, $point = null) + { + $img = $this->getImagine()->open($filename); + $watermark = $this->getImagine()->open($watermarkFilename); + + $size = $img->getSize(); + $wSize = $watermark->getSize(); + + // if x or y position was not given, set its bottom right by default + if(is_array($point)) { + list($x, $y) = $point; + $point = new Point($x, $y); + } elseif ($point === null) { + $x = $size->getWidth() - $wSize->getWidth(); + $y = $size->getHeight() - $wSize->getHeight(); + $point = new Point($x, $y); + } elseif (!$point instanceof Point) { + throw new \InvalidArgumentException( + strtr('"{class}::watermark()" "$point" if not null, should be an "array" or a "{type}" class type, containing both "x" and "y" coordinates.', [ + '{class}' => get_class($this), + '{type}' => 'Imagine\\Image\\Point' + ])); + } + + return $img->paste($watermark, $point); + } + + /** + * Draws text to an image. + * @param string $filename the full path to the image file + * @param string $text the text to write to the image + * @param array $fontConfig the font configuration. The font configuration holds the following keys: + * - font: The path to the font file to use to style the text. Required parameter. + * - size: The font size. Defaults to 12. + * - posX: The X position to write the text. Defaults to 5. + * - posY: The Y position to write the text. Defaults to 5. + * - angle: The angle to use to write the text. Defaults to 0. + * @return \Imagine\Image\ImageInterface + * @throws \Imagine\Exception\InvalidArgumentException + */ + public function text($filename, $text, array $fontConfig) + { + $img = $this->getImagine()->open($filename); + + $font = ArrayHelper::getValue($fontConfig, 'font'); + if ($font === null) { + throw new InvalidArgumentException('"' . get_class($this) . + '::text()" "$fontConfig" parameter should contain a "font" key with the path to the font file to use.'); + } + $fontSize = ArrayHelper::getValue($fontConfig, 'size', 12); + $fontColor = ArrayHelper::getValue($fontConfig, 'color', 'fff'); + $fontPosX = ArrayHelper::getValue($fontConfig, 'posX', 5); + $fontPosY = ArrayHelper::getValue($fontConfig, 'posY', 5); + $fontAngle = ArrayHelper::getValue($fontConfig, 'angle', 0); + + $font = $this->getImagine()->font($font, $fontSize, new Color($fontColor)); + $img->draw()->text($text, $font, new Point($fontPosX, $fontPosY), $fontAngle); + return $img; + } + + /** + * Adds a frame around of the image. Please note that the image will increase `$margin` x 2. + * @param string $filename the full path to the image file + * @param integer $margin the frame size to add around the image + * @param string $color the frame color + * @param integer $alpha + * @return \Imagine\Image\ImageInterface + */ + public function frame($filename, $margin, $color='000', $alpha = 100) + { + $img = $this->getImagine()->open($filename); + + $size = $img->getSize(); + + $pasteTo = new Point($margin, $margin); + $padColor = new Color($color, $alpha); + + $box = new Box($size->getWidth() + ceil($margin * 2), $size->getHeight() + ceil($margin * 2)); + + $image = $this->getImagine()->create( $box, $padColor); + $image->paste($img, $pasteTo); + + return $image; + } +} \ No newline at end of file diff --git a/extensions/yii/imagine/LICENSE.md b/extensions/yii/imagine/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/yii/imagine/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/yii/imagine/README.md b/extensions/yii/imagine/README.md new file mode 100644 index 0000000..324c0cd --- /dev/null +++ b/extensions/yii/imagine/README.md @@ -0,0 +1,70 @@ +Image Extension for Yii 2 +============================== + +This extension adds most common image functions and also acts as a wrapper to [Imagine](http://imagine.readthedocs.org/) +image manipulation library. + +Installation +------------ + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +php composer.phar require yiisoft/yii2-imagine "*" +``` + +or add + +```json +"yiisoft/yii2-imagine": "*" +``` + +to the `require` section of your composer.json. + + +Usage & Documentation +--------------------- + +This extension is a wrapper to the [Imagine](http://imagine.readthedocs.org/) and also adds the most common methods +used for Image manipulation. + +To use this extension, you can use it in to ways, whether you configure it on your application file or you use it +directly. + +The following shows how to use it via application configuration file: + +``` +// configuring on your application configuration file +'components' => [ + 'image' => [ + 'class' => 'yii\imagine\Image', + 'driver' => \yii\imagine\Image::DRIVER_GD2, + ] + ... +] + +// Once configured you can access to the extension like this: +$img = Yii::$app->image->thumb('path/to/image.jpg', 120, 120); + +``` + +This is how to use it directly: + +``` +use yii\imagine\Image; + +$image = new Image(); +$img = $image->thumb('path/to/image.jpg', 120, 120); +``` +**About the methods** +Each method returns an instance to `\Imagine\Image\ManipulatorInterface`, that means that you can easily make use of the methods included in the `Imagine` library: + +``` +// frame, rotate and save an image + +Yii::$app->image->frame('path/to/image.jpg', 5, '666', 0) + ->rotate(-8) + ->save('path/to/destination/image.jpg', ['quality' => 50]); +``` \ No newline at end of file diff --git a/extensions/yii/imagine/composer.json b/extensions/yii/imagine/composer.json new file mode 100644 index 0000000..7cfff10 --- /dev/null +++ b/extensions/yii/imagine/composer.json @@ -0,0 +1,30 @@ +{ + "name": "yiisoft/yii2-imagine", + "description": "The Imagine integration for the Yii framework", + "keywords": ["yii", "imagine", "image", "helper"], + "type": "yii2-extension", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Aimagine", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "Antonio Ramirez", + "email": "amigo.cobos@gmail.com" + } + ], + "require": { + "yiisoft/yii2": "*", + "imagine/imagine": "v0.5.0" + }, + "autoload": { + "psr-0": { + "yii\\imagine\\": "" + } + }, + "target-dir": "yii/imagine" +} diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index c1c4f2a..4da9acc 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -24,6 +24,7 @@ Yii Framework 2 Change Log - Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) - Bug: Fixed issue with tabular input on ActiveField::radio() and ActiveField::checkbox() (jom) - Bug: Fixed the issue that query cache returns the same data for the same SQL but different query methods (qiangxue) +- Enh #46: Added Image extension based on [Imagine library](http://imagine.readthedocs.org) (tonydspaniard) - Enh #364: Improve Inflector::slug with `intl` transliteration. Improved transliteration char map. (tonydspaniard) - Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue) - Enh #802: Added support for retrieving sub-array element or child object property through `ArrayHelper::getValue()` (qiangxue, cebe) diff --git a/tests/unit/data/imagine/GothamRnd-Light.otf b/tests/unit/data/imagine/GothamRnd-Light.otf new file mode 100644 index 0000000..4181a78 Binary files /dev/null and b/tests/unit/data/imagine/GothamRnd-Light.otf differ diff --git a/tests/unit/data/imagine/large.jpg b/tests/unit/data/imagine/large.jpg new file mode 100644 index 0000000..81c47e5 Binary files /dev/null and b/tests/unit/data/imagine/large.jpg differ diff --git a/tests/unit/data/imagine/xparent.gif b/tests/unit/data/imagine/xparent.gif new file mode 100644 index 0000000..2e6fd66 Binary files /dev/null and b/tests/unit/data/imagine/xparent.gif differ diff --git a/tests/unit/extensions/imagine/AbstractImageTest.php b/tests/unit/extensions/imagine/AbstractImageTest.php new file mode 100644 index 0000000..d757b0a --- /dev/null +++ b/tests/unit/extensions/imagine/AbstractImageTest.php @@ -0,0 +1,118 @@ +imageFile = Yii::getAlias('@yiiunit/data/imagine/large') . '.jpg'; + $this->watermarkFile = Yii::getAlias('@yiiunit/data/imagine/xparent') . '.gif'; + $this->runtimeTextFile = Yii::getAlias('@yiiunit/runtime/image-text-test') . '.png'; + $this->runtimeWatermarkFile = Yii::getAlias('@yiiunit/runtime/image-watermark-test') . '.png'; + parent::setUp(); + } + + protected function tearDown() + { + @unlink($this->runtimeTextFile); + @unlink($this->runtimeWatermarkFile); + } + + public function testText() { + if(!$this->isFontTestSupported()) { + $this->markTestSkipped('Skipping ImageGdTest Gd not installed'); + } + + $fontFile = Yii::getAlias('@yiiunit/data/imagine/GothamRnd-Light') . '.otf'; + + $img = $this->image->text($this->imageFile, 'Yii-2 Image', [ + 'font' => $fontFile, + 'size' => 12, + 'color' => '000' + ]); + + $img->save($this->runtimeTextFile); + $this->assertTrue(file_exists($this->runtimeTextFile)); + + } + + public function testCrop() + { + $point = [20,20]; + $img = $this->image->crop($this->imageFile, 100, 100, $point); + + $this->assertEquals(100, $img->getSize()->getWidth()); + $this->assertEquals(100, $img->getSize()->getHeight()); + + $point = new Point(20, 20); + $img = $this->image->crop($this->imageFile, 100, 100, $point); + $this->assertEquals(100, $img->getSize()->getWidth()); + $this->assertEquals(100, $img->getSize()->getHeight()); + + } + + public function testWatermark() + { + $img = $this->image->watermark($this->imageFile, $this->watermarkFile); + $img->save($this->runtimeWatermarkFile); + $this->assertTrue(file_exists($this->runtimeWatermarkFile)); + } + + public function testFrame() + { + $frameSize = 5; + $original = $this->image->getImagine()->open($this->imageFile); + $originalSize = $original->getSize(); + $img = $this->image->frame($this->imageFile, $frameSize, '666', 0); + $size = $img->getSize(); + + $this->assertEquals($size->getWidth(), $originalSize->getWidth() + ($frameSize * 2)); + } + + public function testThumbnail() + { + $img = $this->image->thumbnail($this->imageFile, 120, 120); + + $this->assertEquals(120, $img->getSize()->getWidth()); + $this->assertEquals(120, $img->getSize()->getHeight()); + } + + /** + * @expectedException \yii\base\InvalidConfigException + */ + public function testShouldThrowExceptionOnDriverInvalidArgument() { + $this->image->setDriver('fake-driver'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testShouldThrowExceptionOnCropInvalidArgument() { + $this->image->crop($this->imageFile, 100, 100, new \stdClass()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testShouldThrowExceptionOnWatermarkInvalidArgument() { + $this->image->watermark($this->imageFile, $this->watermarkFile, new \stdClass()); + } + + + abstract protected function isFontTestSupported(); +} diff --git a/tests/unit/extensions/imagine/ImageGdTest.php b/tests/unit/extensions/imagine/ImageGdTest.php new file mode 100644 index 0000000..1fa73fb --- /dev/null +++ b/tests/unit/extensions/imagine/ImageGdTest.php @@ -0,0 +1,31 @@ +markTestSkipped('Skipping ImageGdTest, Gd not installed'); + } else { + $this->image = new Image(); + $this->image->setDriver(Image::DRIVER_GD2); + parent::setUp(); + } + } + + protected function isFontTestSupported() + { + $infos = gd_info(); + return isset($infos['FreeType Support']) ? $infos['FreeType Support'] : false; + } + +} \ No newline at end of file diff --git a/tests/unit/extensions/imagine/ImageGmagickTest.php b/tests/unit/extensions/imagine/ImageGmagickTest.php new file mode 100644 index 0000000..3a6daac --- /dev/null +++ b/tests/unit/extensions/imagine/ImageGmagickTest.php @@ -0,0 +1,30 @@ +markTestSkipped('Skipping ImageGmagickTest, Gmagick is not installed'); + } else { + $this->image = new Image(); + $this->image->setDriver(Image::DRIVER_GMAGICK); + parent::setUp(); + } + } + + protected function isFontTestSupported() + { + return true; + } + +} \ No newline at end of file diff --git a/tests/unit/extensions/imagine/ImageImagickTest.php b/tests/unit/extensions/imagine/ImageImagickTest.php new file mode 100644 index 0000000..26b0545 --- /dev/null +++ b/tests/unit/extensions/imagine/ImageImagickTest.php @@ -0,0 +1,30 @@ +markTestSkipped('Skipping ImageImagickTest, Imagick is not installed'); + } else { + $this->image = new Image(); + $this->image->setDriver(Image::DRIVER_IMAGICK); + parent::setUp(); + } + } + + protected function isFontTestSupported() + { + return true; + } + +} \ No newline at end of file