diff --git a/.travis.yml b/.travis.yml
index 4321d9f3207f4..89b2503c991e0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,6 +11,12 @@ addons:
- language-pack-fr-base
- ldap-utils
- slapd
+ - libtiff-dev
+ - libjpeg-dev
+ - libdjvulibre-dev
+ - libwmf-dev
+ - pkg-config
+ - liblcms2-dev
env:
global:
@@ -27,15 +33,16 @@ matrix:
- php: 5.5
- php: 5.6
- php: 7.0
- env: deps=high
+ env: deps=high IMAGE_DRIVER=gmagick
- php: 7.1
- env: deps=low
+ env: deps=low IMAGE_DRIVER=imagick
fast_finish: true
cache:
directories:
- .phpunit
- php-$MIN_PHP
+ - cache
services:
- memcached
@@ -89,6 +96,8 @@ install:
- if [[ ! $skip ]]; then composer update; fi
- if [[ ! $skip ]]; then ./phpunit install; fi
- if [[ ! $skip && ! $PHP = hhvm* ]]; then php -i; else hhvm --php -r 'print_r($_SERVER);print_r(ini_get_all());'; fi
+ - if [[ $IMAGE_DRIVER = imagick ]]; then bash ./.travis/imagick.sh; fi
+ - if [[ $IMAGE_DRIVER = gmagick ]]; then bash ./.travis/gmagick.sh; fi
script:
- REPORT=' && echo -e "\\e[32mOK\\e[0m {}\\n\\n" || (echo -e "\\e[41mKO\\e[0m {}\\n\\n" && $(exit 1))'
diff --git a/.travis/gmagick.sh b/.travis/gmagick.sh
new file mode 100755
index 0000000000000..259564660892c
--- /dev/null
+++ b/.travis/gmagick.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -xe
+
+GRAPHICSMAGIC_VERSION="1.3.23"
+if [ $TRAVIS_PHP_VERSION = '7.0' ] || [ $TRAVIS_PHP_VERSION = '7.1' ]
+then
+ GMAGICK_VERSION="2.0.4RC1"
+else
+ GMAGICK_VERSION="1.1.7RC2"
+fi
+
+mkdir -p cache
+cd cache
+
+if [ ! -e ./GraphicsMagick-$GRAPHICSMAGIC_VERSION ]
+then
+ wget http://78.108.103.11/MIRROR/ftp/GraphicsMagick/1.3/GraphicsMagick-$GRAPHICSMAGIC_VERSION.tar.xz
+ tar -xf GraphicsMagick-$GRAPHICSMAGIC_VERSION.tar.xz
+ rm GraphicsMagick-$GRAPHICSMAGIC_VERSION.tar.xz
+ cd GraphicsMagick-$GRAPHICSMAGIC_VERSION
+ ./configure --prefix=$HOME/opt/gmagick --enable-shared --with-lcms2
+ make -j
+else
+ cd GraphicsMagick-$GRAPHICSMAGIC_VERSION
+fi
+
+make install
+cd ..
+
+if [ ! -e ./gmagick-$GMAGICK_VERSION ]
+then
+ wget https://pecl.php.net/get/gmagick-$GMAGICK_VERSION.tgz
+ tar -xzf gmagick-$GMAGICK_VERSION.tgz
+ rm gmagick-$GMAGICK_VERSION.tgz
+ cd gmagick-$GMAGICK_VERSION
+ phpize
+ ./configure --with-gmagick=$HOME/opt/gmagick
+ make -j
+else
+ cd gmagick-$GMAGICK_VERSION
+fi
+
+make install
+echo "extension=gmagick.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`
+php --ri gmagick
diff --git a/.travis/imagick.sh b/.travis/imagick.sh
new file mode 100755
index 0000000000000..41a15f873a2d3
--- /dev/null
+++ b/.travis/imagick.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+set -xe
+
+IMAGEMAGICK_VERSION="6.8.9-10"
+IMAGICK_VERSION="3.4.3"
+
+mkdir -p cache
+cd cache
+
+if [ ! -e ./ImageMagick-$IMAGEMAGICK_VERSION ]
+then
+ wget http://www.imagemagick.org/download/releases/ImageMagick-$IMAGEMAGICK_VERSION.tar.xz
+ tar -xf ImageMagick-$IMAGEMAGICK_VERSION.tar.xz
+ rm ImageMagick-$IMAGEMAGICK_VERSION.tar.xz
+ cd ImageMagick-$IMAGEMAGICK_VERSION
+ ./configure --prefix=$HOME/opt/imagemagick
+ make -j
+else
+ cd ImageMagick-$IMAGEMAGICK_VERSION
+fi
+
+make install
+export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$HOME/opt/imagemagick/lib/pkgconfig
+ln -s $HOME/opt/imagemagick/include/ImageMagick-6 $HOME/opt/imagemagick/include/ImageMagick
+cd ..
+
+if [ ! -e ./imagick-$IMAGICK_VERSION ]
+then
+ wget https://pecl.php.net/get/imagick-$IMAGICK_VERSION.tgz
+ tar -xzf imagick-$IMAGICK_VERSION.tgz
+ rm imagick-$IMAGICK_VERSION.tgz
+ cd imagick-$IMAGICK_VERSION
+ phpize
+ ./configure --with-imagick=$HOME/opt/imagemagick
+ make -j
+else
+ cd imagick-$IMAGICK_VERSION
+fi
+
+make install
+echo "extension=imagick.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`
+php --ri imagick
diff --git a/src/Symfony/Component/Image/.gitignore b/src/Symfony/Component/Image/.gitignore
new file mode 100644
index 0000000000000..7db2fa2cf7f5c
--- /dev/null
+++ b/src/Symfony/Component/Image/.gitignore
@@ -0,0 +1,5 @@
+composer.lock
+phpunit.xml
+vendor/
+Tests/Fixtures/results/in_out
+!Tests/Fixtures/results/in_out/.placeholder
diff --git a/src/Symfony/Component/Image/Draw/DrawerInterface.php b/src/Symfony/Component/Image/Draw/DrawerInterface.php
new file mode 100644
index 0000000000000..33d85a57db8af
--- /dev/null
+++ b/src/Symfony/Component/Image/Draw/DrawerInterface.php
@@ -0,0 +1,146 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Draw;
+
+use Symfony\Component\Image\Image\AbstractFont;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+interface DrawerInterface
+{
+ /**
+ * Draws an arc on a starting at a given x, y coordinates under a given
+ * start and end angles.
+ *
+ * @param PointInterface $center
+ * @param BoxInterface $size
+ * @param int $start
+ * @param int $end
+ * @param ColorInterface $color
+ * @param int $thickness
+ *
+ * @throws RuntimeException
+ *
+ * @return DrawerInterface
+ */
+ public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1);
+
+ /**
+ * Same as arc, but also connects end points with a straight line.
+ *
+ * @param PointInterface $center
+ * @param BoxInterface $size
+ * @param int $start
+ * @param int $end
+ * @param ColorInterface $color
+ * @param bool $fill
+ * @param int $thickness
+ *
+ * @throws RuntimeException
+ *
+ * @return DrawerInterface
+ */
+ public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1);
+
+ /**
+ * Draws and ellipse with center at the given x, y coordinates, and given
+ * width and height.
+ *
+ * @param PointInterface $center
+ * @param BoxInterface $size
+ * @param ColorInterface $color
+ * @param bool $fill
+ * @param int $thickness
+ *
+ * @throws RuntimeException
+ *
+ * @return DrawerInterface
+ */
+ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1);
+
+ /**
+ * Draws a line from start(x, y) to end(x, y) coordinates.
+ *
+ * @param PointInterface $start
+ * @param PointInterface $end
+ * @param ColorInterface $outline
+ * @param int $thickness
+ *
+ * @return DrawerInterface
+ */
+ public function line(PointInterface $start, PointInterface $end, ColorInterface $outline, $thickness = 1);
+
+ /**
+ * Same as arc, but connects end points and the center.
+ *
+ * @param PointInterface $center
+ * @param BoxInterface $size
+ * @param int $start
+ * @param int $end
+ * @param ColorInterface $color
+ * @param bool $fill
+ * @param int $thickness
+ *
+ * @throws RuntimeException
+ *
+ * @return DrawerInterface
+ */
+ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1);
+
+ /**
+ * Places a one pixel point at specific coordinates and fills it with
+ * specified color.
+ *
+ * @param PointInterface $position
+ * @param ColorInterface $color
+ *
+ * @throws RuntimeException
+ *
+ * @return DrawerInterface
+ */
+ public function dot(PointInterface $position, ColorInterface $color);
+
+ /**
+ * Draws a polygon using array of x, y coordinates. Must contain at least
+ * three coordinates.
+ *
+ * @param array $coordinates
+ * @param ColorInterface $color
+ * @param bool $fill
+ * @param int $thickness
+ *
+ * @throws RuntimeException
+ *
+ * @return DrawerInterface
+ */
+ public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1);
+
+ /**
+ * Annotates image with specified text at a given position starting on the
+ * top left of the final text box.
+ *
+ * The rotation is done CW
+ *
+ * @param string $string
+ * @param AbstractFont $font
+ * @param PointInterface $position
+ * @param int $angle
+ * @param int $width
+ *
+ * @throws RuntimeException
+ *
+ * @return DrawerInterface
+ */
+ public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null);
+}
diff --git a/src/Symfony/Component/Image/Effects/EffectsInterface.php b/src/Symfony/Component/Image/Effects/EffectsInterface.php
new file mode 100644
index 0000000000000..c327f010b601c
--- /dev/null
+++ b/src/Symfony/Component/Image/Effects/EffectsInterface.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Effects;
+
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+interface EffectsInterface
+{
+ /**
+ * Apply gamma correction.
+ *
+ * @param float $correction
+ *
+ * @return EffectsInterface
+ *
+ * @throws RuntimeException
+ */
+ public function gamma($correction);
+
+ /**
+ * Invert the colors of the image.
+ *
+ * @return EffectsInterface
+ *
+ * @throws RuntimeException
+ */
+ public function negative();
+
+ /**
+ * Grayscale the image.
+ *
+ * @return EffectsInterface
+ *
+ * @throws RuntimeException
+ */
+ public function grayscale();
+
+ /**
+ * Colorize the image.
+ *
+ * @param ColorInterface $color
+ *
+ * @return EffectsInterface
+ *
+ * @throws RuntimeException
+ */
+ public function colorize(ColorInterface $color);
+
+ /**
+ * Sharpens the image.
+ *
+ * @return EffectsInterface
+ *
+ * @throws RuntimeException
+ */
+ public function sharpen();
+
+ /**
+ * Blur the image.
+ *
+ * @param float|int $sigma
+ *
+ * @return EffectsInterface
+ *
+ * @throws RuntimeException
+ */
+ public function blur($sigma);
+}
diff --git a/src/Symfony/Component/Image/Exception/ExceptionInterface.php b/src/Symfony/Component/Image/Exception/ExceptionInterface.php
new file mode 100644
index 0000000000000..0f26b9acce285
--- /dev/null
+++ b/src/Symfony/Component/Image/Exception/ExceptionInterface.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Exception;
+
+interface ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Image/Exception/InvalidArgumentException.php b/src/Symfony/Component/Image/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000000000..75e5da1e81f46
--- /dev/null
+++ b/src/Symfony/Component/Image/Exception/InvalidArgumentException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Exception;
+
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Image/Exception/NotSupportedException.php b/src/Symfony/Component/Image/Exception/NotSupportedException.php
new file mode 100644
index 0000000000000..bd3e8a90506f4
--- /dev/null
+++ b/src/Symfony/Component/Image/Exception/NotSupportedException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Exception;
+
+/**
+ * Should be used when a driver does not support an operation.
+ */
+class NotSupportedException extends RuntimeException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Image/Exception/OutOfBoundsException.php b/src/Symfony/Component/Image/Exception/OutOfBoundsException.php
new file mode 100644
index 0000000000000..13d6dc497d896
--- /dev/null
+++ b/src/Symfony/Component/Image/Exception/OutOfBoundsException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Exception;
+
+class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Image/Exception/RuntimeException.php b/src/Symfony/Component/Image/Exception/RuntimeException.php
new file mode 100644
index 0000000000000..51a63c07155a0
--- /dev/null
+++ b/src/Symfony/Component/Image/Exception/RuntimeException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Exception;
+
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Image/Filter/Advanced/Border.php b/src/Symfony/Component/Image/Filter/Advanced/Border.php
new file mode 100644
index 0000000000000..60ed1facf6989
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Advanced/Border.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Advanced;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Point;
+
+/**
+ * A border filter.
+ */
+class Border implements FilterInterface
+{
+ /**
+ * @var ColorInterface
+ */
+ private $color;
+
+ /**
+ * @var int
+ */
+ private $width;
+
+ /**
+ * @var int
+ */
+ private $height;
+
+ /**
+ * Constructs Border filter with given color, width and height.
+ *
+ * @param ColorInterface $color
+ * @param int $width Width of the border on the left and right sides of the image
+ * @param int $height Height of the border on the top and bottom sides of the image
+ */
+ public function __construct(ColorInterface $color, $width = 1, $height = 1)
+ {
+ $this->color = $color;
+ $this->width = $width;
+ $this->height = $height;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ $size = $image->getSize();
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $draw = $image->draw();
+
+ // Draw top and bottom lines
+ $draw
+ ->line(
+ new Point(0, 0),
+ new Point($width - 1, 0),
+ $this->color,
+ $this->height
+ )
+ ->line(
+ new Point($width - 1, $height - 1),
+ new Point(0, $height - 1),
+ $this->color,
+ $this->height
+ )
+ ;
+
+ // Draw sides
+ $draw
+ ->line(
+ new Point(0, 0),
+ new Point(0, $height - 1),
+ $this->color,
+ $this->width
+ )
+ ->line(
+ new Point($width - 1, 0),
+ new Point($width - 1, $height - 1),
+ $this->color,
+ $this->width
+ )
+ ;
+
+ return $image;
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Advanced/Canvas.php b/src/Symfony/Component/Image/Filter/Advanced/Canvas.php
new file mode 100644
index 0000000000000..7f06bcfdb10e4
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Advanced/Canvas.php
@@ -0,0 +1,74 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Advanced;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\LoaderInterface;
+
+/**
+ * A canvas filter.
+ */
+class Canvas implements FilterInterface
+{
+ /**
+ * @var BoxInterface
+ */
+ private $size;
+
+ /**
+ * @var PointInterface
+ */
+ private $placement;
+
+ /**
+ * @var ColorInterface
+ */
+ private $background;
+
+ /**
+ * @var LoaderInterface
+ */
+ private $imagine;
+
+ /**
+ * Constructs Canvas filter with given width and height and the placement of the current image
+ * inside the new canvas.
+ *
+ * @param LoaderInterface $imagine
+ * @param BoxInterface $size
+ * @param PointInterface $placement
+ * @param ColorInterface $background
+ */
+ public function __construct(LoaderInterface $imagine, BoxInterface $size, PointInterface $placement = null, ColorInterface $background = null)
+ {
+ $this->imagine = $imagine;
+ $this->size = $size;
+ $this->placement = $placement ?: new Point(0, 0);
+ $this->background = $background;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ $canvas = $this->imagine->create($this->size, $this->background);
+ $canvas->paste($image, $this->placement);
+
+ return $canvas;
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Advanced/Grayscale.php b/src/Symfony/Component/Image/Filter/Advanced/Grayscale.php
new file mode 100644
index 0000000000000..481e1292a92e2
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Advanced/Grayscale.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Advanced;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Point;
+
+/**
+ * The Grayscale filter calculates the gray-value based on RGB.
+ */
+class Grayscale extends OnPixelBased implements FilterInterface
+{
+ public function __construct()
+ {
+ parent::__construct(function (ImageInterface $image, Point $point) {
+ $color = $image->getColorAt($point);
+ $image->draw()->dot($point, $color->grayscale());
+ });
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Advanced/OnPixelBased.php b/src/Symfony/Component/Image/Filter/Advanced/OnPixelBased.php
new file mode 100644
index 0000000000000..5ff6c64d81483
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Advanced/OnPixelBased.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Advanced;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Point;
+
+/**
+ * The OnPixelBased takes a callable, and for each pixel, this callable is called with the
+ * image (Symfony\Component\Image\Image\ImageInterface) and the current point (Symfony\Component\Image\Image\Point).
+ */
+class OnPixelBased implements FilterInterface
+{
+ protected $callback;
+
+ public function __construct($callback)
+ {
+ if (!is_callable($callback)) {
+ throw new InvalidArgumentException('$callback has to be callable');
+ }
+
+ $this->callback = $callback;
+ }
+
+ /**
+ * Applies scheduled transformation to ImageInterface instance
+ * Returns processed ImageInterface instance.
+ *
+ * @param ImageInterface $image
+ *
+ * @return ImageInterface
+ */
+ public function apply(ImageInterface $image)
+ {
+ $w = $image->getSize()->getWidth();
+ $h = $image->getSize()->getHeight();
+
+ for ($x = 0; $x < $w; ++$x) {
+ for ($y = 0; $y < $h; ++$y) {
+ call_user_func($this->callback, $image, new Point($x, $y));
+ }
+ }
+
+ return $image;
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Advanced/RelativeResize.php b/src/Symfony/Component/Image/Filter/Advanced/RelativeResize.php
new file mode 100644
index 0000000000000..c6c5b8f4d08ec
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Advanced/RelativeResize.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Advanced;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+
+/**
+ * The RelativeResize filter allows images to be resized relative to their
+ * existing dimensions.
+ */
+class RelativeResize implements FilterInterface
+{
+ private $method;
+ private $parameter;
+
+ /**
+ * Constructs a RelativeResize filter with the given method and argument.
+ *
+ * @param string $method BoxInterface method
+ * @param mixed $parameter Parameter for BoxInterface method
+ */
+ public function __construct($method, $parameter)
+ {
+ if (!in_array($method, array('heighten', 'increase', 'scale', 'widen'))) {
+ throw new InvalidArgumentException(sprintf('Unsupported method: %s', $method));
+ }
+
+ $this->method = $method;
+ $this->parameter = $parameter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->resize(call_user_func(array($image->getSize(), $this->method), $this->parameter));
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/ApplyMask.php b/src/Symfony/Component/Image/Filter/Basic/ApplyMask.php
new file mode 100644
index 0000000000000..a28a9cea17f2b
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/ApplyMask.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+
+/**
+ * An apply mask filter.
+ */
+class ApplyMask implements FilterInterface
+{
+ /**
+ * @var ImageInterface
+ */
+ private $mask;
+
+ /**
+ * @param ImageInterface $mask
+ */
+ public function __construct(ImageInterface $mask)
+ {
+ $this->mask = $mask;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->applyMask($this->mask);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Autorotate.php b/src/Symfony/Component/Image/Filter/Basic/Autorotate.php
new file mode 100644
index 0000000000000..12181c43f86bc
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Autorotate.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+/**
+ * Rotates an image automatically based on exif information.
+ *
+ * Your attention please: This filter requires the use of the
+ * ExifMetadataReader to work.
+ *
+ * @see https://imagine.readthedocs.org/en/latest/usage/metadata.html
+ */
+class Autorotate implements FilterInterface
+{
+ private $color;
+
+ /**
+ * @param string|array|ColorInterface $color A color
+ */
+ public function __construct($color = '000000')
+ {
+ $this->color = $color;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ $metadata = $image->metadata();
+
+ switch (isset($metadata['ifd0.Orientation']) ? $metadata['ifd0.Orientation'] : null) {
+ case 1: // top-left
+ break;
+ case 2: // top-right
+ $image->flipHorizontally();
+ break;
+ case 3: // bottom-right
+ $image->rotate(180, $this->getColor($image));
+ break;
+ case 4: // bottom-left
+ $image->flipHorizontally();
+ $image->rotate(180, $this->getColor($image));
+ break;
+ case 5: // left-top
+ $image->flipHorizontally();
+ $image->rotate(-90, $this->getColor($image));
+ break;
+ case 6: // right-top
+ $image->rotate(90, $this->getColor($image));
+ break;
+ case 7: // right-bottom
+ $image->flipHorizontally();
+ $image->rotate(90, $this->getColor($image));
+ break;
+ case 8: // left-bottom
+ $image->rotate(-90, $this->getColor($image));
+ break;
+ default: // Invalid orientation
+ break;
+ }
+
+ return $image;
+ }
+
+ private function getColor(ImageInterface $image)
+ {
+ if ($this->color instanceof ColorInterface) {
+ return $this->color;
+ }
+
+ return $image->palette()->color($this->color);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Copy.php b/src/Symfony/Component/Image/Filter/Basic/Copy.php
new file mode 100644
index 0000000000000..3c130c78dbd96
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Copy.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+
+/**
+ * A copy filter.
+ */
+class Copy implements FilterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->copy();
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Crop.php b/src/Symfony/Component/Image/Filter/Basic/Crop.php
new file mode 100644
index 0000000000000..5b5a04ee171f3
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Crop.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A crop filter.
+ */
+class Crop implements FilterInterface
+{
+ /**
+ * @var PointInterface
+ */
+ private $start;
+
+ /**
+ * @var BoxInterface
+ */
+ private $size;
+
+ /**
+ * Constructs a Crop filter with given x, y, coordinates and crop width and
+ * height values.
+ *
+ * @param PointInterface $start
+ * @param BoxInterface $size
+ */
+ public function __construct(PointInterface $start, BoxInterface $size)
+ {
+ $this->start = $start;
+ $this->size = $size;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->crop($this->start, $this->size);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Fill.php b/src/Symfony/Component/Image/Filter/Basic/Fill.php
new file mode 100644
index 0000000000000..cc4bbefaa7486
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Fill.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\Fill\FillInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+
+/**
+ * A fill filter.
+ */
+class Fill implements FilterInterface
+{
+ /**
+ * @var FillInterface
+ */
+ private $fill;
+
+ /**
+ * @param FillInterface $fill
+ */
+ public function __construct(FillInterface $fill)
+ {
+ $this->fill = $fill;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->fill($this->fill);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/FlipHorizontally.php b/src/Symfony/Component/Image/Filter/Basic/FlipHorizontally.php
new file mode 100644
index 0000000000000..fe10dfbfd3c90
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/FlipHorizontally.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A "flip horizontally" filter.
+ */
+class FlipHorizontally implements FilterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->flipHorizontally();
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/FlipVertically.php b/src/Symfony/Component/Image/Filter/Basic/FlipVertically.php
new file mode 100644
index 0000000000000..079727b645d45
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/FlipVertically.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A "flip vertically" filter.
+ */
+class FlipVertically implements FilterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->flipVertically();
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Paste.php b/src/Symfony/Component/Image/Filter/Basic/Paste.php
new file mode 100644
index 0000000000000..dbef92eccff67
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Paste.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A paste filter.
+ */
+class Paste implements FilterInterface
+{
+ /**
+ * @var ImageInterface
+ */
+ private $image;
+
+ /**
+ * @var PointInterface
+ */
+ private $start;
+
+ /**
+ * Constructs a Paste filter with given ImageInterface to paste and x, y
+ * coordinates of target position.
+ *
+ * @param ImageInterface $image
+ * @param PointInterface $start
+ */
+ public function __construct(ImageInterface $image, PointInterface $start)
+ {
+ $this->image = $image;
+ $this->start = $start;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->paste($this->image, $this->start);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Resize.php b/src/Symfony/Component/Image/Filter/Basic/Resize.php
new file mode 100644
index 0000000000000..86e9c38f83c41
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Resize.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\BoxInterface;
+
+/**
+ * A resize filter.
+ */
+class Resize implements FilterInterface
+{
+ /**
+ * @var BoxInterface
+ */
+ private $size;
+ private $filter;
+
+ /**
+ * Constructs Resize filter with given width and height.
+ *
+ * @param BoxInterface $size
+ * @param string $filter
+ */
+ public function __construct(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ $this->size = $size;
+ $this->filter = $filter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->resize($this->size, $this->filter);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Rotate.php b/src/Symfony/Component/Image/Filter/Basic/Rotate.php
new file mode 100644
index 0000000000000..82309f322e4e6
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Rotate.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A rotate filter.
+ */
+class Rotate implements FilterInterface
+{
+ /**
+ * @var int
+ */
+ private $angle;
+
+ /**
+ * @var ColorInterface
+ */
+ private $background;
+
+ /**
+ * Constructs Rotate filter with given angle and background color.
+ *
+ * @param int $angle
+ * @param ColorInterface $background
+ */
+ public function __construct($angle, ColorInterface $background = null)
+ {
+ $this->angle = $angle;
+ $this->background = $background;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->rotate($this->angle, $this->background);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Save.php b/src/Symfony/Component/Image/Filter/Basic/Save.php
new file mode 100644
index 0000000000000..4116b05b2b7bf
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Save.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A save filter.
+ */
+class Save implements FilterInterface
+{
+ /**
+ * @var string
+ */
+ private $path;
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Constructs Save filter with given path and options.
+ *
+ * @param string $path
+ * @param array $options
+ */
+ public function __construct($path = null, array $options = array())
+ {
+ $this->path = $path;
+ $this->options = $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->save($this->path, $this->options);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Show.php b/src/Symfony/Component/Image/Filter/Basic/Show.php
new file mode 100644
index 0000000000000..752d4dc683662
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Show.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A show filter.
+ */
+class Show implements FilterInterface
+{
+ /**
+ * @var string
+ */
+ private $format;
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Constructs the Show filter with given format and options.
+ *
+ * @param string $format
+ * @param array $options
+ */
+ public function __construct($format, array $options = array())
+ {
+ $this->format = $format;
+ $this->options = $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->show($this->format, $this->options);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Strip.php b/src/Symfony/Component/Image/Filter/Basic/Strip.php
new file mode 100644
index 0000000000000..0b87c3ca4ebd0
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Strip.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A strip filter.
+ */
+class Strip implements FilterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->strip();
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/Thumbnail.php b/src/Symfony/Component/Image/Filter/Basic/Thumbnail.php
new file mode 100644
index 0000000000000..ad949f4dab517
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/Thumbnail.php
@@ -0,0 +1,59 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A thumbnail filter.
+ */
+class Thumbnail implements FilterInterface
+{
+ /**
+ * @var BoxInterface
+ */
+ private $size;
+
+ /**
+ * @var string
+ */
+ private $mode;
+
+ /**
+ * @var string
+ */
+ private $filter;
+
+ /**
+ * Constructs the Thumbnail filter with given width, height and mode.
+ *
+ * @param BoxInterface $size
+ * @param string $mode
+ * @param string $filter
+ */
+ public function __construct(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_INSET, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ $this->size = $size;
+ $this->mode = $mode;
+ $this->filter = $filter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->thumbnail($this->size, $this->mode, $this->filter);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php b/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php
new file mode 100644
index 0000000000000..06fb5102d423d
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter\Basic;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Filter\FilterInterface;
+
+/**
+ * A filter to render web-optimized images
+ */
+class WebOptimization implements FilterInterface
+{
+ private $palette;
+ private $path;
+ private $options;
+
+ public function __construct($path = null, array $options = array())
+ {
+ $this->path = $path;
+ $this->options = array_replace(array(
+ 'resolution-units' => ImageInterface::RESOLUTION_PIXELSPERINCH,
+ 'resolution-y' => 72,
+ 'resolution-x' => 72,
+ ), $options);
+ $this->palette = new RGB();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ $image
+ ->usePalette($this->palette)
+ ->strip();
+
+ if (is_callable($this->path)) {
+ $path = call_user_func($this->path, $image);
+ } elseif (null !== $this->path) {
+ $path = $this->path;
+ } else {
+ return $image;
+ }
+
+ return $image->save($path, $this->options);
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/FilterInterface.php b/src/Symfony/Component/Image/Filter/FilterInterface.php
new file mode 100644
index 0000000000000..7f9f90d37f046
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/FilterInterface.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter;
+
+use Symfony\Component\Image\Image\ImageInterface;
+
+/**
+ * Interface for imagine filters
+ */
+interface FilterInterface
+{
+ /**
+ * Applies scheduled transformation to ImageInterface instance
+ * Returns processed ImageInterface instance
+ *
+ * @param ImageInterface $image
+ *
+ * @return ImageInterface
+ */
+ public function apply(ImageInterface $image);
+}
diff --git a/src/Symfony/Component/Image/Filter/LoaderAware.php b/src/Symfony/Component/Image/Filter/LoaderAware.php
new file mode 100644
index 0000000000000..1041222eb8e03
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/LoaderAware.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Image\LoaderInterface;
+
+/**
+ * LoaderAware base class
+ */
+abstract class LoaderAware implements FilterInterface
+{
+ /**
+ * An LoaderInterface instance.
+ *
+ * @var LoaderInterface
+ */
+ private $imagine;
+
+ /**
+ * Set LoaderInterface instance.
+ *
+ * @param LoaderInterface $imagine An LoaderInterface instance
+ */
+ public function setLoader(LoaderInterface $imagine)
+ {
+ $this->imagine = $imagine;
+ }
+
+ /**
+ * Get LoaderInterface instance.
+ *
+ * @return LoaderInterface
+ *
+ * @throws InvalidArgumentException
+ */
+ public function getLoader()
+ {
+ if (!$this->imagine instanceof LoaderInterface) {
+ throw new InvalidArgumentException(sprintf('In order to use %s pass an Symfony\Component\Image\Image\LoaderInterface instance to filter constructor', get_class($this)));
+ }
+
+ return $this->imagine;
+ }
+}
diff --git a/src/Symfony/Component/Image/Filter/Transformation.php b/src/Symfony/Component/Image/Filter/Transformation.php
new file mode 100644
index 0000000000000..2e3a1a663958d
--- /dev/null
+++ b/src/Symfony/Component/Image/Filter/Transformation.php
@@ -0,0 +1,240 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Filter;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Filter\Basic\ApplyMask;
+use Symfony\Component\Image\Filter\Basic\Copy;
+use Symfony\Component\Image\Filter\Basic\Crop;
+use Symfony\Component\Image\Filter\Basic\Fill;
+use Symfony\Component\Image\Filter\Basic\FlipVertically;
+use Symfony\Component\Image\Filter\Basic\FlipHorizontally;
+use Symfony\Component\Image\Filter\Basic\Paste;
+use Symfony\Component\Image\Filter\Basic\Resize;
+use Symfony\Component\Image\Filter\Basic\Rotate;
+use Symfony\Component\Image\Filter\Basic\Save;
+use Symfony\Component\Image\Filter\Basic\Show;
+use Symfony\Component\Image\Filter\Basic\Strip;
+use Symfony\Component\Image\Filter\Basic\Thumbnail;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\LoaderInterface;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Fill\FillInterface;
+use Symfony\Component\Image\Image\ManipulatorInterface;
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * A transformation filter
+ */
+final class Transformation implements FilterInterface, ManipulatorInterface
+{
+ /**
+ * @var array
+ */
+ private $filters = array();
+
+ /**
+ * @var array
+ */
+ private $sorted;
+
+ /**
+ * An LoaderInterface instance.
+ *
+ * @var LoaderInterface
+ */
+ private $imagine;
+
+ /**
+ * Class constructor.
+ *
+ * @param LoaderInterface $imagine An LoaderInterface instance
+ */
+ public function __construct(LoaderInterface $imagine = null)
+ {
+ $this->imagine = $imagine;
+ }
+
+ /**
+ * Applies a given FilterInterface onto given ImageInterface and returns
+ * modified ImageInterface
+ *
+ * @param ImageInterface $image
+ * @param FilterInterface $filter
+ *
+ * @return ImageInterface
+ * @throws InvalidArgumentException
+ */
+ public function applyFilter(ImageInterface $image, FilterInterface $filter)
+ {
+ if ($filter instanceof LoaderAware) {
+ if ($this->imagine === null) {
+ throw new InvalidArgumentException(sprintf('In order to use %s pass an Symfony\Component\Image\Image\LoaderInterface instance to Transformation constructor', get_class($filter)));
+ }
+ $filter->setLoader($this->imagine);
+ }
+
+ return $filter->apply($image);
+ }
+
+ /**
+ * Returns a list of filters sorted by their priority. Filters with same priority will be returned in the order they were added.
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ if (null === $this->sorted) {
+ if (!empty($this->filters)) {
+ ksort($this->filters);
+ $this->sorted = call_user_func_array('array_merge', $this->filters);
+ } else {
+ $this->sorted = array();
+ }
+ }
+
+ return $this->sorted;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return array_reduce(
+ $this->getFilters(),
+ array($this, 'applyFilter'),
+ $image
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function copy()
+ {
+ return $this->add(new Copy());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function crop(PointInterface $start, BoxInterface $size)
+ {
+ return $this->add(new Crop($start, $size));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function flipHorizontally()
+ {
+ return $this->add(new FlipHorizontally());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function flipVertically()
+ {
+ return $this->add(new FlipVertically());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function strip()
+ {
+ return $this->add(new Strip());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function paste(ImageInterface $image, PointInterface $start)
+ {
+ return $this->add(new Paste($image, $start));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyMask(ImageInterface $mask)
+ {
+ return $this->add(new ApplyMask($mask));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fill(FillInterface $fill)
+ {
+ return $this->add(new Fill($fill));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ return $this->add(new Resize($size, $filter));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rotate($angle, ColorInterface $background = null)
+ {
+ return $this->add(new Rotate($angle, $background));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save($path = null, array $options = array())
+ {
+ return $this->add(new Save($path, $options));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function show($format, array $options = array())
+ {
+ return $this->add(new Show($format, $options));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_INSET, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ return $this->add(new Thumbnail($size, $mode, $filter));
+ }
+
+ /**
+ * Registers a given FilterInterface in an internal array of filters for
+ * later application to an instance of ImageInterface
+ *
+ * @param FilterInterface $filter
+ * @param int $priority
+ * @return Transformation
+ */
+ public function add(FilterInterface $filter, $priority = 0)
+ {
+ $this->filters[$priority][] = $filter;
+ $this->sorted = null;
+
+ return $this;
+ }
+}
diff --git a/src/Symfony/Component/Image/Gd/Drawer.php b/src/Symfony/Component/Image/Gd/Drawer.php
new file mode 100644
index 0000000000000..b543d4c8e0aba
--- /dev/null
+++ b/src/Symfony/Component/Image/Gd/Drawer.php
@@ -0,0 +1,333 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gd;
+
+use Symfony\Component\Image\Draw\DrawerInterface;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\AbstractFont;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor;
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Drawer implementation using the GD library
+ */
+final class Drawer implements DrawerInterface
+{
+ /**
+ * @var resource
+ */
+ private $resource;
+
+ /**
+ * @var array
+ */
+ private $info;
+
+ /**
+ * Constructs Drawer with a given gd image resource
+ *
+ * @param resource $resource
+ */
+ public function __construct($resource)
+ {
+ $this->loadGdInfo();
+ $this->resource = $resource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1)
+ {
+ imagesetthickness($this->resource, max(1, (int) $thickness));
+
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Draw arc operation failed');
+ }
+
+ if (false === imagearc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color))) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Draw arc operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Draw arc operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * This function does not work properly because of a bug in GD
+ *
+ * {@inheritdoc}
+ */
+ public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ imagesetthickness($this->resource, max(1, (int) $thickness));
+
+ if ($fill) {
+ $style = IMG_ARC_CHORD;
+ } else {
+ $style = IMG_ARC_CHORD | IMG_ARC_NOFILL;
+ }
+
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Draw chord operation failed');
+ }
+
+ if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style)) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Draw chord operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Draw chord operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ imagesetthickness($this->resource, max(1, (int) $thickness));
+
+ if ($fill) {
+ $callback = 'imagefilledellipse';
+ } else {
+ $callback = 'imageellipse';
+ }
+
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Draw ellipse operation failed');
+ }
+
+ if (false === $callback($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $this->getColor($color))) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Draw ellipse operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Draw ellipse operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1)
+ {
+ imagesetthickness($this->resource, max(1, (int) $thickness));
+
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Draw line operation failed');
+ }
+
+ if (false === imageline($this->resource, $start->getX(), $start->getY(), $end->getX(), $end->getY(), $this->getColor($color))) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Draw line operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Draw line operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ imagesetthickness($this->resource, max(1, (int) $thickness));
+
+ if ($fill) {
+ $style = IMG_ARC_EDGED;
+ } else {
+ $style = IMG_ARC_EDGED | IMG_ARC_NOFILL;
+ }
+
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Draw chord operation failed');
+ }
+
+ if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style)) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Draw chord operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Draw chord operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dot(PointInterface $position, ColorInterface $color)
+ {
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Draw point operation failed');
+ }
+
+ if (false === imagesetpixel($this->resource, $position->getX(), $position->getY(), $this->getColor($color))) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Draw point operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Draw point operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ imagesetthickness($this->resource, max(1, (int) $thickness));
+
+ if (count($coordinates) < 3) {
+ throw new InvalidArgumentException(sprintf('A polygon must consist of at least 3 points, %d given', count($coordinates)));
+ }
+
+ $points = call_user_func_array('array_merge', array_map(function (PointInterface $p) {
+ return array($p->getX(), $p->getY());
+ }, $coordinates));
+
+ if ($fill) {
+ $callback = 'imagefilledpolygon';
+ } else {
+ $callback = 'imagepolygon';
+ }
+
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Draw polygon operation failed');
+ }
+
+ if (false === $callback($this->resource, $points, count($coordinates), $this->getColor($color))) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Draw polygon operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Draw polygon operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null)
+ {
+ if (!$this->info['FreeType Support']) {
+ throw new RuntimeException('GD is not compiled with FreeType support');
+ }
+
+ $angle = -1 * $angle;
+ $fontsize = $font->getSize();
+ $fontfile = $font->getFile();
+ $x = $position->getX();
+ $y = $position->getY() + $fontsize;
+
+ if ($width !== null) {
+ $string = $this->wrapText($string, $font, $angle, $width);
+ }
+
+ if (false === imagealphablending($this->resource, true)) {
+ throw new RuntimeException('Font mask operation failed');
+ }
+
+ if (false === imagefttext($this->resource, $fontsize, $angle, $x, $y, $this->getColor($font->getColor()), $fontfile, $string)) {
+ imagealphablending($this->resource, false);
+ throw new RuntimeException('Font mask operation failed');
+ }
+
+ if (false === imagealphablending($this->resource, false)) {
+ throw new RuntimeException('Font mask operation failed');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Internal
+ *
+ * Generates a GD color from Color instance
+ *
+ * @param ColorInterface $color
+ *
+ * @return resource
+ *
+ * @throws RuntimeException
+ * @throws InvalidArgumentException
+ */
+ private function getColor(ColorInterface $color)
+ {
+ if (!$color instanceof RGBColor) {
+ throw new InvalidArgumentException('GD driver only supports RGB colors');
+ }
+
+ $gdColor = imagecolorallocatealpha($this->resource, $color->getRed(), $color->getGreen(), $color->getBlue(), (100 - $color->getAlpha()) * 127 / 100);
+ if (false === $gdColor) {
+ throw new RuntimeException(sprintf('Unable to allocate color "RGB(%s, %s, %s)" with transparency of %d percent', $color->getRed(), $color->getGreen(), $color->getBlue(), $color->getAlpha()));
+ }
+
+ return $gdColor;
+ }
+
+ private function loadGdInfo()
+ {
+ if (!function_exists('gd_info')) {
+ throw new RuntimeException('Gd not installed');
+ }
+
+ $this->info = gd_info();
+ }
+
+ /**
+ * Internal
+ *
+ * Fits a string into box with given width
+ */
+ private function wrapText($string, AbstractFont $font, $angle, $width)
+ {
+ $result = '';
+ $words = explode(' ', $string);
+ foreach ($words as $word) {
+ $teststring = $result . ' ' . $word;
+ $testbox = imagettfbbox($font->getSize(), $angle, $font->getFile(), $teststring);
+ if ($testbox[2] > $width) {
+ $result .= ($result == '' ? '' : "\n") . $word;
+ } else {
+ $result .= ($result == '' ? '' : ' ') . $word;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Symfony/Component/Image/Gd/Effects.php b/src/Symfony/Component/Image/Gd/Effects.php
new file mode 100644
index 0000000000000..4044ba0489593
--- /dev/null
+++ b/src/Symfony/Component/Image/Gd/Effects.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gd;
+
+use Symfony\Component\Image\Effects\EffectsInterface;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor;
+
+/**
+ * Effects implementation using the GD library
+ */
+class Effects implements EffectsInterface
+{
+ private $resource;
+
+ public function __construct($resource)
+ {
+ $this->resource = $resource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function gamma($correction)
+ {
+ if (false === imagegammacorrect($this->resource, 1.0, $correction)) {
+ throw new RuntimeException('Failed to apply gamma correction to the image');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function negative()
+ {
+ if (false === imagefilter($this->resource, IMG_FILTER_NEGATE)) {
+ throw new RuntimeException('Failed to negate the image');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function grayscale()
+ {
+ if (false === imagefilter($this->resource, IMG_FILTER_GRAYSCALE)) {
+ throw new RuntimeException('Failed to grayscale the image');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function colorize(ColorInterface $color)
+ {
+ if (!$color instanceof RGBColor) {
+ throw new RuntimeException('Colorize effects only accepts RGB color in GD context');
+ }
+
+ if (false === imagefilter($this->resource, IMG_FILTER_COLORIZE, $color->getRed(), $color->getGreen(), $color->getBlue())) {
+ throw new RuntimeException('Failed to colorize the image');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sharpen()
+ {
+ $sharpenMatrix = array(array(-1,-1,-1), array(-1,16,-1), array(-1,-1,-1));
+ $divisor = array_sum(array_map('array_sum', $sharpenMatrix));
+
+ if (false === imageconvolution($this->resource, $sharpenMatrix, $divisor, 0)) {
+ throw new RuntimeException('Failed to sharpen the image');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blur($sigma = 1)
+ {
+ if (false === imagefilter($this->resource, IMG_FILTER_GAUSSIAN_BLUR)) {
+ throw new RuntimeException('Failed to blur the image');
+ }
+
+ return $this;
+ }
+}
diff --git a/src/Symfony/Component/Image/Gd/Font.php b/src/Symfony/Component/Image/Gd/Font.php
new file mode 100644
index 0000000000000..bb141af92ae46
--- /dev/null
+++ b/src/Symfony/Component/Image/Gd/Font.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gd;
+
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\AbstractFont;
+use Symfony\Component\Image\Image\Box;
+
+/**
+ * Font implementation using the GD library
+ */
+final class Font extends AbstractFont
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function box($string, $angle = 0)
+ {
+ if (!function_exists('imageftbbox')) {
+ throw new RuntimeException('GD must have been compiled with `--with-freetype-dir` option to use the Font feature.');
+ }
+
+ $angle = -1 * $angle;
+ $info = imageftbbox($this->size, $angle, $this->file, $string);
+ $xs = array($info[0], $info[2], $info[4], $info[6]);
+ $ys = array($info[1], $info[3], $info[5], $info[7]);
+ $width = abs(max($xs) - min($xs));
+ $height = abs(max($ys) - min($ys));
+
+ return new Box($width, $height);
+ }
+}
diff --git a/src/Symfony/Component/Image/Gd/Image.php b/src/Symfony/Component/Image/Gd/Image.php
new file mode 100644
index 0000000000000..051c89408c791
--- /dev/null
+++ b/src/Symfony/Component/Image/Gd/Image.php
@@ -0,0 +1,735 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gd;
+
+use Symfony\Component\Image\Image\AbstractImage;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Fill\FillInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor;
+use Symfony\Component\Image\Image\ProfileInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+/**
+ * Image implementation using the GD library
+ */
+final class Image extends AbstractImage
+{
+ /**
+ * @var resource
+ */
+ private $resource;
+
+ /**
+ * @var Layers|null
+ */
+ private $layers;
+
+ /**
+ * @var PaletteInterface
+ */
+ private $palette;
+
+ /**
+ * Constructs a new Image instance
+ *
+ * @param resource $resource
+ * @param PaletteInterface $palette
+ * @param MetadataBag $metadata
+ */
+ public function __construct($resource, PaletteInterface $palette, MetadataBag $metadata)
+ {
+ $this->metadata = $metadata;
+ $this->palette = $palette;
+ $this->resource = $resource;
+ }
+
+ /**
+ * Makes sure the current image resource is destroyed
+ */
+ public function __destruct()
+ {
+ if (is_resource($this->resource) && 'gd' === get_resource_type($this->resource)) {
+ imagedestroy($this->resource);
+ }
+ }
+
+ /**
+ * Returns Gd resource
+ *
+ * @return resource
+ */
+ public function getGdResource()
+ {
+ return $this->resource;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function copy()
+ {
+ $size = $this->getSize();
+ $copy = $this->createImage($size, 'copy');
+
+ if (false === imagecopy($copy, $this->resource, 0, 0, 0, 0, $size->getWidth(), $size->getHeight())) {
+ throw new RuntimeException('Image copy operation failed');
+ }
+
+ return new Image($copy, $this->palette, $this->metadata);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function crop(PointInterface $start, BoxInterface $size)
+ {
+ if (!$start->in($this->getSize())) {
+ throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders');
+ }
+
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $dest = $this->createImage($size, 'crop');
+
+ if (false === imagecopy($dest, $this->resource, 0, 0, $start->getX(), $start->getY(), $width, $height)) {
+ throw new RuntimeException('Image crop operation failed');
+ }
+
+ imagedestroy($this->resource);
+
+ $this->resource = $dest;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function paste(ImageInterface $image, PointInterface $start)
+ {
+ if (!$image instanceof self) {
+ throw new InvalidArgumentException(sprintf('Gd\Image can only paste() Gd\Image instances, %s given', get_class($image)));
+ }
+
+ $size = $image->getSize();
+ if (!$this->getSize()->contains($size, $start)) {
+ throw new OutOfBoundsException('Cannot paste image of the given size at the specified position, as it moves outside of the current image\'s box');
+ }
+
+ imagealphablending($this->resource, true);
+ imagealphablending($image->resource, true);
+
+ if (false === imagecopy($this->resource, $image->resource, $start->getX(), $start->getY(), 0, 0, $size->getWidth(), $size->getHeight())) {
+ throw new RuntimeException('Image paste operation failed');
+ }
+
+ imagealphablending($this->resource, false);
+ imagealphablending($image->resource, false);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ if (ImageInterface::FILTER_UNDEFINED !== $filter) {
+ throw new InvalidArgumentException('Unsupported filter type, GD only supports ImageInterface::FILTER_UNDEFINED filter');
+ }
+
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $dest = $this->createImage($size, 'resize');
+
+ imagealphablending($this->resource, true);
+ imagealphablending($dest, true);
+
+ if (false === imagecopyresampled($dest, $this->resource, 0, 0, 0, 0, $width, $height, imagesx($this->resource), imagesy($this->resource))) {
+ throw new RuntimeException('Image resize operation failed');
+ }
+
+ imagealphablending($this->resource, false);
+ imagealphablending($dest, false);
+
+ imagedestroy($this->resource);
+
+ $this->resource = $dest;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function rotate($angle, ColorInterface $background = null)
+ {
+ $color = $background ? $background : $this->palette->color('fff');
+ $resource = imagerotate($this->resource, -1 * $angle, $this->getColor($color));
+
+ if (false === $resource) {
+ throw new RuntimeException('Image rotate operation failed');
+ }
+
+ imagedestroy($this->resource);
+ $this->resource = $resource;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function save($path = null, array $options = array())
+ {
+ $path = null === $path ? (isset($this->metadata['filepath']) ? $this->metadata['filepath'] : $path) : $path;
+
+ if (null === $path) {
+ throw new RuntimeException('You can omit save path only if image has been open from a file');
+ }
+
+ if (isset($options['format'])) {
+ $format = $options['format'];
+ } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) {
+ $format = $extension;
+ } else {
+ $originalPath = isset($this->metadata['filepath']) ? $this->metadata['filepath'] : null;
+ $format = pathinfo($originalPath, \PATHINFO_EXTENSION);
+ }
+
+ $this->saveOrOutput($format, $options, $path);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function show($format, array $options = array())
+ {
+ header('Content-type: '.$this->getMimeType($format));
+
+ $this->saveOrOutput($format, $options);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($format, array $options = array())
+ {
+ ob_start();
+ $this->saveOrOutput($format, $options);
+
+ return ob_get_clean();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return $this->get('png');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function flipHorizontally()
+ {
+ $size = $this->getSize();
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+ $dest = $this->createImage($size, 'flip');
+
+ for ($i = 0; $i < $width; $i++) {
+ if (false === imagecopy($dest, $this->resource, $i, 0, ($width - 1) - $i, 0, 1, $height)) {
+ throw new RuntimeException('Horizontal flip operation failed');
+ }
+ }
+
+ imagedestroy($this->resource);
+
+ $this->resource = $dest;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function flipVertically()
+ {
+ $size = $this->getSize();
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+ $dest = $this->createImage($size, 'flip');
+
+ for ($i = 0; $i < $height; $i++) {
+ if (false === imagecopy($dest, $this->resource, 0, $i, 0, ($height - 1) - $i, $width, 1)) {
+ throw new RuntimeException('Vertical flip operation failed');
+ }
+ }
+
+ imagedestroy($this->resource);
+
+ $this->resource = $dest;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ final public function strip()
+ {
+ // GD strips profiles and comment, so there's nothing to do here
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function draw()
+ {
+ return new Drawer($this->resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function effects()
+ {
+ return new Effects($this->resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSize()
+ {
+ return new Box(imagesx($this->resource), imagesy($this->resource));
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function applyMask(ImageInterface $mask)
+ {
+ if (!$mask instanceof self) {
+ throw new InvalidArgumentException('Cannot mask non-gd images');
+ }
+
+ $size = $this->getSize();
+ $maskSize = $mask->getSize();
+
+ if ($size != $maskSize) {
+ throw new InvalidArgumentException(sprintf('The given mask doesn\'t match current image\'s size, Current mask\'s dimensions are %s, while image\'s dimensions are %s', $maskSize, $size));
+ }
+
+ for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) {
+ for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) {
+ $position = new Point($x, $y);
+ $color = $this->getColorAt($position);
+ $maskColor = $mask->getColorAt($position);
+ $round = (int) round(max($color->getAlpha(), (100 - $color->getAlpha()) * $maskColor->getRed() / 255));
+
+ if (false === imagesetpixel($this->resource, $x, $y, $this->getColor($color->dissolve($round - $color->getAlpha())))) {
+ throw new RuntimeException('Apply mask operation failed');
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function fill(FillInterface $fill)
+ {
+ $size = $this->getSize();
+
+ for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) {
+ for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) {
+ if (false === imagesetpixel($this->resource, $x, $y, $this->getColor($fill->getColor(new Point($x, $y))))) {
+ throw new RuntimeException('Fill operation failed');
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mask()
+ {
+ $mask = $this->copy();
+
+ if (false === imagefilter($mask->resource, IMG_FILTER_GRAYSCALE)) {
+ throw new RuntimeException('Mask operation failed');
+ }
+
+ return $mask;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function histogram()
+ {
+ $size = $this->getSize();
+ $colors = array();
+
+ for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) {
+ for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) {
+ $colors[] = $this->getColorAt(new Point($x, $y));
+ }
+ }
+
+ return array_unique($colors);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getColorAt(PointInterface $point)
+ {
+ if (!$point->in($this->getSize())) {
+ throw new RuntimeException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight()));
+ }
+
+ $index = imagecolorat($this->resource, $point->getX(), $point->getY());
+ $info = imagecolorsforindex($this->resource, $index);
+
+ return $this->palette->color(array($info['red'], $info['green'], $info['blue']), max(min(100 - (int) round($info['alpha'] / 127 * 100), 100), 0));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function layers()
+ {
+ if (null === $this->layers) {
+ $this->layers = new Layers($this, $this->palette, $this->resource);
+ }
+
+ return $this->layers;
+ }
+
+ /**
+ * {@inheritdoc}
+ **/
+ public function interlace($scheme)
+ {
+ static $supportedInterlaceSchemes = array(
+ ImageInterface::INTERLACE_NONE => 0,
+ ImageInterface::INTERLACE_LINE => 1,
+ ImageInterface::INTERLACE_PLANE => 1,
+ ImageInterface::INTERLACE_PARTITION => 1,
+ );
+
+ if (!array_key_exists($scheme, $supportedInterlaceSchemes)) {
+ throw new InvalidArgumentException('Unsupported interlace type');
+ }
+
+ imageinterlace($this->resource, $supportedInterlaceSchemes[$scheme]);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function palette()
+ {
+ return $this->palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function profile(ProfileInterface $profile)
+ {
+ throw new RuntimeException('GD driver does not support color profiles');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function usePalette(PaletteInterface $palette)
+ {
+ if (!$palette instanceof RGB) {
+ throw new RuntimeException('GD driver only supports RGB palette');
+ }
+
+ $this->palette = $palette;
+
+ return $this;
+ }
+
+ /**
+ * Internal
+ *
+ * Performs save or show operation using one of GD's image... functions
+ *
+ * @param string $format
+ * @param array $options
+ * @param string $filename
+ *
+ * @throws InvalidArgumentException
+ * @throws RuntimeException
+ */
+ private function saveOrOutput($format, array $options, $filename = null)
+ {
+ $format = $this->normalizeFormat($format);
+
+ if (!$this->supported($format)) {
+ throw new InvalidArgumentException(sprintf('Saving image in "%s" format is not supported, please use one of the following extensions: "%s"', $format, implode('", "', $this->supported())));
+ }
+
+ $save = 'image'.$format;
+ $args = array(&$this->resource, $filename);
+
+ // Preserve BC until version 1.0
+ if (isset($options['quality']) && !isset($options['png_compression_level'])) {
+ $options['png_compression_level'] = round((100 - $options['quality']) * 9 / 100);
+ }
+ if (isset($options['filters']) && !isset($options['png_compression_filter'])) {
+ $options['png_compression_filter'] = $options['filters'];
+ }
+
+ $options = $this->updateSaveOptions($options);
+
+ if ($format === 'jpeg' && isset($options['jpeg_quality'])) {
+ $args[] = $options['jpeg_quality'];
+ }
+
+ if ($format === 'png') {
+ if (isset($options['png_compression_level'])) {
+ if ($options['png_compression_level'] < 0 || $options['png_compression_level'] > 9) {
+ throw new InvalidArgumentException('png_compression_level option should be an integer from 0 to 9');
+ }
+ $args[] = $options['png_compression_level'];
+ } else {
+ $args[] = -1; // use default level
+ }
+
+ if (isset($options['png_compression_filter'])) {
+ if (~PNG_ALL_FILTERS & $options['png_compression_filter']) {
+ throw new InvalidArgumentException('png_compression_filter option should be a combination of the PNG_FILTER_XXX constants');
+ }
+ $args[] = $options['png_compression_filter'];
+ }
+ }
+
+ if (($format === 'wbmp' || $format === 'xbm') && isset($options['foreground'])) {
+ $args[] = $options['foreground'];
+ }
+
+ $this->setExceptionHandler();
+
+ if (false === call_user_func_array($save, $args)) {
+ throw new RuntimeException('Save operation failed');
+ }
+
+ $this->resetExceptionHandler();
+ }
+
+ /**
+ * Internal
+ *
+ * Generates a GD image
+ *
+ * @param BoxInterface $size
+ * @param string the operation initiating the creation
+ *
+ * @return resource
+ *
+ * @throws RuntimeException
+ *
+ */
+ private function createImage(BoxInterface $size, $operation)
+ {
+ $resource = imagecreatetruecolor($size->getWidth(), $size->getHeight());
+
+ if (false === $resource) {
+ throw new RuntimeException('Image '.$operation.' failed');
+ }
+
+ if (false === imagealphablending($resource, false) || false === imagesavealpha($resource, true)) {
+ throw new RuntimeException('Image '.$operation.' failed');
+ }
+
+ if (function_exists('imageantialias')) {
+ imageantialias($resource, true);
+ }
+
+ $transparent = imagecolorallocatealpha($resource, 255, 255, 255, 127);
+ imagefill($resource, 0, 0, $transparent);
+ imagecolortransparent($resource, $transparent);
+
+ return $resource;
+ }
+
+ /**
+ * Internal
+ *
+ * Generates a GD color from Color instance
+ *
+ * @param ColorInterface $color
+ *
+ * @return integer A color identifier
+ *
+ * @throws RuntimeException
+ * @throws InvalidArgumentException
+ */
+ private function getColor(ColorInterface $color)
+ {
+ if (!$color instanceof RGBColor) {
+ throw new InvalidArgumentException('GD driver only supports RGB colors');
+ }
+
+ $index = imagecolorallocatealpha($this->resource, $color->getRed(), $color->getGreen(), $color->getBlue(), round(127 * (100 - $color->getAlpha()) / 100));
+
+ if (false === $index) {
+ throw new RuntimeException(sprintf('Unable to allocate color "RGB(%s, %s, %s)" with transparency of %d percent', $color->getRed(), $color->getGreen(), $color->getBlue(), $color->getAlpha()));
+ }
+
+ return $index;
+ }
+
+ /**
+ * Internal
+ *
+ * Normalizes a given format name
+ *
+ * @param string $format
+ *
+ * @return string
+ */
+ private function normalizeFormat($format)
+ {
+ $format = strtolower($format);
+
+ if ('jpg' === $format || 'pjpeg' === $format) {
+ $format = 'jpeg';
+ }
+
+ return $format;
+ }
+
+ /**
+ * Internal
+ *
+ * Checks whether a given format is supported by GD library
+ *
+ * @param string $format
+ *
+ * @return Boolean
+ */
+ private function supported($format = null)
+ {
+ $formats = array('gif', 'jpeg', 'png', 'wbmp', 'xbm');
+
+ if (null === $format) {
+ return $formats;
+ }
+
+ return in_array($format, $formats);
+ }
+
+ private function setExceptionHandler()
+ {
+ set_error_handler(function ($errno, $errstr, $errfile, $errline) {
+ if (0 === error_reporting()) {
+ return;
+ }
+
+ throw new RuntimeException($errstr, $errno, new \ErrorException($errstr, 0, $errno, $errfile, $errline));
+ }, E_WARNING | E_NOTICE);
+ }
+
+ private function resetExceptionHandler()
+ {
+ restore_error_handler();
+ }
+
+ /**
+ * Internal
+ *
+ * Get the mime type based on format.
+ *
+ * @param string $format
+ *
+ * @return string mime-type
+ *
+ * @throws RuntimeException
+ */
+ private function getMimeType($format)
+ {
+ $format = $this->normalizeFormat($format);
+
+ if (!$this->supported($format)) {
+ throw new RuntimeException('Invalid format');
+ }
+
+ static $mimeTypes = array(
+ 'jpeg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+ 'wbmp' => 'image/vnd.wap.wbmp',
+ 'xbm' => 'image/xbm',
+ );
+
+ return $mimeTypes[$format];
+ }
+}
diff --git a/src/Symfony/Component/Image/Gd/Layers.php b/src/Symfony/Component/Image/Gd/Layers.php
new file mode 100644
index 0000000000000..4c13e0a2dea28
--- /dev/null
+++ b/src/Symfony/Component/Image/Gd/Layers.php
@@ -0,0 +1,144 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gd;
+
+use Symfony\Component\Image\Image\AbstractLayers;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Exception\NotSupportedException;
+
+class Layers extends AbstractLayers
+{
+ private $image;
+ private $offset;
+ private $resource;
+ private $palette;
+
+ public function __construct(Image $image, PaletteInterface $palette, $resource)
+ {
+ if (!is_resource($resource)) {
+ throw new RuntimeException('Invalid Gd resource provided');
+ }
+
+ $this->image = $image;
+ $this->resource = $resource;
+ $this->offset = 0;
+ $this->palette = $palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function merge()
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function coalesce()
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function animate($format, $delay, $loops)
+ {
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current()
+ {
+ return new Image($this->resource, $this->palette, new MetadataBag());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return $this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ ++$this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ $this->offset = 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ return $this->offset < 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ return 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetExists($offset)
+ {
+ return 0 === $offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetGet($offset)
+ {
+ if (0 === $offset) {
+ return new Image($this->resource, $this->palette, new MetadataBag());
+ }
+
+ throw new RuntimeException('GD only supports one layer at offset 0');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetSet($offset, $value)
+ {
+ throw new NotSupportedException('GD does not support layer set');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetUnset($offset)
+ {
+ throw new NotSupportedException('GD does not support layer unset');
+ }
+}
diff --git a/src/Symfony/Component/Image/Gd/Loader.php b/src/Symfony/Component/Image/Gd/Loader.php
new file mode 100644
index 0000000000000..85437cad45c78
--- /dev/null
+++ b/src/Symfony/Component/Image/Gd/Loader.php
@@ -0,0 +1,195 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gd;
+
+use Symfony\Component\Image\Image\AbstractLoader;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+/**
+ * Loader implementation using the GD library
+ */
+final class Loader extends AbstractLoader
+{
+ /**
+ * @var array
+ */
+ private $info;
+
+ /**
+ * @throws RuntimeException
+ */
+ public function __construct()
+ {
+ $this->loadGdInfo();
+ $this->requireGdVersion('2.0.1');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(BoxInterface $size, ColorInterface $color = null)
+ {
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $resource = imagecreatetruecolor($width, $height);
+
+ if (false === $resource) {
+ throw new RuntimeException('Create operation failed');
+ }
+
+ $palette = null !== $color ? $color->getPalette() : new RGB();
+ $color = $color ? $color : $palette->color('fff');
+
+ if (!$color instanceof RGBColor) {
+ throw new InvalidArgumentException('GD driver only supports RGB colors');
+ }
+
+ $index = imagecolorallocatealpha($resource, $color->getRed(), $color->getGreen(), $color->getBlue(), round(127 * (100 - $color->getAlpha()) / 100));
+
+ if (false === $index) {
+ throw new RuntimeException('Unable to allocate color');
+ }
+
+ if (false === imagefill($resource, 0, 0, $index)) {
+ throw new RuntimeException('Could not set background color fill');
+ }
+
+ if ($color->getAlpha() >= 95) {
+ imagecolortransparent($resource, $index);
+ }
+
+ return $this->wrap($resource, $palette, new MetadataBag());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function open($path)
+ {
+ $path = $this->checkPath($path);
+ $data = @file_get_contents($path);
+
+ if (false === $data) {
+ throw new RuntimeException(sprintf('Failed to open file %s', $path));
+ }
+
+ $resource = @imagecreatefromstring($data);
+
+ if (!is_resource($resource)) {
+ throw new RuntimeException(sprintf('Unable to open image %s', $path));
+ }
+
+ return $this->wrap($resource, new RGB(), $this->getMetadataReader()->readFile($path));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load($string)
+ {
+ return $this->doLoad($string, $this->getMetadataReader()->readData($string));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read($resource)
+ {
+ if (!is_resource($resource)) {
+ throw new InvalidArgumentException('Variable does not contain a stream resource');
+ }
+
+ $content = stream_get_contents($resource);
+
+ if (false === $content) {
+ throw new InvalidArgumentException('Cannot read resource content');
+ }
+
+ return $this->doLoad($content, $this->getMetadataReader()->readData($content, $resource));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function font($file, $size, ColorInterface $color)
+ {
+ if (!$this->info['FreeType Support']) {
+ throw new RuntimeException('GD is not compiled with FreeType support');
+ }
+
+ return new Font($file, $size, $color);
+ }
+
+ private function wrap($resource, PaletteInterface $palette, MetadataBag $metadata)
+ {
+ if (!imageistruecolor($resource)) {
+ list($width, $height) = array(imagesx($resource), imagesy($resource));
+
+ // create transparent truecolor canvas
+ $truecolor = imagecreatetruecolor($width, $height);
+ $transparent = imagecolorallocatealpha($truecolor, 255, 255, 255, 127);
+
+ imagefill($truecolor, 0, 0, $transparent);
+ imagecolortransparent($truecolor, $transparent);
+
+ imagecopymerge($truecolor, $resource, 0, 0, 0, 0, $width, $height, 100);
+
+ imagedestroy($resource);
+ $resource = $truecolor;
+ }
+
+ if (false === imagealphablending($resource, false) || false === imagesavealpha($resource, true)) {
+ throw new RuntimeException('Could not set alphablending, savealpha and antialias values');
+ }
+
+ if (function_exists('imageantialias')) {
+ imageantialias($resource, true);
+ }
+
+ return new Image($resource, $palette, $metadata);
+ }
+
+ private function loadGdInfo()
+ {
+ if (!function_exists('gd_info')) {
+ throw new RuntimeException('Gd not installed');
+ }
+
+ $this->info = gd_info();
+ }
+
+ private function requireGdVersion($version)
+ {
+ if (version_compare(GD_VERSION, $version, '<')) {
+ throw new RuntimeException(sprintf('GD2 version %s or higher is required, %s provided', $version, GD_VERSION));
+ }
+ }
+
+ private function doLoad($string, MetadataBag $metadata)
+ {
+ $resource = @imagecreatefromstring($string);
+
+ if (!is_resource($resource)) {
+ throw new RuntimeException('An image could not be created from the given input');
+ }
+
+ return $this->wrap($resource, new RGB(), $metadata);
+ }
+}
diff --git a/src/Symfony/Component/Image/Gmagick/Drawer.php b/src/Symfony/Component/Image/Gmagick/Drawer.php
new file mode 100644
index 0000000000000..327d880639bda
--- /dev/null
+++ b/src/Symfony/Component/Image/Gmagick/Drawer.php
@@ -0,0 +1,356 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gmagick;
+
+use Symfony\Component\Image\Draw\DrawerInterface;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\NotSupportedException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\AbstractFont;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Drawer implementation using the Gmagick PHP extension
+ */
+final class Drawer implements DrawerInterface
+{
+ /**
+ * @var \Gmagick
+ */
+ private $gmagick;
+
+ /**
+ * @param \Gmagick $gmagick
+ */
+ public function __construct(\Gmagick $gmagick)
+ {
+ $this->gmagick = $gmagick;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1)
+ {
+ $x = $center->getX();
+ $y = $center->getY();
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ try {
+ $pixel = $this->getColor($color);
+ $arc = new \GmagickDraw();
+
+ $arc->setstrokecolor($pixel);
+ $arc->setstrokewidth(max(1, (int) $thickness));
+ $arc->setfillcolor('transparent');
+ $arc->arc(
+ $x - $width / 2,
+ $y - $height / 2,
+ $x + $width / 2,
+ $y + $height / 2,
+ $start,
+ $end
+ );
+
+ $this->gmagick->drawImage($arc);
+
+ $pixel = null;
+
+ $arc = null;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Draw arc operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ $x = $center->getX();
+ $y = $center->getY();
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ try {
+ $pixel = $this->getColor($color);
+ $chord = new \GmagickDraw();
+
+ $chord->setstrokecolor($pixel);
+ $chord->setstrokewidth(max(1, (int) $thickness));
+
+ if ($fill) {
+ $chord->setfillcolor($pixel);
+ } else {
+ $x1 = round($x + $width / 2 * cos(deg2rad($start)));
+ $y1 = round($y + $height / 2 * sin(deg2rad($start)));
+ $x2 = round($x + $width / 2 * cos(deg2rad($end)));
+ $y2 = round($y + $height / 2 * sin(deg2rad($end)));
+
+ $this->line(new Point($x1, $y1), new Point($x2, $y2), $color);
+
+ $chord->setfillcolor('transparent');
+ }
+
+ $chord->arc($x - $width / 2, $y - $height / 2, $x + $width / 2, $y + $height / 2, $start, $end);
+
+ $this->gmagick->drawImage($chord);
+
+ $pixel = null;
+
+ $chord = null;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Draw chord operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ try {
+ $pixel = $this->getColor($color);
+ $ellipse = new \GmagickDraw();
+
+ $ellipse->setstrokecolor($pixel);
+ $ellipse->setstrokewidth(max(1, (int) $thickness));
+
+ if ($fill) {
+ $ellipse->setfillcolor($pixel);
+ } else {
+ $ellipse->setfillcolor('transparent');
+ }
+
+ $ellipse->ellipse(
+ $center->getX(),
+ $center->getY(),
+ $width / 2,
+ $height / 2,
+ 0, 360
+ );
+
+ $this->gmagick->drawImage($ellipse);
+
+ $pixel = null;
+
+ $ellipse = null;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Draw ellipse operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1)
+ {
+ try {
+ $pixel = $this->getColor($color);
+ $line = new \GmagickDraw();
+
+ $line->setstrokecolor($pixel);
+ $line->setstrokewidth(max(1, (int) $thickness));
+ $line->setfillcolor($pixel);
+ $line->line(
+ $start->getX(),
+ $start->getY(),
+ $end->getX(),
+ $end->getY()
+ );
+
+ $this->gmagick->drawImage($line);
+
+ $pixel = null;
+
+ $line = null;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Draw line operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $x1 = round($center->getX() + $width / 2 * cos(deg2rad($start)));
+ $y1 = round($center->getY() + $height / 2 * sin(deg2rad($start)));
+ $x2 = round($center->getX() + $width / 2 * cos(deg2rad($end)));
+ $y2 = round($center->getY() + $height / 2 * sin(deg2rad($end)));
+
+ if ($fill) {
+ $this->chord($center, $size, $start, $end, $color, true, $thickness);
+ $this->polygon(
+ array(
+ $center,
+ new Point($x1, $y1),
+ new Point($x2, $y2),
+ ),
+ $color,
+ true,
+ $thickness
+ );
+ } else {
+ $this->arc($center, $size, $start, $end, $color, $thickness);
+ $this->line($center, new Point($x1, $y1), $color, $thickness);
+ $this->line($center, new Point($x2, $y2), $color, $thickness);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dot(PointInterface $position, ColorInterface $color)
+ {
+ $x = $position->getX();
+ $y = $position->getY();
+
+ try {
+ $pixel = $this->getColor($color);
+ $point = new \GmagickDraw();
+
+ $point->setfillcolor($pixel);
+ $point->point($x, $y);
+
+ $this->gmagick->drawimage($point);
+
+ $pixel = null;
+ $point = null;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Draw point operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ if (count($coordinates) < 3) {
+ throw new InvalidArgumentException(sprintf('Polygon must consist of at least 3 coordinates, %d given', count($coordinates)));
+ }
+
+ $points = array_map(function (PointInterface $p) {
+ return array('x' => $p->getX(), 'y' => $p->getY());
+ }, $coordinates);
+
+ try {
+ $pixel = $this->getColor($color);
+ $polygon = new \GmagickDraw();
+
+ $polygon->setstrokecolor($pixel);
+ $polygon->setstrokewidth(max(1, (int) $thickness));
+
+ if ($fill) {
+ $polygon->setfillcolor($pixel);
+ } else {
+ $polygon->setfillcolor('transparent');
+ }
+
+ $polygon->polygon($points);
+
+ $this->gmagick->drawImage($polygon);
+
+ unset($pixel, $polygon);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Draw polygon operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null)
+ {
+ try {
+ $pixel = $this->getColor($font->getColor());
+ $text = new \GmagickDraw();
+
+ $text->setfont($font->getFile());
+ /**
+ * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027
+ *
+ * ensure font resolution is the same as GD's hard-coded 96
+ */
+ $text->setfontsize((int) ($font->getSize() * (96 / 72)));
+ $text->setfillcolor($pixel);
+
+ $info = $this->gmagick->queryfontmetrics($text, $string);
+ $rad = deg2rad($angle);
+ $cos = cos($rad);
+ $sin = sin($rad);
+
+ $x1 = round(0 * $cos - 0 * $sin);
+ $x2 = round($info['textWidth'] * $cos - $info['textHeight'] * $sin);
+ $y1 = round(0 * $sin + 0 * $cos);
+ $y2 = round($info['textWidth'] * $sin + $info['textHeight'] * $cos);
+
+ $xdiff = 0 - min($x1, $x2);
+ $ydiff = 0 - min($y1, $y2);
+
+ if ($width !== null) {
+ throw new NotSupportedException('Gmagick doesn\'t support queryfontmetrics function for multiline text', 1);
+ }
+
+ $this->gmagick->annotateimage($text, $position->getX() + $x1 + $xdiff, $position->getY() + $y2 + $ydiff, $angle, $string);
+
+ unset($pixel, $text);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Draw text operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets specifically formatted color string from Color instance
+ *
+ * @param ColorInterface $color
+ *
+ * @return \GmagickPixel
+ *
+ * @throws InvalidArgumentException In case a non-opaque color is passed
+ */
+ private function getColor(ColorInterface $color)
+ {
+ if (!$color->isOpaque()) {
+ throw new InvalidArgumentException('Gmagick doesn\'t support transparency');
+ }
+
+ return new \GmagickPixel((string) $color);
+ }
+}
diff --git a/src/Symfony/Component/Image/Gmagick/Effects.php b/src/Symfony/Component/Image/Gmagick/Effects.php
new file mode 100644
index 0000000000000..488a00b560a0a
--- /dev/null
+++ b/src/Symfony/Component/Image/Gmagick/Effects.php
@@ -0,0 +1,106 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gmagick;
+
+use Symfony\Component\Image\Effects\EffectsInterface;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Exception\NotSupportedException;
+
+/**
+ * Effects implementation using the Gmagick PHP extension
+ */
+class Effects implements EffectsInterface
+{
+ private $gmagick;
+
+ public function __construct(\Gmagick $gmagick)
+ {
+ $this->gmagick = $gmagick;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function gamma($correction)
+ {
+ try {
+ $this->gmagick->gammaimage($correction);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to apply gamma correction to the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function negative()
+ {
+ if (!method_exists($this->gmagick, 'negateimage')) {
+ throw new NotSupportedException('Gmagick version 1.1.0 RC3 is required for negative effect');
+ }
+
+ try {
+ $this->gmagick->negateimage(false, \Gmagick::CHANNEL_ALL);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to negate the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function grayscale()
+ {
+ try {
+ $this->gmagick->setImageType(2);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function colorize(ColorInterface $color)
+ {
+ throw new NotSupportedException('Gmagick does not support colorize');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sharpen()
+ {
+ throw new NotSupportedException('Gmagick does not support sharpen yet');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blur($sigma = 1)
+ {
+ try {
+ $this->gmagick->blurImage(0, $sigma);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to blur the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+}
diff --git a/src/Symfony/Component/Image/Gmagick/Font.php b/src/Symfony/Component/Image/Gmagick/Font.php
new file mode 100644
index 0000000000000..7641426bbf185
--- /dev/null
+++ b/src/Symfony/Component/Image/Gmagick/Font.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gmagick;
+
+use Symfony\Component\Image\Image\AbstractFont;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+/**
+ * Font implementation using the Gmagick PHP extension
+ */
+final class Font extends AbstractFont
+{
+ /**
+ * @var \Gmagick
+ */
+ private $gmagick;
+
+ /**
+ * @param \Gmagick $gmagick
+ * @param string $file
+ * @param integer $size
+ * @param ColorInterface $color
+ */
+ public function __construct(\Gmagick $gmagick, $file, $size, ColorInterface $color)
+ {
+ $this->gmagick = $gmagick;
+
+ parent::__construct($file, $size, $color);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function box($string, $angle = 0)
+ {
+ $text = new \GmagickDraw();
+
+ $text->setfont($this->file);
+ /**
+ * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027
+ *
+ * ensure font resolution is the same as GD's hard-coded 96
+ */
+ $text->setfontsize((int) ($this->size * (96 / 72)));
+ $text->setfontstyle(\Gmagick::STYLE_OBLIQUE);
+
+ $info = $this->gmagick->queryfontmetrics($text, $string);
+
+ $box = new Box($info['textWidth'], $info['textHeight']);
+
+ return $box;
+ }
+}
diff --git a/src/Symfony/Component/Image/Gmagick/Image.php b/src/Symfony/Component/Image/Gmagick/Image.php
new file mode 100644
index 0000000000000..72ff395c92298
--- /dev/null
+++ b/src/Symfony/Component/Image/Gmagick/Image.php
@@ -0,0 +1,790 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gmagick;
+
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\AbstractImage;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Fill\FillInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Image\ProfileInterface;
+
+/**
+ * Image implementation using the Gmagick PHP extension
+ */
+final class Image extends AbstractImage
+{
+ /**
+ * @var \Gmagick
+ */
+ private $gmagick;
+ /**
+ * @var Layers
+ */
+ private $layers;
+
+ /**
+ * @var PaletteInterface
+ */
+ private $palette;
+
+ private static $colorspaceMapping = array(
+ PaletteInterface::PALETTE_CMYK => \Gmagick::COLORSPACE_CMYK,
+ PaletteInterface::PALETTE_RGB => \Gmagick::COLORSPACE_RGB,
+ );
+
+ /**
+ * Constructs a new Image instance
+ *
+ * @param \Gmagick $gmagick
+ * @param PaletteInterface $palette
+ * @param MetadataBag $metadata
+ */
+ public function __construct(\Gmagick $gmagick, PaletteInterface $palette, MetadataBag $metadata)
+ {
+ $this->metadata = $metadata;
+ $this->gmagick = $gmagick;
+ $this->setColorspace($palette);
+ $this->layers = new Layers($this, $this->palette, $this->gmagick);
+ }
+
+ /**
+ * Destroys allocated gmagick resources
+ */
+ public function __destruct()
+ {
+ if ($this->gmagick instanceof \Gmagick) {
+ $this->gmagick->clear();
+ $this->gmagick->destroy();
+ }
+ }
+
+ /**
+ * Returns gmagick instance
+ *
+ * @return \Gmagick
+ */
+ public function getGmagick()
+ {
+ return $this->gmagick;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function copy()
+ {
+ return new self(clone $this->gmagick, $this->palette, clone $this->metadata);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function crop(PointInterface $start, BoxInterface $size)
+ {
+ if (!$start->in($this->getSize())) {
+ throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders');
+ }
+
+ try {
+ $this->gmagick->cropimage($size->getWidth(), $size->getHeight(), $start->getX(), $start->getY());
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Crop operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function flipHorizontally()
+ {
+ try {
+ $this->gmagick->flopimage();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Horizontal flip operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function flipVertically()
+ {
+ try {
+ $this->gmagick->flipimage();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Vertical flip operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function strip()
+ {
+ try {
+ try {
+ $this->profile($this->palette->profile());
+ } catch (\Exception $e) {
+ // here we discard setting the profile as the previous incorporated profile
+ // is corrupted, let's now strip the image
+ }
+ $this->gmagick->stripimage();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Strip operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function paste(ImageInterface $image, PointInterface $start)
+ {
+ if (!$image instanceof self) {
+ throw new InvalidArgumentException(sprintf('Gmagick\Image can only paste() Gmagick\Image instances, %s given', get_class($image)));
+ }
+
+ if (!$this->getSize()->contains($image->getSize(), $start)) {
+ throw new OutOfBoundsException('Cannot paste image of the given size at the specified position, as it moves outside of the current image\'s box');
+ }
+
+ try {
+ $this->gmagick->compositeimage($image->gmagick, \Gmagick::COMPOSITE_DEFAULT, $start->getX(), $start->getY());
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Paste operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ static $supportedFilters = array(
+ ImageInterface::FILTER_UNDEFINED => \Gmagick::FILTER_UNDEFINED,
+ ImageInterface::FILTER_BESSEL => \Gmagick::FILTER_BESSEL,
+ ImageInterface::FILTER_BLACKMAN => \Gmagick::FILTER_BLACKMAN,
+ ImageInterface::FILTER_BOX => \Gmagick::FILTER_BOX,
+ ImageInterface::FILTER_CATROM => \Gmagick::FILTER_CATROM,
+ ImageInterface::FILTER_CUBIC => \Gmagick::FILTER_CUBIC,
+ ImageInterface::FILTER_GAUSSIAN => \Gmagick::FILTER_GAUSSIAN,
+ ImageInterface::FILTER_HANNING => \Gmagick::FILTER_HANNING,
+ ImageInterface::FILTER_HAMMING => \Gmagick::FILTER_HAMMING,
+ ImageInterface::FILTER_HERMITE => \Gmagick::FILTER_HERMITE,
+ ImageInterface::FILTER_LANCZOS => \Gmagick::FILTER_LANCZOS,
+ ImageInterface::FILTER_MITCHELL => \Gmagick::FILTER_MITCHELL,
+ ImageInterface::FILTER_POINT => \Gmagick::FILTER_POINT,
+ ImageInterface::FILTER_QUADRATIC => \Gmagick::FILTER_QUADRATIC,
+ ImageInterface::FILTER_SINC => \Gmagick::FILTER_SINC,
+ ImageInterface::FILTER_TRIANGLE => \Gmagick::FILTER_TRIANGLE
+ );
+
+ if (!array_key_exists($filter, $supportedFilters)) {
+ throw new InvalidArgumentException('Unsupported filter type');
+ }
+
+ try {
+ $this->gmagick->resizeimage($size->getWidth(), $size->getHeight(), $supportedFilters[$filter], 1);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Resize operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function rotate($angle, ColorInterface $background = null)
+ {
+ try {
+ $background = $background ?: $this->palette->color('fff');
+ $pixel = $this->getColor($background);
+
+ $this->gmagick->rotateimage($pixel, $angle);
+
+ unset($pixel);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Rotate operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Internal
+ *
+ * Applies options before save or output
+ *
+ * @param \Gmagick $image
+ * @param array $options
+ * @param string $path
+ *
+ * @throws InvalidArgumentException
+ */
+ private function applyImageOptions(\Gmagick $image, array $options, $path)
+ {
+ if (isset($options['format'])) {
+ $format = $options['format'];
+ } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) {
+ $format = $extension;
+ } else {
+ $format = pathinfo($image->getImageFilename(), \PATHINFO_EXTENSION);
+ }
+
+ $format = strtolower($format);
+
+ $options = $this->updateSaveOptions($options);
+
+ if (isset($options['jpeg_quality']) && in_array($format, array('jpeg', 'jpg', 'pjpeg'))) {
+ $image->setCompressionQuality($options['jpeg_quality']);
+ }
+
+ if ((isset($options['png_compression_level']) || isset($options['png_compression_filter'])) && $format === 'png') {
+ // first digit: compression level (default: 7)
+ if (isset($options['png_compression_level'])) {
+ if ($options['png_compression_level'] < 0 || $options['png_compression_level'] > 9) {
+ throw new InvalidArgumentException('png_compression_level option should be an integer from 0 to 9');
+ }
+ $compression = $options['png_compression_level'] * 10;
+ } else {
+ $compression = 70;
+ }
+
+ // second digit: compression filter (default: 5)
+ if (isset($options['png_compression_filter'])) {
+ if ($options['png_compression_filter'] < 0 || $options['png_compression_filter'] > 9) {
+ throw new InvalidArgumentException('png_compression_filter option should be an integer from 0 to 9');
+ }
+ $compression += $options['png_compression_filter'];
+ } else {
+ $compression += 5;
+ }
+
+ $image->setCompressionQuality($compression);
+ }
+
+ if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) {
+ if ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERCENTIMETER) {
+ $image->setimageunits(\Gmagick::RESOLUTION_PIXELSPERCENTIMETER);
+ } elseif ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERINCH) {
+ $image->setimageunits(\Gmagick::RESOLUTION_PIXELSPERINCH);
+ } else {
+ throw new InvalidArgumentException('Unsupported image unit format');
+ }
+
+ $image->setimageresolution($options['resolution-x'], $options['resolution-y']);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function save($path = null, array $options = array())
+ {
+ $path = null === $path ? $this->gmagick->getImageFilename() : $path;
+
+ if ('' === trim($path)) {
+ throw new RuntimeException('You can omit save path only if image has been open from a file');
+ }
+
+ try {
+ $this->prepareOutput($options, $path);
+ $allFrames = !isset($options['animated']) || false === $options['animated'];
+ $this->gmagick->writeimage($path, $allFrames);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Save operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function show($format, array $options = array())
+ {
+ header('Content-type: '.$this->getMimeType($format));
+ echo $this->get($format, $options);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($format, array $options = array())
+ {
+ try {
+ $options['format'] = $format;
+ $this->prepareOutput($options);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Get operation failed', $e->getCode(), $e);
+ }
+
+ return $this->gmagick->getimagesblob();
+ }
+
+ /**
+ * @param array $options
+ * @param string $path
+ */
+ private function prepareOutput(array $options, $path = null)
+ {
+ if (isset($options['format'])) {
+ $this->gmagick->setimageformat($options['format']);
+ }
+
+ if (isset($options['animated']) && true === $options['animated']) {
+ $format = isset($options['format']) ? $options['format'] : 'gif';
+ $delay = isset($options['animated.delay']) ? $options['animated.delay'] : null;
+ $loops = isset($options['animated.loops']) ? $options['animated.loops'] : 0;
+
+ $options['flatten'] = false;
+
+ $this->layers->animate($format, $delay, $loops);
+ } else {
+ $this->layers->merge();
+ }
+ $this->applyImageOptions($this->gmagick, $options, $path);
+
+ // flatten only if image has multiple layers
+ if ((!isset($options['flatten']) || $options['flatten'] === true) && count($this->layers) > 1) {
+ $this->flatten();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return $this->get('png');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function draw()
+ {
+ return new Drawer($this->gmagick);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function effects()
+ {
+ return new Effects($this->gmagick);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSize()
+ {
+ try {
+ $i = $this->gmagick->getimageindex();
+ $this->gmagick->setimageindex(0); //rewind
+ $width = $this->gmagick->getimagewidth();
+ $height = $this->gmagick->getimageheight();
+ $this->gmagick->setimageindex($i);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Get size operation failed', $e->getCode(), $e);
+ }
+
+ return new Box($width, $height);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function applyMask(ImageInterface $mask)
+ {
+ if (!$mask instanceof self) {
+ throw new InvalidArgumentException('Can only apply instances of Symfony\Component\Image\Gmagick\Image as masks');
+ }
+
+ $size = $this->getSize();
+ $maskSize = $mask->getSize();
+
+ if ($size != $maskSize) {
+ throw new InvalidArgumentException(sprintf('The given mask doesn\'t match current image\'s size, current mask\'s dimensions are %s, while image\'s dimensions are %s', $maskSize, $size));
+ }
+
+ try {
+ $mask = $mask->copy();
+ $this->gmagick->compositeimage($mask->gmagick, \Gmagick::COMPOSITE_DEFAULT, 0, 0);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Apply mask operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mask()
+ {
+ $mask = $this->copy();
+
+ try {
+ $mask->gmagick->modulateimage(100, 0, 100);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Mask operation failed', $e->getCode(), $e);
+ }
+
+ return $mask;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function fill(FillInterface $fill)
+ {
+ try {
+ $draw = new \GmagickDraw();
+ $size = $this->getSize();
+
+ $w = $size->getWidth();
+ $h = $size->getHeight();
+
+ for ($x = 0; $x < $w; $x++) {
+ for ($y = 0; $y < $h; $y++) {
+ $pixel = $this->getColor($fill->getColor(new Point($x, $y)));
+
+ $draw->setfillcolor($pixel);
+ $draw->point($x, $y);
+
+ $pixel = null;
+ }
+ }
+
+ $this->gmagick->drawimage($draw);
+
+ $draw = null;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Fill operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function histogram()
+ {
+ try {
+ $pixels = $this->gmagick->getimagehistogram();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Error while fetching histogram', $e->getCode(), $e);
+ }
+
+ $image = $this;
+
+ return array_map(function (\GmagickPixel $pixel) use ($image) {
+ return $image->pixelToColor($pixel);
+ }, $pixels);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getColorAt(PointInterface $point)
+ {
+ if (!$point->in($this->getSize())) {
+ throw new InvalidArgumentException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight()));
+ }
+
+ try {
+ $cropped = clone $this->gmagick;
+ $histogram = $cropped
+ ->cropImage(1, 1, $point->getX(), $point->getY())
+ ->getImageHistogram();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Unable to get the pixel', $e->getCode(), $e);
+ }
+
+ $pixel = array_shift($histogram);
+
+ unset($histogram, $cropped);
+
+ return $this->pixelToColor($pixel);
+ }
+
+ /**
+ * Returns a color given a pixel, depending the Palette context
+ *
+ * Note : this method is public for PHP 5.3 compatibility
+ *
+ * @param \GmagickPixel $pixel
+ *
+ * @return ColorInterface
+ *
+ * @throws InvalidArgumentException In case a unknown color is requested
+ */
+ public function pixelToColor(\GmagickPixel $pixel)
+ {
+ static $colorMapping = array(
+ ColorInterface::COLOR_RED => \Gmagick::COLOR_RED,
+ ColorInterface::COLOR_GREEN => \Gmagick::COLOR_GREEN,
+ ColorInterface::COLOR_BLUE => \Gmagick::COLOR_BLUE,
+ ColorInterface::COLOR_CYAN => \Gmagick::COLOR_CYAN,
+ ColorInterface::COLOR_MAGENTA => \Gmagick::COLOR_MAGENTA,
+ ColorInterface::COLOR_YELLOW => \Gmagick::COLOR_YELLOW,
+ ColorInterface::COLOR_KEYLINE => \Gmagick::COLOR_BLACK,
+ // There is no gray component in \Gmagick, let's use one of the RGB comp
+ ColorInterface::COLOR_GRAY => \Gmagick::COLOR_RED,
+ );
+
+ if ($this->palette->supportsAlpha()) {
+ try {
+ $alpha = (int) round($pixel->getcolorvalue(\Gmagick::COLOR_ALPHA) * 100);
+ } catch (\GmagickPixelException $e) {
+ $alpha = null;
+ }
+ } else {
+ $alpha = null;
+ }
+
+ $palette = $this->palette();
+
+ return $this->palette->color(array_map(function ($color) use ($palette, $pixel, $colorMapping) {
+ if (!isset($colorMapping[$color])) {
+ throw new InvalidArgumentException(sprintf('Color %s is not mapped in Gmagick', $color));
+ }
+ $multiplier = 255;
+ if ($palette->name() === PaletteInterface::PALETTE_CMYK) {
+ $multiplier = 100;
+ }
+
+ return $pixel->getcolorvalue($colorMapping[$color]) * $multiplier;
+ }, $this->palette->pixelDefinition()), $alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function layers()
+ {
+ return $this->layers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function interlace($scheme)
+ {
+ static $supportedInterlaceSchemes = array(
+ ImageInterface::INTERLACE_NONE => \Gmagick::INTERLACE_NO,
+ ImageInterface::INTERLACE_LINE => \Gmagick::INTERLACE_LINE,
+ ImageInterface::INTERLACE_PLANE => \Gmagick::INTERLACE_PLANE,
+ ImageInterface::INTERLACE_PARTITION => \Gmagick::INTERLACE_PARTITION,
+ );
+
+ if (!array_key_exists($scheme, $supportedInterlaceSchemes)) {
+ throw new InvalidArgumentException('Unsupported interlace type');
+ }
+
+ $this->gmagick->setInterlaceScheme($supportedInterlaceSchemes[$scheme]);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function usePalette(PaletteInterface $palette)
+ {
+ if (!isset(static::$colorspaceMapping[$palette->name()])) {
+ throw new InvalidArgumentException(sprintf('The palette %s is not supported by Gmagick driver',$palette->name()));
+ }
+
+ if ($this->palette->name() === $palette->name()) {
+ return $this;
+ }
+
+ try {
+ try {
+ $hasICCProfile = (Boolean) $this->gmagick->getimageprofile('ICM');
+ } catch (\GmagickException $e) {
+ $hasICCProfile = false;
+ }
+
+ if (!$hasICCProfile) {
+ $this->profile($this->palette->profile());
+ }
+
+ $this->profile($palette->profile());
+
+ $this->setColorspace($palette);
+ $this->palette = $palette;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to set colorspace', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function palette()
+ {
+ return $this->palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function profile(ProfileInterface $profile)
+ {
+ try {
+ $this->gmagick->profileimage('ICM', $profile->data());
+ } catch (\GmagickException $e) {
+ if (false !== strpos($e->getMessage(), 'LCMS encoding not enabled')) {
+ throw new RuntimeException(sprintf('Unable to add profile %s to image, be sue to compile graphicsmagick with `--with-lcms2` option', $profile->name()), $e->getCode(), $e);
+ }
+
+ throw new RuntimeException(sprintf('Unable to add profile %s to image', $profile->name()), $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Internal
+ *
+ * Flatten the image.
+ */
+ private function flatten()
+ {
+ /**
+ * @see http://pecl.php.net/bugs/bug.php?id=22435
+ */
+ if (method_exists($this->gmagick, 'flattenImages')) {
+ try {
+ $this->gmagick = $this->gmagick->flattenImages();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Flatten operation failed', $e->getCode(), $e);
+ }
+ }
+ }
+
+ /**
+ * Gets specifically formatted color string from Color instance
+ *
+ * @param ColorInterface $color
+ *
+ * @return \GmagickPixel
+ *
+ * @throws InvalidArgumentException
+ */
+ private function getColor(ColorInterface $color)
+ {
+ if (!$color->isOpaque()) {
+ throw new InvalidArgumentException('Gmagick doesn\'t support transparency');
+ }
+
+ return new \GmagickPixel((string) $color);
+ }
+
+ /**
+ * Internal
+ *
+ * Get the mime type based on format.
+ *
+ * @param string $format
+ *
+ * @return string mime-type
+ *
+ * @throws InvalidArgumentException
+ */
+ private function getMimeType($format)
+ {
+ static $mimeTypes = array(
+ 'jpeg' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+ 'wbmp' => 'image/vnd.wap.wbmp',
+ 'xbm' => 'image/xbm',
+ );
+
+ if (!isset($mimeTypes[$format])) {
+ throw new InvalidArgumentException(sprintf('Unsupported format given. Only %s are supported, %s given', implode(", ", array_keys($mimeTypes)), $format));
+ }
+
+ return $mimeTypes[$format];
+ }
+
+ /**
+ * Sets colorspace and image type, assigns the palette.
+ *
+ * @param PaletteInterface $palette
+ *
+ * @throws InvalidArgumentException
+ */
+ private function setColorspace(PaletteInterface $palette)
+ {
+ if (!isset(static::$colorspaceMapping[$palette->name()])) {
+ throw new InvalidArgumentException(sprintf('The palette %s is not supported by Gmagick driver', $palette->name()));
+ }
+
+ $this->gmagick->setimagecolorspace(static::$colorspaceMapping[$palette->name()]);
+ $this->palette = $palette;
+ }
+}
diff --git a/src/Symfony/Component/Image/Gmagick/Layers.php b/src/Symfony/Component/Image/Gmagick/Layers.php
new file mode 100644
index 0000000000000..448f213f975f3
--- /dev/null
+++ b/src/Symfony/Component/Image/Gmagick/Layers.php
@@ -0,0 +1,272 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gmagick;
+
+use Symfony\Component\Image\Image\AbstractLayers;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Exception\NotSupportedException;
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+
+class Layers extends AbstractLayers
+{
+ /**
+ * @var Image
+ */
+ private $image;
+
+ /**
+ * @var \Gmagick
+ */
+ private $resource;
+
+ /**
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * @var array
+ */
+ private $layers = array();
+
+ /**
+ * @var PaletteInterface
+ */
+ private $palette;
+
+ public function __construct(Image $image, PaletteInterface $palette, \Gmagick $resource)
+ {
+ $this->image = $image;
+ $this->resource = $resource;
+ $this->palette = $palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function merge()
+ {
+ foreach ($this->layers as $offset => $image) {
+ try {
+ $this->resource->setimageindex($offset);
+ $this->resource->setimage($image->getGmagick());
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to substitute layer', $e->getCode(), $e);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function coalesce()
+ {
+ throw new NotSupportedException('Gmagick does not support coalescing');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function animate($format, $delay, $loops)
+ {
+ if ('gif' !== strtolower($format)) {
+ throw new NotSupportedException('Animated picture is currently only supported on gif');
+ }
+
+ if (!is_int($loops) || $loops < 0) {
+ throw new InvalidArgumentException('Loops must be a positive integer.');
+ }
+
+ if (null !== $delay && (!is_int($delay) || $delay < 0)) {
+ throw new InvalidArgumentException('Delay must be either null or a positive integer.');
+ }
+
+ try {
+ foreach ($this as $offset => $layer) {
+ $this->resource->setimageindex($offset);
+ $this->resource->setimageformat($format);
+
+ if (null !== $delay) {
+ $this->resource->setimagedelay($delay / 10);
+ }
+
+ $this->resource->setimageiterations($loops);
+ }
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to animate layers', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current()
+ {
+ return $this->extractAt($this->offset);
+ }
+
+ /**
+ * Tries to extract layer at given offset
+ *
+ * @param integer $offset
+ * @return Image
+ * @throws RuntimeException
+ */
+ private function extractAt($offset)
+ {
+ if (!isset($this->layers[$offset])) {
+ try {
+ $this->resource->setimageindex($offset);
+ $this->layers[$offset] = new Image($this->resource->getimage(), $this->palette, new MetadataBag());
+ } catch (\GmagickException $e) {
+ throw new RuntimeException(sprintf('Failed to extract layer %d', $offset), $e->getCode(), $e);
+ }
+ }
+
+ return $this->layers[$offset];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return $this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ ++$this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ $this->offset = 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ return $this->offset < count($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ try {
+ return $this->resource->getnumberimages();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Failed to count the number of layers', $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetExists($offset)
+ {
+ return is_int($offset) && $offset >= 0 && $offset < count($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetGet($offset)
+ {
+ return $this->extractAt($offset);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetSet($offset, $image)
+ {
+ if (!$image instanceof Image) {
+ throw new InvalidArgumentException('Only a Gmagick Image can be used as layer');
+ }
+
+ if (null === $offset) {
+ $offset = count($this) - 1;
+ } else {
+ if (!is_int($offset)) {
+ throw new InvalidArgumentException('Invalid offset for layer, it must be an integer');
+ }
+
+ if (count($this) < $offset || 0 > $offset) {
+ throw new OutOfBoundsException(sprintf('Invalid offset for layer, it must be a value between 0 and %d, %d given', count($this), $offset));
+ }
+
+ if (isset($this[$offset])) {
+ unset($this[$offset]);
+ $offset = $offset - 1;
+ }
+ }
+
+ $frame = $image->getGmagick();
+
+ try {
+ if (count($this) > 0) {
+ $this->resource->setimageindex($offset);
+ $this->resource->nextimage();
+ }
+ $this->resource->addimage($frame);
+
+ /**
+ * ugly hack to bypass issue https://bugs.php.net/bug.php?id=64623
+ */
+ if (count($this) == 2) {
+ $this->resource->setimageindex($offset+1);
+ $this->resource->nextimage();
+ $this->resource->addimage($frame);
+ unset($this[0]);
+ }
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Unable to set the layer', $e->getCode(), $e);
+ }
+
+ $this->layers = array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetUnset($offset)
+ {
+ try {
+ $this->extractAt($offset);
+ } catch (RuntimeException $e) {
+ return;
+ }
+
+ try {
+ $this->resource->setimageindex($offset);
+ $this->resource->removeimage();
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Unable to remove layer', $e->getCode(), $e);
+ }
+ }
+}
diff --git a/src/Symfony/Component/Image/Gmagick/Loader.php b/src/Symfony/Component/Image/Gmagick/Loader.php
new file mode 100644
index 0000000000000..d12b09e827051
--- /dev/null
+++ b/src/Symfony/Component/Image/Gmagick/Loader.php
@@ -0,0 +1,165 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Gmagick;
+
+use Symfony\Component\Image\Image\AbstractLoader;
+use Symfony\Component\Image\Exception\NotSupportedException;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\Grayscale;
+use Symfony\Component\Image\Image\Palette\CMYK;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Palette\Color\CMYK as CMYKColor;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+/**
+ * Loader implementation using the Gmagick PHP extension
+ */
+class Loader extends AbstractLoader
+{
+ /**
+ * @throws RuntimeException
+ */
+ public function __construct()
+ {
+ if (!class_exists('Gmagick')) {
+ throw new RuntimeException('Gmagick not installed');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function open($path)
+ {
+ $path = $this->checkPath($path);
+
+ try {
+ $gmagick = new \Gmagick($path);
+ $image = new Image($gmagick, $this->createPalette($gmagick), $this->getMetadataReader()->readFile($path));
+ } catch (\GmagickException $e) {
+ throw new RuntimeException(sprintf('Unable to open image %s', $path), $e->getCode(), $e);
+ }
+
+ return $image;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(BoxInterface $size, ColorInterface $color = null)
+ {
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $palette = null !== $color ? $color->getPalette() : new RGB();
+ $color = null !== $color ? $color : $palette->color('fff');
+
+ try {
+ $gmagick = new \Gmagick();
+ // Gmagick does not support creation of CMYK GmagickPixel
+ // see https://bugs.php.net/bug.php?id=64466
+ if ($color instanceof CMYKColor) {
+ $switchPalette = $palette;
+ $palette = new RGB();
+ $pixel = new \GmagickPixel($palette->color((string) $color));
+ } else {
+ $switchPalette = null;
+ $pixel = new \GmagickPixel((string) $color);
+ }
+
+ if ($color->getPalette()->supportsAlpha() && $color->getAlpha() < 100) {
+ throw new NotSupportedException('alpha transparency is not supported');
+ }
+
+ $gmagick->newimage($width, $height, $pixel->getcolor(false));
+ $gmagick->setimagecolorspace(\Gmagick::COLORSPACE_TRANSPARENT);
+ $gmagick->setimagebackgroundcolor($pixel);
+
+ $image = new Image($gmagick, $palette, new MetadataBag());
+
+ if ($switchPalette) {
+ $image->usePalette($switchPalette);
+ }
+
+ return $image;
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Could not create empty image', $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load($string)
+ {
+ return $this->doLoad($string, $this->getMetadataReader()->readData($string));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read($resource)
+ {
+ if (!is_resource($resource)) {
+ throw new InvalidArgumentException('Variable does not contain a stream resource');
+ }
+
+ $content = stream_get_contents($resource);
+
+ if (false === $content) {
+ throw new InvalidArgumentException('Couldn\'t read given resource');
+ }
+
+ return $this->doLoad($content, $this->getMetadataReader()->readData($content, $resource));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function font($file, $size, ColorInterface $color)
+ {
+ $gmagick = new \Gmagick();
+ $gmagick->newimage(1, 1, 'transparent');
+
+ return new Font($gmagick, $file, $size, $color);
+ }
+
+ private function createPalette(\Gmagick $gmagick)
+ {
+ switch ($gmagick->getimagecolorspace()) {
+ case \Gmagick::COLORSPACE_SRGB:
+ case \Gmagick::COLORSPACE_RGB:
+ return new RGB();
+ case \Gmagick::COLORSPACE_CMYK:
+ return new CMYK();
+ case \Gmagick::COLORSPACE_GRAY:
+ return new Grayscale();
+ default:
+ throw new NotSupportedException('Only RGB and CMYK colorspace are currently supported');
+ }
+ }
+
+ private function doLoad($content, MetadataBag $metadata)
+ {
+ try {
+ $gmagick = new \Gmagick();
+ $gmagick->readimageblob($content);
+ } catch (\GmagickException $e) {
+ throw new RuntimeException('Could not load image from string', $e->getCode(), $e);
+ }
+
+ return new Image($gmagick, $this->createPalette($gmagick), $metadata);
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/AbstractFont.php b/src/Symfony/Component/Image/Image/AbstractFont.php
new file mode 100644
index 0000000000000..d5c057652a4d2
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/AbstractFont.php
@@ -0,0 +1,75 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+/**
+ * Abstract font base class
+ */
+abstract class AbstractFont implements FontInterface
+{
+ /**
+ * @var string
+ */
+ protected $file;
+
+ /**
+ * @var integer
+ */
+ protected $size;
+
+ /**
+ * @var ColorInterface
+ */
+ protected $color;
+
+ /**
+ * Constructs a font with specified $file, $size and $color
+ *
+ * The font size is to be specified in points (e.g. 10pt means 10)
+ *
+ * @param string $file
+ * @param integer $size
+ * @param ColorInterface $color
+ */
+ public function __construct($file, $size, ColorInterface $color)
+ {
+ $this->file = $file;
+ $this->size = $size;
+ $this->color = $color;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function getFile()
+ {
+ return $this->file;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function getSize()
+ {
+ return $this->size;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function getColor()
+ {
+ return $this->color;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/AbstractImage.php b/src/Symfony/Component/Image/Image/AbstractImage.php
new file mode 100644
index 0000000000000..d01bd679a4575
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/AbstractImage.php
@@ -0,0 +1,120 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+
+abstract class AbstractImage implements ImageInterface
+{
+ /**
+ * @var MetadataBag
+ */
+ protected $metadata;
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_INSET, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ if ($mode !== ImageInterface::THUMBNAIL_INSET &&
+ $mode !== ImageInterface::THUMBNAIL_OUTBOUND) {
+ throw new InvalidArgumentException('Invalid mode specified');
+ }
+
+ $imageSize = $this->getSize();
+ $ratios = array(
+ $size->getWidth() / $imageSize->getWidth(),
+ $size->getHeight() / $imageSize->getHeight()
+ );
+
+ $thumbnail = $this->copy();
+
+ $thumbnail->usePalette($this->palette());
+ $thumbnail->strip();
+ // if target width is larger than image width
+ // AND target height is longer than image height
+ if ($size->contains($imageSize)) {
+ return $thumbnail;
+ }
+
+ if ($mode === ImageInterface::THUMBNAIL_INSET) {
+ $ratio = min($ratios);
+ } else {
+ $ratio = max($ratios);
+ }
+
+ if ($mode === ImageInterface::THUMBNAIL_OUTBOUND) {
+ if (!$imageSize->contains($size)) {
+ $size = new Box(
+ min($imageSize->getWidth(), $size->getWidth()),
+ min($imageSize->getHeight(), $size->getHeight())
+ );
+ } else {
+ $imageSize = $thumbnail->getSize()->scale($ratio);
+ $thumbnail->resize($imageSize, $filter);
+ }
+ $thumbnail->crop(new Point(
+ max(0, round(($imageSize->getWidth() - $size->getWidth()) / 2)),
+ max(0, round(($imageSize->getHeight() - $size->getHeight()) / 2))
+ ), $size);
+ } else {
+ if (!$imageSize->contains($size)) {
+ $imageSize = $imageSize->scale($ratio);
+ $thumbnail->resize($imageSize, $filter);
+ } else {
+ $imageSize = $thumbnail->getSize()->scale($ratio);
+ $thumbnail->resize($imageSize, $filter);
+ }
+ }
+
+ return $thumbnail;
+ }
+
+ /**
+ * Updates a given array of save options for backward compatibility with legacy names
+ *
+ * @param array $options
+ *
+ * @return array
+ */
+ protected function updateSaveOptions(array $options)
+ {
+ // Preserve BC until version 1.0
+ if (isset($options['quality']) && !isset($options['jpeg_quality'])) {
+ $options['jpeg_quality'] = $options['quality'];
+ }
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function metadata()
+ {
+ return $this->metadata;
+ }
+
+ /**
+ * Assures the metadata instance will be cloned, too
+ */
+ public function __clone()
+ {
+ if ($this->metadata !== null) {
+ $this->metadata = clone $this->metadata;
+ }
+ }
+
+}
diff --git a/src/Symfony/Component/Image/Image/AbstractLayers.php b/src/Symfony/Component/Image/Image/AbstractLayers.php
new file mode 100644
index 0000000000000..936e7421cf1c3
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/AbstractLayers.php
@@ -0,0 +1,61 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+abstract class AbstractLayers implements LayersInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function add(ImageInterface $image)
+ {
+ $this[] = $image;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set($offset, ImageInterface $image)
+ {
+ $this[$offset] = $image;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove($offset)
+ {
+ unset($this[$offset]);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($offset)
+ {
+ return $this[$offset];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has($offset)
+ {
+ return isset($this[$offset]);
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/AbstractLoader.php b/src/Symfony/Component/Image/Image/AbstractLoader.php
new file mode 100644
index 0000000000000..4006aff865d53
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/AbstractLoader.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Image\Metadata\DefaultMetadataReader;
+use Symfony\Component\Image\Image\Metadata\ExifMetadataReader;
+use Symfony\Component\Image\Image\Metadata\MetadataReaderInterface;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+abstract class AbstractLoader implements LoaderInterface
+{
+ /** @var MetadataReaderInterface */
+ private $metadataReader;
+
+ /**
+ * @param MetadataReaderInterface $metadataReader
+ *
+ * @return LoaderInterface
+ */
+ public function setMetadataReader(MetadataReaderInterface $metadataReader)
+ {
+ $this->metadataReader = $metadataReader;
+
+ return $this;
+ }
+
+ /**
+ * @return MetadataReaderInterface
+ */
+ public function getMetadataReader()
+ {
+ if (null === $this->metadataReader) {
+ if (ExifMetadataReader::isSupported()) {
+ $this->metadataReader = new ExifMetadataReader();
+ } else {
+ $this->metadataReader = new DefaultMetadataReader();
+ }
+ }
+
+ return $this->metadataReader;
+ }
+
+ /**
+ * Checks a path that could be used with LoaderInterface::open and returns
+ * a proper string.
+ *
+ * @param string|object $path
+ *
+ * @return string
+ *
+ * @throws InvalidArgumentException In case the given path is invalid.
+ */
+ protected function checkPath($path)
+ {
+ // provide compatibility with objects such as \SplFileInfo
+ if (is_object($path) && method_exists($path, '__toString')) {
+ $path = (string) $path;
+ }
+
+ $handle = @fopen($path, 'r');
+
+ if (false === $handle) {
+ throw new InvalidArgumentException(sprintf('File %s does not exist.', $path));
+ }
+
+ fclose($handle);
+
+ return $path;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Box.php b/src/Symfony/Component/Image/Image/Box.php
new file mode 100644
index 0000000000000..2872e48a6f670
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Box.php
@@ -0,0 +1,122 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+/**
+ * A box implementation
+ */
+final class Box implements BoxInterface
+{
+ /**
+ * @var integer
+ */
+ private $width;
+
+ /**
+ * @var integer
+ */
+ private $height;
+
+ /**
+ * Constructs the Size with given width and height
+ *
+ * @param integer $width
+ * @param integer $height
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct($width, $height)
+ {
+ if ($height < 1 || $width < 1) {
+ throw new InvalidArgumentException(sprintf('Length of either side cannot be 0 or negative, current size is %sx%s', $width, $height));
+ }
+
+ $this->width = (int) $width;
+ $this->height = (int) $height;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function scale($ratio)
+ {
+ return new Box(round($ratio * $this->width), round($ratio * $this->height));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function increase($size)
+ {
+ return new Box((int) $size + $this->width, (int) $size + $this->height);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function contains(BoxInterface $box, PointInterface $start = null)
+ {
+ $start = $start ? $start : new Point(0, 0);
+
+ return $start->in($this) && $this->width >= $box->getWidth() + $start->getX() && $this->height >= $box->getHeight() + $start->getY();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function square()
+ {
+ return $this->width * $this->height;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return sprintf('%dx%d px', $this->width, $this->height);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function widen($width)
+ {
+ return $this->scale($width / $this->width);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function heighten($height)
+ {
+ return $this->scale($height / $this->height);
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/BoxInterface.php b/src/Symfony/Component/Image/Image/BoxInterface.php
new file mode 100644
index 0000000000000..0fde68689da20
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/BoxInterface.php
@@ -0,0 +1,94 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+/**
+ * Interface for a box
+ */
+interface BoxInterface
+{
+ /**
+ * Gets current image height
+ *
+ * @return integer
+ */
+ public function getHeight();
+
+ /**
+ * Gets current image width
+ *
+ * @return integer
+ */
+ public function getWidth();
+
+ /**
+ * Creates new BoxInterface instance with ratios applied to both sides
+ *
+ * @param float $ratio
+ *
+ * @return BoxInterface
+ */
+ public function scale($ratio);
+
+ /**
+ * Creates new BoxInterface, adding given size to both sides
+ *
+ * @param integer $size
+ *
+ * @return BoxInterface
+ */
+ public function increase($size);
+
+ /**
+ * Checks whether current box can fit given box at a given start position,
+ * start position defaults to top left corner xy(0,0)
+ *
+ * @param BoxInterface $box
+ * @param PointInterface $start
+ *
+ * @return Boolean
+ */
+ public function contains(BoxInterface $box, PointInterface $start = null);
+
+ /**
+ * Gets current box square, useful for getting total number of pixels in a
+ * given box
+ *
+ * @return integer
+ */
+ public function square();
+
+ /**
+ * Returns a string representation of the current box
+ *
+ * @return string
+ */
+ public function __toString();
+
+ /**
+ * Resizes box to given width, constraining proportions and returns the new box
+ *
+ * @param integer $width
+ *
+ * @return BoxInterface
+ */
+ public function widen($width);
+
+ /**
+ * Resizes box to given height, constraining proportions and returns the new box
+ *
+ * @param integer $height
+ *
+ * @return BoxInterface
+ */
+ public function heighten($height);
+}
diff --git a/src/Symfony/Component/Image/Image/Fill/FillInterface.php b/src/Symfony/Component/Image/Image/Fill/FillInterface.php
new file mode 100644
index 0000000000000..2ec2d32c73df6
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Fill/FillInterface.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Fill;
+
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Interface for the fill
+ */
+interface FillInterface
+{
+ /**
+ * Gets color of the fill for the given position
+ *
+ * @param PointInterface $position
+ *
+ * @return ColorInterface
+ */
+ public function getColor(PointInterface $position);
+}
diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php
new file mode 100644
index 0000000000000..68bad65ff5921
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Fill\Gradient;
+
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Horizontal gradient fill
+ */
+final class Horizontal extends Linear
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getDistance(PointInterface $position)
+ {
+ return $position->getX();
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php
new file mode 100644
index 0000000000000..5335a16bc674e
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Fill\Gradient;
+
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Fill\FillInterface;
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Linear gradient fill
+ */
+abstract class Linear implements FillInterface
+{
+ /**
+ * @var integer
+ */
+ private $length;
+
+ /**
+ * @var ColorInterface
+ */
+ private $start;
+
+ /**
+ * @var ColorInterface
+ */
+ private $end;
+
+ /**
+ * Constructs a linear gradient with overall gradient length, and start and
+ * end shades, which default to 0 and 255 accordingly
+ *
+ * @param integer $length
+ * @param ColorInterface $start
+ * @param ColorInterface $end
+ */
+ final public function __construct($length, ColorInterface $start, ColorInterface $end)
+ {
+ $this->length = $length;
+ $this->start = $start;
+ $this->end = $end;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function getColor(PointInterface $position)
+ {
+ $l = $this->getDistance($position);
+
+ if ($l >= $this->length) {
+ return $this->end;
+ }
+
+ if ($l < 0) {
+ return $this->start;
+ }
+
+ return $this->start->getPalette()->blend($this->start, $this->end, $l / $this->length);
+ }
+
+ /**
+ * @return ColorInterface
+ */
+ final public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * @return ColorInterface
+ */
+ final public function getEnd()
+ {
+ return $this->end;
+ }
+
+ /**
+ * Get the distance of the position relative to the beginning of the gradient
+ *
+ * @param PointInterface $position
+ *
+ * @return integer
+ */
+ abstract protected function getDistance(PointInterface $position);
+}
diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php
new file mode 100644
index 0000000000000..cd422d04b3026
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Fill\Gradient;
+
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Vertical gradient fill
+ */
+final class Vertical extends Linear
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getDistance(PointInterface $position)
+ {
+ return $position->getY();
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/FontInterface.php b/src/Symfony/Component/Image/Image/FontInterface.php
new file mode 100644
index 0000000000000..3bb4ab41e2303
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/FontInterface.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+/**
+ * The font interface
+ */
+interface FontInterface
+{
+ /**
+ * Gets the fontfile for current font
+ *
+ * @return string
+ */
+ public function getFile();
+
+ /**
+ * Gets font's integer point size
+ *
+ * @return integer
+ */
+ public function getSize();
+
+ /**
+ * Gets font's color
+ *
+ * @return ColorInterface
+ */
+ public function getColor();
+
+ /**
+ * Gets BoxInterface of font size on the image based on string and angle
+ *
+ * @param string $string
+ * @param integer $angle
+ *
+ * @return BoxInterface
+ */
+ public function box($string, $angle = 0);
+}
diff --git a/src/Symfony/Component/Image/Image/Histogram/Bucket.php b/src/Symfony/Component/Image/Image/Histogram/Bucket.php
new file mode 100644
index 0000000000000..c5b63cb8fa657
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Histogram/Bucket.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Histogram;
+
+/**
+ * Bucket histogram
+ */
+final class Bucket implements \Countable
+{
+ /**
+ * @var Range
+ */
+ private $range;
+
+ /**
+ * @var integer
+ */
+ private $count;
+
+ /**
+ * @param Range $range
+ * @param integer $count
+ */
+ public function __construct(Range $range, $count = 0)
+ {
+ $this->range = $range;
+ $this->count = $count;
+ }
+
+ /**
+ * @param integer $value
+ */
+ public function add($value)
+ {
+ if ($this->range->contains($value)) {
+ $this->count++;
+ }
+ }
+
+ /**
+ * @return integer The number of elements in the bucket.
+ */
+ public function count()
+ {
+ return $this->count;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Histogram/Range.php b/src/Symfony/Component/Image/Image/Histogram/Range.php
new file mode 100644
index 0000000000000..ff28ce9786902
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Histogram/Range.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Histogram;
+
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+
+/**
+ * Range histogram
+ */
+final class Range
+{
+ /**
+ * @var integer
+ */
+ private $start;
+
+ /**
+ * @var integer
+ */
+ private $end;
+
+ /**
+ * @param integer $start
+ * @param integer $end
+ *
+ * @throws OutOfBoundsException
+ */
+ public function __construct($start, $end)
+ {
+ if ($end <= $start) {
+ throw new OutOfBoundsException(sprintf('Range end cannot be bigger than start, %d %d given accordingly', $this->start, $this->end));
+ }
+
+ $this->start = $start;
+ $this->end = $end;
+ }
+
+ /**
+ * @param integer $value
+ *
+ * @return Boolean
+ */
+ public function contains($value)
+ {
+ return $value >= $this->start && $value < $this->end;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/ImageInterface.php b/src/Symfony/Component/Image/Image/ImageInterface.php
new file mode 100644
index 0000000000000..562d47cdbd8b1
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/ImageInterface.php
@@ -0,0 +1,173 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Draw\DrawerInterface;
+use Symfony\Component\Image\Effects\EffectsInterface;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+
+/**
+ * The image interface
+ */
+interface ImageInterface extends ManipulatorInterface
+{
+ const RESOLUTION_PIXELSPERINCH = 'ppi';
+ const RESOLUTION_PIXELSPERCENTIMETER = 'ppc';
+
+ const INTERLACE_NONE = 'none';
+ const INTERLACE_LINE = 'line';
+ const INTERLACE_PLANE = 'plane';
+ const INTERLACE_PARTITION = 'partition';
+
+ const FILTER_UNDEFINED = 'undefined';
+ const FILTER_POINT = 'point';
+ const FILTER_BOX = 'box';
+ const FILTER_TRIANGLE = 'triangle';
+ const FILTER_HERMITE = 'hermite';
+ const FILTER_HANNING = 'hanning';
+ const FILTER_HAMMING = 'hamming';
+ const FILTER_BLACKMAN = 'blackman';
+ const FILTER_GAUSSIAN = 'gaussian';
+ const FILTER_QUADRATIC = 'quadratic';
+ const FILTER_CUBIC = 'cubic';
+ const FILTER_CATROM = 'catrom';
+ const FILTER_MITCHELL = 'mitchell';
+ const FILTER_LANCZOS = 'lanczos';
+ const FILTER_BESSEL = 'bessel';
+ const FILTER_SINC = 'sinc';
+
+ /**
+ * Returns the image content as a binary string
+ *
+ * @param string $format
+ * @param array $options
+ *
+ * @throws RuntimeException
+ *
+ * @return string binary
+ */
+ public function get($format, array $options = array());
+
+ /**
+ * Returns the image content as a PNG binary string
+ *
+ * @throws RuntimeException
+ *
+ * @return string binary
+ */
+ public function __toString();
+
+ /**
+ * Instantiates and returns a DrawerInterface instance for image drawing
+ *
+ * @return DrawerInterface
+ */
+ public function draw();
+
+ /**
+ * @return EffectsInterface
+ */
+ public function effects();
+
+ /**
+ * Returns current image size
+ *
+ * @return BoxInterface
+ */
+ public function getSize();
+
+ /**
+ * Transforms creates a grayscale mask from current image, returns a new
+ * image, while keeping the existing image unmodified
+ *
+ * @return ImageInterface
+ */
+ public function mask();
+
+ /**
+ * Returns array of image colors as Symfony\Component\Image\Image\Palette\Color\ColorInterface instances
+ *
+ * @return array
+ */
+ public function histogram();
+
+ /**
+ * Returns color at specified positions of current image
+ *
+ * @param PointInterface $point
+ *
+ * @throws RuntimeException
+ *
+ * @return ColorInterface
+ */
+ public function getColorAt(PointInterface $point);
+
+ /**
+ * Returns the image layers when applicable.
+ *
+ * @throws RuntimeException In case the layer can not be returned
+ * @throws OutOfBoundsException In case the index is not a valid value
+ *
+ * @return LayersInterface
+ */
+ public function layers();
+
+ /**
+ * Enables or disables interlacing
+ *
+ * @param string $scheme
+ *
+ * @throws InvalidArgumentException When an unsupported Interface type is supplied
+ *
+ * @return ImageInterface
+ */
+ public function interlace($scheme);
+
+ /**
+ * Return the current color palette
+ *
+ * @return PaletteInterface
+ */
+ public function palette();
+
+ /**
+ * Set a palette for the image. Useful to change colorspace.
+ *
+ * @param PaletteInterface $palette
+ *
+ * @return ImageInterface
+ *
+ * @throws RuntimeException
+ */
+ public function usePalette(PaletteInterface $palette);
+
+ /**
+ * Applies a color profile on the Image
+ *
+ * @param ProfileInterface $profile
+ *
+ * @return ImageInterface
+ *
+ * @throws RuntimeException
+ */
+ public function profile(ProfileInterface $profile);
+
+ /**
+ * Returns the Image's meta data
+ *
+ * @return Metadata\MetadataBag
+ */
+ public function metadata();
+}
diff --git a/src/Symfony/Component/Image/Image/LayersInterface.php b/src/Symfony/Component/Image/Image/LayersInterface.php
new file mode 100644
index 0000000000000..991d86adbe631
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/LayersInterface.php
@@ -0,0 +1,107 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+
+/**
+ * The layers interface
+ */
+interface LayersInterface extends \Iterator, \Countable, \ArrayAccess
+{
+ /**
+ * Merge layers into the original objects
+ *
+ * @throws RuntimeException
+ */
+ public function merge();
+
+ /**
+ * Animates layers
+ *
+ * @param string $format The output output format
+ * @param integer $delay The delay in milliseconds between two frames
+ * @param integer $loops The number of loops, 0 means infinite
+ *
+ * @return LayersInterface
+ *
+ * @throws InvalidArgumentException In case an invalid argument is provided
+ * @throws RuntimeException In case the driver fails to animate
+ */
+ public function animate($format, $delay, $loops);
+
+ /**
+ * Coalesce layers. Each layer in the sequence is the same size as the first and composited with the next layer in
+ * the sequence.
+ */
+ public function coalesce();
+
+ /**
+ * Adds an image at the end of the layers stack
+ *
+ * @param ImageInterface $image
+ *
+ * @return LayersInterface
+ *
+ * @throws RuntimeException
+ */
+ public function add(ImageInterface $image);
+
+ /**
+ * Set an image at offset
+ *
+ * @param integer $offset
+ * @param ImageInterface $image
+ *
+ * @return LayersInterface
+ *
+ * @throws RuntimeException
+ * @throws InvalidArgumentException
+ * @throws OutOfBoundsException
+ */
+ public function set($offset, ImageInterface $image);
+
+ /**
+ * Removes the image at offset
+ *
+ * @param integer $offset
+ *
+ * @return LayersInterface
+ *
+ * @throws RuntimeException
+ * @throws InvalidArgumentException
+ */
+ public function remove($offset);
+
+ /**
+ * Returns the image at offset
+ *
+ * @param integer $offset
+ *
+ * @return ImageInterface
+ *
+ * @throws RuntimeException
+ * @throws InvalidArgumentException
+ */
+ public function get($offset);
+
+ /**
+ * Returns true if a layer at offset is preset
+ *
+ * @param integer $offset
+ *
+ * @return Boolean
+ */
+ public function has($offset);
+}
diff --git a/src/Symfony/Component/Image/Image/LoaderInterface.php b/src/Symfony/Component/Image/Image/LoaderInterface.php
new file mode 100644
index 0000000000000..54009a1483658
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/LoaderInterface.php
@@ -0,0 +1,83 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+/**
+ * The imagine interface
+ */
+interface LoaderInterface
+{
+ const VERSION = '0.7-dev';
+
+ /**
+ * Creates a new empty image with an optional background color
+ *
+ * @param BoxInterface $size
+ * @param ColorInterface $color
+ *
+ * @throws InvalidArgumentException
+ * @throws RuntimeException
+ *
+ * @return ImageInterface
+ */
+ public function create(BoxInterface $size, ColorInterface $color = null);
+
+ /**
+ * Opens an existing image from $path
+ *
+ * @param string $path
+ *
+ * @throws RuntimeException
+ *
+ * @return ImageInterface
+ */
+ public function open($path);
+
+ /**
+ * Loads an image from a binary $string
+ *
+ * @param string $string
+ *
+ * @throws RuntimeException
+ *
+ * @return ImageInterface
+ */
+ public function load($string);
+
+ /**
+ * Loads an image from a resource $resource
+ *
+ * @param resource $resource
+ *
+ * @throws RuntimeException
+ *
+ * @return ImageInterface
+ */
+ public function read($resource);
+
+ /**
+ * Constructs a font with specified $file, $size and $color
+ *
+ * The font size is to be specified in points (e.g. 10pt means 10)
+ *
+ * @param string $file
+ * @param integer $size
+ * @param ColorInterface $color
+ *
+ * @return FontInterface
+ */
+ public function font($file, $size, ColorInterface $color);
+}
diff --git a/src/Symfony/Component/Image/Image/ManipulatorInterface.php b/src/Symfony/Component/Image/Image/ManipulatorInterface.php
new file mode 100644
index 0000000000000..2510222877222
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/ManipulatorInterface.php
@@ -0,0 +1,181 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Fill\FillInterface;
+
+/**
+ * The manipulator interface
+ */
+interface ManipulatorInterface
+{
+ const THUMBNAIL_INSET = 'inset';
+ const THUMBNAIL_OUTBOUND = 'outbound';
+
+ /**
+ * Copies current source image into a new ImageInterface instance
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function copy();
+
+ /**
+ * Crops a specified box out of the source image (modifies the source image)
+ * Returns cropped self
+ *
+ * @param PointInterface $start
+ * @param BoxInterface $size
+ *
+ * @throws OutOfBoundsException
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function crop(PointInterface $start, BoxInterface $size);
+
+ /**
+ * Resizes current image and returns self
+ *
+ * @param BoxInterface $size
+ * @param string $filter
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED);
+
+ /**
+ * Rotates an image at the given angle.
+ * Optional $background can be used to specify the fill color of the empty
+ * area of rotated image.
+ *
+ * @param integer $angle
+ * @param ColorInterface $background
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function rotate($angle, ColorInterface $background = null);
+
+ /**
+ * Pastes an image into a parent image
+ * Throws exceptions if image exceeds parent image borders or if paste
+ * operation fails
+ *
+ * Returns source image
+ *
+ * @param ImageInterface $image
+ * @param PointInterface $start
+ *
+ * @throws InvalidArgumentException
+ * @throws OutOfBoundsException
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function paste(ImageInterface $image, PointInterface $start);
+
+ /**
+ * Saves the image at a specified path, the target file extension is used
+ * to determine file format, only jpg, jpeg, gif, png, wbmp and xbm are
+ * supported
+ *
+ * @param string $path
+ * @param array $options
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function save($path = null, array $options = array());
+
+ /**
+ * Outputs the image content
+ *
+ * @param string $format
+ * @param array $options
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function show($format, array $options = array());
+
+ /**
+ * Flips current image using horizontal axis
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function flipHorizontally();
+
+ /**
+ * Flips current image using vertical axis
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function flipVertically();
+
+ /**
+ * Remove all profiles and comments
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function strip();
+
+ /**
+ * Generates a thumbnail from a current image
+ * Returns it as a new image, doesn't modify the current image
+ *
+ * @param BoxInterface $size
+ * @param string $mode
+ * @param string $filter The filter to use for resizing, one of ImageInterface::FILTER_*
+ *
+ * @throws RuntimeException
+ *
+ * @return static
+ */
+ public function thumbnail(BoxInterface $size, $mode = self::THUMBNAIL_INSET, $filter = ImageInterface::FILTER_UNDEFINED);
+
+ /**
+ * Applies a given mask to current image's alpha channel
+ *
+ * @param ImageInterface $mask
+ *
+ * @return static
+ */
+ public function applyMask(ImageInterface $mask);
+
+ /**
+ * Fills image with provided filling, by replacing each pixel's color in
+ * the current image with corresponding color from FillInterface, and
+ * returns modified image
+ *
+ * @param FillInterface $fill
+ *
+ * @return static
+ */
+ public function fill(FillInterface $fill);
+}
diff --git a/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php
new file mode 100644
index 0000000000000..0c35323a48983
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Metadata;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+abstract class AbstractMetadataReader implements MetadataReaderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function readFile($file)
+ {
+ if (stream_is_local($file)) {
+ if (!is_file($file)) {
+ throw new InvalidArgumentException(sprintf('File %s does not exist.', $file));
+ }
+
+ return new MetadataBag(array_merge(array('filepath' => realpath($file), 'uri' => $file), $this->extractFromFile($file)));
+ }
+
+ return new MetadataBag(array_merge(array('uri' => $file), $this->extractFromFile($file)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readData($data, $originalResource = null)
+ {
+ if (null !== $originalResource) {
+ return new MetadataBag(array_merge($this->getStreamMetadata($originalResource), $this->extractFromData($data)));
+ }
+
+ return new MetadataBag($this->extractFromData($data));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readStream($resource)
+ {
+ if (!is_resource($resource)) {
+ throw new InvalidArgumentException('Invalid resource provided.');
+ }
+
+ return new MetadataBag(array_merge($this->getStreamMetadata($resource), $this->extractFromStream($resource)));
+ }
+
+ /**
+ * Gets the URI from a stream resource
+ *
+ * @param resource $resource
+ *
+ * @return string|null The URI f ava
+ */
+ private function getStreamMetadata($resource)
+ {
+ $metadata = array();
+
+ if (false !== $data = @stream_get_meta_data($resource)) {
+ $metadata['uri'] = $data['uri'];
+ if (stream_is_local($resource)) {
+ $metadata['filepath'] = realpath($data['uri']);
+ }
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * Extracts metadata from a file
+ *
+ * @param $file
+ *
+ * @return array An associative array of metadata
+ */
+ abstract protected function extractFromFile($file);
+
+ /**
+ * Extracts metadata from raw data
+ *
+ * @param $data
+ *
+ * @return array An associative array of metadata
+ */
+ abstract protected function extractFromData($data);
+
+ /**
+ * Extracts metadata from a stream
+ *
+ * @param $resource
+ *
+ * @return array An associative array of metadata
+ */
+ abstract protected function extractFromStream($resource);
+}
diff --git a/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php
new file mode 100644
index 0000000000000..c8f23833e8f10
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Metadata;
+
+/**
+ * Default metadata reader
+ */
+class DefaultMetadataReader extends AbstractMetadataReader
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractFromFile($file)
+ {
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractFromData($data)
+ {
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractFromStream($resource)
+ {
+ return array();
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php
new file mode 100644
index 0000000000000..7dc1881d5b6e4
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php
@@ -0,0 +1,115 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Metadata;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\NotSupportedException;
+
+/**
+ * Metadata driven by Exif information
+ */
+class ExifMetadataReader extends AbstractMetadataReader
+{
+ public function __construct()
+ {
+ if (!self::isSupported()) {
+ throw new NotSupportedException('PHP exif extension is required to use the ExifMetadataReader');
+ }
+ }
+
+ public static function isSupported()
+ {
+ return function_exists('exif_read_data');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractFromFile($file)
+ {
+ if (stream_is_local($file)) {
+ if (false === is_readable($file)) {
+ throw new InvalidArgumentException(sprintf('File %s is not readable.', $file));
+ }
+
+ return $this->extract($file);
+ }
+
+ if (false === $data = @file_get_contents($file)) {
+ throw new InvalidArgumentException(sprintf('File %s is not readable.', $file));
+ }
+
+ return $this->doReadData($data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractFromData($data)
+ {
+ return $this->doReadData($data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractFromStream($resource)
+ {
+ return $this->doReadData(stream_get_contents($resource));
+ }
+
+ /**
+ * Extracts metadata from raw data, merges with existing metadata
+ *
+ * @param string $data
+ *
+ * @return MetadataBag
+ */
+ private function doReadData($data)
+ {
+ if (substr($data, 0, 2) === 'II') {
+ $mime = 'image/tiff';
+ } else {
+ $mime = 'image/jpeg';
+ }
+
+ return $this->extract('data://' . $mime . ';base64,' . base64_encode($data));
+ }
+
+ /**
+ * Performs the exif data extraction given a path or data-URI representation.
+ *
+ * @param string $path The path to the file or the data-URI representation.
+ *
+ * @return MetadataBag
+ */
+ private function extract($path)
+ {
+ if (false === $exifData = @exif_read_data($path, null, true, false)) {
+ return array();
+ }
+
+ $metadata = array();
+ $sources = array('EXIF' => 'exif', 'IFD0' => 'ifd0');
+
+ foreach ($sources as $name => $prefix) {
+ if (!isset($exifData[$name])) {
+ continue;
+ }
+ foreach ($exifData[$name] as $prop => $value) {
+ $metadata[$prefix.'.'.$prop] = $value;
+ }
+ }
+
+ return $metadata;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php b/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php
new file mode 100644
index 0000000000000..91087a03249ab
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php
@@ -0,0 +1,97 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Metadata;
+
+/**
+ * An interface for Image Metadata
+ */
+class MetadataBag implements \ArrayAccess, \IteratorAggregate, \Countable
+{
+ /** @var array */
+ private $data;
+
+ public function __construct(array $data = array())
+ {
+ $this->data = $data;
+ }
+
+ /**
+ * Returns the metadata key, default value if it does not exist
+ *
+ * @param string $key
+ * @param mixed|null $default
+ *
+ * @return mixed
+ */
+ public function get($key, $default = null)
+ {
+ return array_key_exists($key, $this->data) ? $this->data[$key] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ return count($this->data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetExists($offset)
+ {
+ return array_key_exists($offset, $this->data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetSet($offset, $value)
+ {
+ $this->data[$offset] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetUnset($offset)
+ {
+ unset($this->data[$offset]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetGet($offset)
+ {
+ return $this->get($offset);
+ }
+
+ /**
+ * Returns metadata as an array
+ *
+ * @return array An associative array
+ */
+ public function toArray()
+ {
+ return $this->data;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php b/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php
new file mode 100644
index 0000000000000..110b809e06ebf
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Metadata;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+interface MetadataReaderInterface
+{
+ /**
+ * Reads metadata from a file.
+ *
+ * @param $file The path to the file where to read metadata.
+ *
+ * @throws InvalidArgumentException In case the file does not exist.
+ *
+ * @return MetadataBag
+ */
+ public function readFile($file);
+
+ /**
+ * Reads metadata from a binary string.
+ *
+ * @param $data The binary string to read.
+ * @param $originalResource An optional resource to gather stream metadata.
+ *
+ * @return MetadataBag
+ */
+ public function readData($data, $originalResource = null);
+
+ /**
+ * Reads metadata from a stream.
+ *
+ * @param $resource The stream to read.
+ *
+ * @throws InvalidArgumentException In case the resource is not valid.
+ *
+ * @return MetadataBag
+ */
+ public function readStream($resource);
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/CMYK.php b/src/Symfony/Component/Image/Image/Palette/CMYK.php
new file mode 100644
index 0000000000000..da49bb0a45ad4
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/CMYK.php
@@ -0,0 +1,118 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette;
+
+use Symfony\Component\Image\Image\Palette\Color\CMYK as CMYKColor;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Image\Profile;
+use Symfony\Component\Image\Image\ProfileInterface;
+
+class CMYK implements PaletteInterface
+{
+ private $parser;
+ private $profile;
+ private static $colors = array();
+
+ public function __construct()
+ {
+ $this->parser = new ColorParser();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function name()
+ {
+ return PaletteInterface::PALETTE_CMYK;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pixelDefinition()
+ {
+ return array(
+ ColorInterface::COLOR_CYAN,
+ ColorInterface::COLOR_MAGENTA,
+ ColorInterface::COLOR_YELLOW,
+ ColorInterface::COLOR_KEYLINE,
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsAlpha()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function color($color, $alpha = null)
+ {
+ if (null !== $alpha) {
+ throw new InvalidArgumentException('CMYK palette does not support alpha');
+ }
+
+ $color = $this->parser->parseToCMYK($color);
+ $index = sprintf('cmyk(%d, %d, %d, %d)', $color[0], $color[1], $color[2], $color[3]);
+
+ if (false === array_key_exists($index, self::$colors)) {
+ self::$colors[$index] = new CMYKColor($this, $color);
+ }
+
+ return self::$colors[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blend(ColorInterface $color1, ColorInterface $color2, $amount)
+ {
+ if (!$color1 instanceof CMYKColor || ! $color2 instanceof CMYKColor) {
+ throw new RuntimeException('CMYK palette can only blend CMYK colors');
+ }
+
+ return $this->color(array(
+ min(100, $color1->getCyan() + $color2->getCyan() * $amount),
+ min(100, $color1->getMagenta() + $color2->getMagenta() * $amount),
+ min(100, $color1->getYellow() + $color2->getYellow() * $amount),
+ min(100, $color1->getKeyline() + $color2->getKeyline() * $amount),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function useProfile(ProfileInterface $profile)
+ {
+ $this->profile = $profile;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function profile()
+ {
+ if (!$this->profile) {
+ $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/Adobe/CMYK/USWebUncoated.icc');
+ }
+
+ return $this->profile;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php b/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php
new file mode 100644
index 0000000000000..366cb46f66803
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php
@@ -0,0 +1,219 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\CMYK as CMYKPalette;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+final class CMYK implements ColorInterface
+{
+ /**
+ * @var integer
+ */
+ private $c;
+
+ /**
+ * @var integer
+ */
+ private $m;
+
+ /**
+ * @var integer
+ */
+ private $y;
+
+ /**
+ * @var integer
+ */
+ private $k;
+
+ /**
+ *
+ * @var CMYK
+ */
+ private $palette;
+
+ public function __construct(CMYKPalette $palette, array $color)
+ {
+ $this->palette = $palette;
+ $this->setColor($color);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue($component)
+ {
+ switch ($component) {
+ case ColorInterface::COLOR_CYAN:
+ return $this->getCyan();
+ case ColorInterface::COLOR_MAGENTA:
+ return $this->getMagenta();
+ case ColorInterface::COLOR_YELLOW:
+ return $this->getYellow();
+ case ColorInterface::COLOR_KEYLINE:
+ return $this->getKeyline();
+ default:
+ throw new InvalidArgumentException(sprintf('Color component %s is not valid', $component));
+ }
+ }
+
+ /**
+ * Returns Cyan value of the color
+ *
+ * @return integer
+ */
+ public function getCyan()
+ {
+ return $this->c;
+ }
+
+ /**
+ * Returns Magenta value of the color
+ *
+ * @return integer
+ */
+ public function getMagenta()
+ {
+ return $this->m;
+ }
+
+ /**
+ * Returns Yellow value of the color
+ *
+ * @return integer
+ */
+ public function getYellow()
+ {
+ return $this->y;
+ }
+
+ /**
+ * Returns Key value of the color
+ *
+ * @return integer
+ */
+ public function getKeyline()
+ {
+ return $this->k;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPalette()
+ {
+ return $this->palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAlpha()
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dissolve($alpha)
+ {
+ throw new RuntimeException('CMYK does not support dissolution');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function lighten($shade)
+ {
+ return $this->palette->color(
+ array(
+ $this->c,
+ $this->m,
+ $this->y,
+ max(0, $this->k - $shade),
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function darken($shade)
+ {
+ return $this->palette->color(
+ array(
+ $this->c,
+ $this->m,
+ $this->y,
+ min(100, $this->k + $shade),
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function grayscale()
+ {
+ $color = array(
+ $this->c * (1 - $this->k / 100) + $this->k,
+ $this->m * (1 - $this->k / 100) + $this->k,
+ $this->y * (1 - $this->k / 100) + $this->k,
+ );
+
+ $gray = min(100, round(0.299 * $color[0] + 0.587 * $color[1] + 0.114 * $color[2]));
+
+ return $this->palette->color(array($gray, $gray, $gray, $this->k));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isOpaque()
+ {
+ return true;
+ }
+
+ /**
+ * Returns hex representation of the color
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return sprintf('cmyk(%d%%, %d%%, %d%%, %d%%)', $this->c, $this->m, $this->y, $this->k);
+ }
+
+ /**
+ * Internal, Performs checks for color validity (an of array(C, M, Y, K))
+ *
+ * @param array $color
+ *
+ * @throws InvalidArgumentException
+ */
+ private function setColor(array $color)
+ {
+ if (count($color) !== 4) {
+ throw new InvalidArgumentException('Color argument must look like array(C, M, Y, K), where C, M, Y, K are the integer values between 0 and 255 for cyan, magenta, yellow and black color indexes accordingly');
+ }
+
+ $colors = array_values($color);
+ array_walk($colors, function ($color) {
+ return max(0, min(100, $color));
+ });
+
+ list($this->c, $this->m, $this->y, $this->k) = $colors;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php b/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php
new file mode 100644
index 0000000000000..47c128984432e
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+
+interface ColorInterface
+{
+ const COLOR_RED = 'red';
+ const COLOR_GREEN = 'green';
+ const COLOR_BLUE = 'blue';
+
+ const COLOR_CYAN = 'cyan';
+ const COLOR_MAGENTA = 'magenta';
+ const COLOR_YELLOW = 'yellow';
+ const COLOR_KEYLINE = 'keyline';
+
+ const COLOR_GRAY = 'gray';
+
+ /**
+ * Return the value of one of the component.
+ *
+ * @param string $component One of the ColorInterface::COLOR_* component
+ *
+ * @return Integer
+ */
+ public function getValue($component);
+
+ /**
+ * Returns percentage of transparency of the color
+ *
+ * @return integer
+ */
+ public function getAlpha();
+
+ /**
+ * Returns the palette attached to the current color
+ *
+ * @return PaletteInterface
+ */
+ public function getPalette();
+
+ /**
+ * Returns a copy of current color, incrementing the alpha channel by the
+ * given amount
+ *
+ * @param integer $alpha
+ *
+ * @return ColorInterface
+ */
+ public function dissolve($alpha);
+
+ /**
+ * Returns a copy of the current color, lightened by the specified number
+ * of shades
+ *
+ * @param integer $shade
+ *
+ * @return ColorInterface
+ */
+ public function lighten($shade);
+
+ /**
+ * Returns a copy of the current color, darkened by the specified number of
+ * shades
+ *
+ * @param integer $shade
+ *
+ * @return ColorInterface
+ */
+ public function darken($shade);
+
+ /**
+ * Returns a gray related to the current color
+ *
+ * @return ColorInterface
+ */
+ public function grayscale();
+
+ /**
+ * Checks if the current color is opaque
+ *
+ * @return Boolean
+ */
+ public function isOpaque();
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/Color/Gray.php b/src/Symfony/Component/Image/Image/Palette/Color/Gray.php
new file mode 100644
index 0000000000000..c0be1212c5d2a
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/Color/Gray.php
@@ -0,0 +1,164 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\Grayscale;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+final class Gray implements ColorInterface
+{
+ /**
+ * @var integer
+ */
+ private $gray;
+
+ /**
+ * @var integer
+ */
+ private $alpha;
+
+ /**
+ *
+ * @var Grayscale
+ */
+ private $palette;
+
+ public function __construct(Grayscale $palette, array $color, $alpha)
+ {
+ $this->palette = $palette;
+ $this->setColor($color);
+ $this->setAlpha($alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue($component)
+ {
+ switch ($component) {
+ case ColorInterface::COLOR_GRAY:
+ return $this->getGray();
+ default:
+ throw new InvalidArgumentException(sprintf('Color component %s is not valid', $component));
+ }
+ }
+
+ /**
+ * Returns Gray value of the color
+ *
+ * @return integer
+ */
+ public function getGray()
+ {
+ return $this->gray;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPalette()
+ {
+ return $this->palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAlpha()
+ {
+ return $this->alpha;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dissolve($alpha)
+ {
+ return $this->palette->color(
+ array($this->gray), $this->alpha + $alpha
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function lighten($shade)
+ {
+ return $this->palette->color(array(min(255, $this->gray + $shade)), $this->alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function darken($shade)
+ {
+ return $this->palette->color(array(max(0, $this->gray - $shade)), $this->alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function grayscale()
+ {
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isOpaque()
+ {
+ return 100 === $this->alpha;
+ }
+
+ /**
+ * Returns hex representation of the color
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return sprintf('#%02x%02x%02x', $this->gray, $this->gray, $this->gray);
+ }
+
+ /**
+ * Performs checks for validity of given alpha value and sets it
+ *
+ * @param integer $alpha
+ *
+ * @throws InvalidArgumentException
+ */
+ private function setAlpha($alpha)
+ {
+ if (!is_int($alpha) || $alpha < 0 || $alpha > 100) {
+ throw new InvalidArgumentException(sprintf('Alpha must be an integer between 0 and 100, %s given', $alpha));
+ }
+
+ $this->alpha = $alpha;
+ }
+
+ /**
+ * Performs checks for color validity (array of array(gray))
+ *
+ * @param array $color
+ *
+ * @throws InvalidArgumentException
+ */
+ private function setColor(array $color)
+ {
+ if (count($color) !== 1) {
+ throw new InvalidArgumentException('Color argument must look like array(gray), where gray is the integer value between 0 and 255 for the grayscale');
+ }
+
+ list($this->gray) = array_values($color);
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/Color/RGB.php b/src/Symfony/Component/Image/Image/Palette/Color/RGB.php
new file mode 100644
index 0000000000000..7cf5e16569826
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/Color/RGB.php
@@ -0,0 +1,214 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\RGB as RGBPalette;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+final class RGB implements ColorInterface
+{
+ /**
+ * @var integer
+ */
+ private $r;
+
+ /**
+ * @var integer
+ */
+ private $g;
+
+ /**
+ * @var integer
+ */
+ private $b;
+
+ /**
+ * @var integer
+ */
+ private $alpha;
+
+ /**
+ *
+ * @var RGBPalette
+ */
+ private $palette;
+
+ public function __construct(RGBPalette $palette, array $color, $alpha)
+ {
+ $this->palette = $palette;
+ $this->setColor($color);
+ $this->setAlpha($alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue($component)
+ {
+ switch ($component) {
+ case ColorInterface::COLOR_RED:
+ return $this->getRed();
+ case ColorInterface::COLOR_GREEN:
+ return $this->getGreen();
+ case ColorInterface::COLOR_BLUE:
+ return $this->getBlue();
+ default:
+ throw new InvalidArgumentException(sprintf('Color component %s is not valid', $component));
+ }
+ }
+
+ /**
+ * Returns RED value of the color
+ *
+ * @return integer
+ */
+ public function getRed()
+ {
+ return $this->r;
+ }
+
+ /**
+ * Returns GREEN value of the color
+ *
+ * @return integer
+ */
+ public function getGreen()
+ {
+ return $this->g;
+ }
+
+ /**
+ * Returns BLUE value of the color
+ *
+ * @return integer
+ */
+ public function getBlue()
+ {
+ return $this->b;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPalette()
+ {
+ return $this->palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAlpha()
+ {
+ return $this->alpha;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dissolve($alpha)
+ {
+ return $this->palette->color(array($this->r, $this->g, $this->b), $this->alpha + $alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function lighten($shade)
+ {
+ return $this->palette->color(
+ array(
+ min(255, $this->r + $shade),
+ min(255, $this->g + $shade),
+ min(255, $this->b + $shade),
+ ), $this->alpha
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function darken($shade)
+ {
+ return $this->palette->color(
+ array(
+ max(0, $this->r - $shade),
+ max(0, $this->g - $shade),
+ max(0, $this->b - $shade),
+ ), $this->alpha
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function grayscale()
+ {
+ $gray = min(255, round(0.299 * $this->getRed() + 0.114 * $this->getBlue() + 0.587 * $this->getGreen()));
+
+ return $this->palette->color(array($gray, $gray, $gray), $this->alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isOpaque()
+ {
+ return 100 === $this->alpha;
+ }
+
+ /**
+ * Returns hex representation of the color
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return sprintf('#%02x%02x%02x', $this->r, $this->g, $this->b);
+ }
+
+ /**
+ * Internal
+ *
+ * Performs checks for validity of given alpha value and sets it
+ *
+ * @param integer $alpha
+ *
+ * @throws InvalidArgumentException
+ */
+ private function setAlpha($alpha)
+ {
+ if (!is_int($alpha) || $alpha < 0 || $alpha > 100) {
+ throw new InvalidArgumentException(sprintf('Alpha must be an integer between 0 and 100, %s given', $alpha));
+ }
+
+ $this->alpha = $alpha;
+ }
+
+ /**
+ * Internal
+ *
+ * Performs checks for color validity (array of array(R, G, B))
+ *
+ * @param array $color
+ *
+ * @throws InvalidArgumentException
+ */
+ private function setColor(array $color)
+ {
+ if (count($color) !== 3) {
+ throw new InvalidArgumentException('Color argument must look like array(R, G, B), where R, G, B are the integer values between 0 and 255 for red, green and blue color indexes accordingly');
+ }
+
+ list($this->r, $this->g, $this->b) = array_values($color);
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/ColorParser.php b/src/Symfony/Component/Image/Image/Palette/ColorParser.php
new file mode 100644
index 0000000000000..e63ad588e4c97
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/ColorParser.php
@@ -0,0 +1,153 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+class ColorParser
+{
+ /**
+ * Parses a color to a RGB tuple
+ *
+ * @param string|array|integer $color
+ *
+ * @return array
+ *
+ * @throws InvalidArgumentException
+ */
+ public function parseToRGB($color)
+ {
+ $color = $this->parse($color);
+
+ if (4 === count($color)) {
+ $color = array(
+ 255 * (1 - $color[0] / 100) * (1 - $color[3] / 100),
+ 255 * (1 - $color[1] / 100) * (1 - $color[3] / 100),
+ 255 * (1 - $color[2] / 100) * (1 - $color[3] / 100),
+ );
+ }
+
+ return $color;
+ }
+
+ /**
+ * Parses a color to a CMYK tuple
+ *
+ * @param string|array|integer $color
+ *
+ * @return array
+ *
+ * @throws InvalidArgumentException
+ */
+ public function parseToCMYK($color)
+ {
+ $color = $this->parse($color);
+
+ if (3 === count($color)) {
+ $r = $color[0] / 255;
+ $g = $color[1] / 255;
+ $b = $color[2] / 255;
+
+ $k = 1 - max($r, $g, $b);
+
+ $color = array(
+ 1 === $k ? 0 : round((1 - $r - $k) / (1- $k) * 100),
+ 1 === $k ? 0 : round((1 - $g - $k) / (1- $k) * 100),
+ 1 === $k ? 0 : round((1 - $b - $k) / (1- $k) * 100),
+ round($k * 100)
+ );
+ }
+
+ return $color;
+ }
+
+ /**
+ * Parses a color to a grayscale value
+ *
+ * @param string|array|integer $color
+ *
+ * @return array
+ *
+ * @throws InvalidArgumentException
+ */
+ public function parseToGrayscale($color)
+ {
+ if (is_array($color) && 1 === count($color)) {
+ return array_values($color);
+ }
+
+ $color = array_unique($this->parse($color));
+
+ if (1 !== count($color)) {
+ throw new InvalidArgumentException('The provided color has different values of red, green and blue components. Grayscale colors must have the same values for these.');
+ }
+
+ return $color;
+ }
+
+ /**
+ * Parses a color
+ *
+ * @param string|array|integer $color
+ *
+ * @return array
+ *
+ * @throws InvalidArgumentException
+ */
+ private function parse($color)
+ {
+ if (!is_string($color) && !is_array($color) && !is_int($color)) {
+ throw new InvalidArgumentException(sprintf('Color must be specified as a hexadecimal string, array or integer, %s given', gettype($color)));
+ }
+
+ if (is_array($color)) {
+ if (3 === count($color) || 4 === count($color)) {
+ return array_values($color);
+ }
+ throw new InvalidArgumentException('Color argument if array, must look like array(R, G, B), or array(C, M, Y, K) where R, G, B are the integer values between 0 and 255 for red, green and blue or cyan, magenta, yellow and black color indexes accordingly');
+ }
+
+ if (is_string($color)) {
+ if (0 === strpos($color, 'cmyk(')) {
+ $substrColor = substr($color, 5, strlen($color) - 6);
+
+ $components = array_map(function ($component) {
+ return round(trim($component, ' %'));
+ }, explode(',', $substrColor));
+
+ if (count($components) !== 4) {
+ throw new InvalidArgumentException(sprintf('Unable to parse color %s', $color));
+ }
+
+ return $components;
+ } else {
+ $color = ltrim($color, '#');
+
+ if (strlen($color) !== 3 && strlen($color) !== 6) {
+ throw new InvalidArgumentException(sprintf('Color must be a hex value in regular (6 characters) or short (3 characters) notation, "%s" given', $color));
+ }
+
+ if (strlen($color) === 3) {
+ $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
+ }
+
+ $color = array_map('hexdec', str_split($color, 2));
+ }
+ }
+
+ if (is_int($color)) {
+ $color = array(255 & ($color >> 16), 255 & ($color >> 8), 255 & $color);
+ }
+
+ return $color;
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/Grayscale.php b/src/Symfony/Component/Image/Image/Palette/Grayscale.php
new file mode 100644
index 0000000000000..01f34eaba02e0
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/Grayscale.php
@@ -0,0 +1,123 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette;
+
+use Symfony\Component\Image\Image\Palette\Color\Gray as GrayColor;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\ProfileInterface;
+use Symfony\Component\Image\Image\Profile;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+class Grayscale implements PaletteInterface
+{
+ /**
+ * @var ColorParser
+ */
+ private $parser;
+
+ /**
+ * @var ProfileInterface
+ */
+ private $profile;
+
+ /**
+ * @var array
+ */
+ protected static $colors = array();
+
+ public function __construct()
+ {
+ $this->parser = new ColorParser();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function name()
+ {
+ return PaletteInterface::PALETTE_GRAYSCALE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pixelDefinition()
+ {
+ return array(ColorInterface::COLOR_GRAY);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsAlpha()
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function useProfile(ProfileInterface $profile)
+ {
+ $this->profile = $profile;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function profile()
+ {
+ if (!$this->profile) {
+ $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC');
+ }
+
+ return $this->profile;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function color($color, $alpha = null)
+ {
+ if (null === $alpha) {
+ $alpha = 0;
+ }
+
+ $color = $this->parser->parseToGrayscale($color);
+ $index = sprintf('#%02x%02x%02x-%d', $color[0], $color[0], $color[0], $alpha);
+
+ if (false === array_key_exists($index, static::$colors)) {
+ static::$colors[$index] = new GrayColor($this, $color, $alpha);
+ }
+
+ return static::$colors[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blend(ColorInterface $color1, ColorInterface $color2, $amount)
+ {
+ if (!$color1 instanceof GrayColor || ! $color2 instanceof GrayColor) {
+ throw new RuntimeException('Grayscale palette can only blend Grayscale colors');
+ }
+
+ return $this->color(
+ array(
+ (int) min(255, min($color1->getGray(), $color2->getGray()) + round(abs($color2->getGray() - $color1->getGray()) * $amount)),
+ ),
+ (int) min(100, min($color1->getAlpha(), $color2->getAlpha()) + round(abs($color2->getAlpha() - $color1->getAlpha()) * $amount))
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php b/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php
new file mode 100644
index 0000000000000..04d8aae3f8ba1
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette;
+
+use Symfony\Component\Image\Image\ProfileInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+interface PaletteInterface
+{
+ const PALETTE_GRAYSCALE = 'gray';
+ const PALETTE_RGB = 'rgb';
+ const PALETTE_CMYK = 'cmyk';
+
+ /**
+ * Returns a color given some values
+ *
+ * @param string|array|integer $color A color
+ * @param integer|null $alpha Set alpha to null to disable it
+ *
+ * @return ColorInterface
+ *
+ * @throws InvalidArgumentException In case you pass an alpha value to a
+ * Palette that does not support alpha
+ */
+ public function color($color, $alpha = null);
+
+ /**
+ * Blend two colors given an amount
+ *
+ * @param ColorInterface $color1
+ * @param ColorInterface $color2
+ * @param float $amount The amount of color2 in color1
+ *
+ * @return ColorInterface
+ */
+ public function blend(ColorInterface $color1, ColorInterface $color2, $amount);
+
+ /**
+ * Attachs an ICC profile to this Palette.
+ *
+ * (A default profile is provided by default)
+ *
+ * @param ProfileInterface $profile
+ *
+ * @return PaletteInterface
+ */
+ public function useProfile(ProfileInterface $profile);
+
+ /**
+ * Returns the ICC profile attached to this Palette.
+ *
+ * @return ProfileInterface
+ */
+ public function profile();
+
+ /**
+ * Returns the name of this Palette, one of PaletteInterface::PALETTE_*
+ * constants
+ *
+ * @return String
+ */
+ public function name();
+
+ /**
+ * Returns an array containing ColorInterface::COLOR_* constants that
+ * define the structure of colors for a pixel.
+ *
+ * @return array
+ */
+ public function pixelDefinition();
+
+ /**
+ * Tells if alpha channel is supported in this palette
+ *
+ * @return Boolean
+ */
+ public function supportsAlpha();
+}
diff --git a/src/Symfony/Component/Image/Image/Palette/RGB.php b/src/Symfony/Component/Image/Image/Palette/RGB.php
new file mode 100644
index 0000000000000..639aaa27d74c4
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Palette/RGB.php
@@ -0,0 +1,129 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Palette;
+
+use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\ProfileInterface;
+use Symfony\Component\Image\Image\Profile;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+class RGB implements PaletteInterface
+{
+ /**
+ * @var ColorParser
+ */
+ private $parser;
+
+ /**
+ * @var ProfileInterface
+ */
+ private $profile;
+
+ /**
+ * @var array
+ */
+ protected static $colors = array();
+
+ public function __construct()
+ {
+ $this->parser = new ColorParser();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function name()
+ {
+ return PaletteInterface::PALETTE_RGB;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pixelDefinition()
+ {
+ return array(
+ ColorInterface::COLOR_RED,
+ ColorInterface::COLOR_GREEN,
+ ColorInterface::COLOR_BLUE,
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsAlpha()
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function useProfile(ProfileInterface $profile)
+ {
+ $this->profile = $profile;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function profile()
+ {
+ if (!$this->profile) {
+ $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc');
+ }
+
+ return $this->profile;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function color($color, $alpha = null)
+ {
+ if (null === $alpha) {
+ $alpha = 100;
+ }
+
+ $color = $this->parser->parseToRGB($color);
+ $index = sprintf('#%02x%02x%02x-%d', $color[0], $color[1], $color[2], $alpha);
+
+ if (false === array_key_exists($index, static::$colors)) {
+ static::$colors[$index] = new RGBColor($this, $color, $alpha);
+ }
+
+ return static::$colors[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blend(ColorInterface $color1, ColorInterface $color2, $amount)
+ {
+ if (!$color1 instanceof RGBColor || ! $color2 instanceof RGBColor) {
+ throw new RuntimeException('RGB palette can only blend RGB colors');
+ }
+
+ return $this->color(
+ array(
+ (int) min(255, min($color1->getRed(), $color2->getRed()) + round(abs($color2->getRed() - $color1->getRed()) * $amount)),
+ (int) min(255, min($color1->getGreen(), $color2->getGreen()) + round(abs($color2->getGreen() - $color1->getGreen()) * $amount)),
+ (int) min(255, min($color1->getBlue(), $color2->getBlue()) + round(abs($color2->getBlue() - $color1->getBlue()) * $amount)),
+ ),
+ (int) min(100, min($color1->getAlpha(), $color2->getAlpha()) + round(abs($color2->getAlpha() - $color1->getAlpha()) * $amount))
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Point.php b/src/Symfony/Component/Image/Image/Point.php
new file mode 100644
index 0000000000000..a33f3eada01f5
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Point.php
@@ -0,0 +1,88 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+/**
+ * The point class
+ */
+final class Point implements PointInterface
+{
+ /**
+ * @var integer
+ */
+ private $x;
+
+ /**
+ * @var integer
+ */
+ private $y;
+
+ /**
+ * Constructs a point of coordinates
+ *
+ * @param integer $x
+ * @param integer $y
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct($x, $y)
+ {
+ if ($x < 0 || $y < 0) {
+ throw new InvalidArgumentException(sprintf('A coordinate cannot be positioned outside of a bounding box (x: %s, y: %s given)', $x, $y));
+ }
+
+ $this->x = $x;
+ $this->y = $y;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getX()
+ {
+ return $this->x;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getY()
+ {
+ return $this->y;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function in(BoxInterface $box)
+ {
+ return $this->x < $box->getWidth() && $this->y < $box->getHeight();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function move($amount)
+ {
+ return new Point($this->x + $amount, $this->y + $amount);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return sprintf('(%d, %d)', $this->x, $this->y);
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/Point/Center.php b/src/Symfony/Component/Image/Image/Point/Center.php
new file mode 100644
index 0000000000000..9a28b70828f71
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Point/Center.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image\Point;
+
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Point as OriginalPoint;
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Point center
+ */
+final class Center implements PointInterface
+{
+ /**
+ * @var BoxInterface
+ */
+ private $box;
+
+ /**
+ * Constructs coordinate with size instance, it needs to be relative to
+ *
+ * @param BoxInterface $box
+ */
+ public function __construct(BoxInterface $box)
+ {
+ $this->box = $box;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getX()
+ {
+ return ceil($this->box->getWidth() / 2);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getY()
+ {
+ return ceil($this->box->getHeight() / 2);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function in(BoxInterface $box)
+ {
+ return $this->getX() < $box->getWidth() && $this->getY() < $box->getHeight();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function move($amount)
+ {
+ return new OriginalPoint($this->getX() + $amount, $this->getY() + $amount);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return sprintf('(%d, %d)', $this->getX(), $this->getY());
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/PointInterface.php b/src/Symfony/Component/Image/Image/PointInterface.php
new file mode 100644
index 0000000000000..f42217e27c19d
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/PointInterface.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+/**
+ * The point interface
+ */
+interface PointInterface
+{
+ /**
+ * Gets points x coordinate
+ *
+ * @return integer
+ */
+ public function getX();
+
+ /**
+ * Gets points y coordinate
+ *
+ * @return integer
+ */
+ public function getY();
+
+ /**
+ * Checks if current coordinate is inside a given box
+ *
+ * @param BoxInterface $box
+ *
+ * @return Boolean
+ */
+ public function in(BoxInterface $box);
+
+ /**
+ * Returns another point, moved by a given amount from current coordinates
+ *
+ * @param integer $amount
+ * @return ImageInterface
+ */
+ public function move($amount);
+
+ /**
+ * Gets a string representation for the current point
+ *
+ * @return string
+ */
+ public function __toString();
+}
diff --git a/src/Symfony/Component/Image/Image/Profile.php b/src/Symfony/Component/Image/Image/Profile.php
new file mode 100644
index 0000000000000..7374e6f525d36
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/Profile.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+
+class Profile implements ProfileInterface
+{
+ private $data;
+ private $name;
+
+ public function __construct($name, $data)
+ {
+ $this->name = $name;
+ $this->data = $data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function name()
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function data()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Creates a profile from a path to a file
+ *
+ * @param String $path
+ *
+ * @return Profile
+ *
+ * @throws InvalidArgumentException In case the provided path is not valid
+ */
+ public static function fromPath($path)
+ {
+ if (!file_exists($path) || !is_file($path) || !is_readable($path)) {
+ throw new InvalidArgumentException(sprintf('Path %s is an invalid profile file or is not readable', $path));
+ }
+
+ return new static(basename($path), file_get_contents($path));
+ }
+}
diff --git a/src/Symfony/Component/Image/Image/ProfileInterface.php b/src/Symfony/Component/Image/Image/ProfileInterface.php
new file mode 100644
index 0000000000000..3e09656c75ea7
--- /dev/null
+++ b/src/Symfony/Component/Image/Image/ProfileInterface.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Image;
+
+interface ProfileInterface
+{
+ /**
+ * Returns the name of the profile
+ *
+ * @return String
+ */
+ public function name();
+
+ /**
+ * Returns the profile data
+ *
+ * @return String
+ */
+ public function data();
+}
diff --git a/src/Symfony/Component/Image/Imagick/Drawer.php b/src/Symfony/Component/Image/Imagick/Drawer.php
new file mode 100644
index 0000000000000..f91d68f942c0e
--- /dev/null
+++ b/src/Symfony/Component/Image/Imagick/Drawer.php
@@ -0,0 +1,404 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Imagick;
+
+use Symfony\Component\Image\Draw\DrawerInterface;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\AbstractFont;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+
+/**
+ * Drawer implementation using the Imagick PHP extension
+ */
+final class Drawer implements DrawerInterface
+{
+ /**
+ * @var \Imagick
+ */
+ private $imagick;
+
+ /**
+ * @param \Imagick $imagick
+ */
+ public function __construct(\Imagick $imagick)
+ {
+ $this->imagick = $imagick;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1)
+ {
+ $x = $center->getX();
+ $y = $center->getY();
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ try {
+ $pixel = $this->getColor($color);
+ $arc = new \ImagickDraw();
+
+ $arc->setStrokeColor($pixel);
+ $arc->setStrokeWidth(max(1, (int) $thickness));
+ $arc->setFillColor('transparent');
+ $arc->arc($x - $width / 2, $y - $height / 2, $x + $width / 2, $y + $height / 2, $start, $end);
+
+ $this->imagick->drawImage($arc);
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ $arc->clear();
+ $arc->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Draw arc operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ $x = $center->getX();
+ $y = $center->getY();
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ try {
+ $pixel = $this->getColor($color);
+ $chord = new \ImagickDraw();
+
+ $chord->setStrokeColor($pixel);
+ $chord->setStrokeWidth(max(1, (int) $thickness));
+
+ if ($fill) {
+ $chord->setFillColor($pixel);
+ } else {
+ $this->line(
+ new Point(round($x + $width / 2 * cos(deg2rad($start))), round($y + $height / 2 * sin(deg2rad($start)))),
+ new Point(round($x + $width / 2 * cos(deg2rad($end))), round($y + $height / 2 * sin(deg2rad($end)))),
+ $color
+ );
+
+ $chord->setFillColor('transparent');
+ }
+
+ $chord->arc(
+ $x - $width / 2,
+ $y - $height / 2,
+ $x + $width / 2,
+ $y + $height / 2,
+ $start,
+ $end
+ );
+
+ $this->imagick->drawImage($chord);
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ $chord->clear();
+ $chord->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Draw chord operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ try {
+ $pixel = $this->getColor($color);
+ $ellipse = new \ImagickDraw();
+
+ $ellipse->setStrokeColor($pixel);
+ $ellipse->setStrokeWidth(max(1, (int) $thickness));
+
+ if ($fill) {
+ $ellipse->setFillColor($pixel);
+ } else {
+ $ellipse->setFillColor('transparent');
+ }
+
+ $ellipse->ellipse(
+ $center->getX(),
+ $center->getY(),
+ $width / 2,
+ $height / 2,
+ 0, 360
+ );
+
+ if (false === $this->imagick->drawImage($ellipse)) {
+ throw new RuntimeException('Ellipse operation failed');
+ }
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ $ellipse->clear();
+ $ellipse->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Draw ellipse operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1)
+ {
+ try {
+ $pixel = $this->getColor($color);
+ $line = new \ImagickDraw();
+
+ $line->setStrokeColor($pixel);
+ $line->setStrokeWidth(max(1, (int) $thickness));
+ $line->setFillColor($pixel);
+ $line->line(
+ $start->getX(),
+ $start->getY(),
+ $end->getX(),
+ $end->getY()
+ );
+
+ $this->imagick->drawImage($line);
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ $line->clear();
+ $line->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Draw line operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $x1 = round($center->getX() + $width / 2 * cos(deg2rad($start)));
+ $y1 = round($center->getY() + $height / 2 * sin(deg2rad($start)));
+ $x2 = round($center->getX() + $width / 2 * cos(deg2rad($end)));
+ $y2 = round($center->getY() + $height / 2 * sin(deg2rad($end)));
+
+ if ($fill) {
+ $this->chord($center, $size, $start, $end, $color, true, $thickness);
+ $this->polygon(
+ array(
+ $center,
+ new Point($x1, $y1),
+ new Point($x2, $y2),
+ ),
+ $color,
+ true,
+ $thickness
+ );
+ } else {
+ $this->arc($center, $size, $start, $end, $color, $thickness);
+ $this->line($center, new Point($x1, $y1), $color, $thickness);
+ $this->line($center, new Point($x2, $y2), $color, $thickness);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dot(PointInterface $position, ColorInterface $color)
+ {
+ $x = $position->getX();
+ $y = $position->getY();
+
+ try {
+ $pixel = $this->getColor($color);
+ $point = new \ImagickDraw();
+
+ $point->setFillColor($pixel);
+ $point->point($x, $y);
+
+ $this->imagick->drawimage($point);
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ $point->clear();
+ $point->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Draw point operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1)
+ {
+ if (count($coordinates) < 3) {
+ throw new InvalidArgumentException(sprintf('Polygon must consist of at least 3 coordinates, %d given', count($coordinates)));
+ }
+
+ $points = array_map(function (PointInterface $p) {
+ return array('x' => $p->getX(), 'y' => $p->getY());
+ }, $coordinates);
+
+ try {
+ $pixel = $this->getColor($color);
+ $polygon = new \ImagickDraw();
+
+ $polygon->setStrokeColor($pixel);
+ $polygon->setStrokeWidth(max(1, (int) $thickness));
+
+ if ($fill) {
+ $polygon->setFillColor($pixel);
+ } else {
+ $polygon->setFillColor('transparent');
+ }
+
+ $polygon->polygon($points);
+ $this->imagick->drawImage($polygon);
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ $polygon->clear();
+ $polygon->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Draw polygon operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null)
+ {
+ try {
+ $pixel = $this->getColor($font->getColor());
+ $text = new \ImagickDraw();
+
+ $text->setFont($font->getFile());
+ /**
+ * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027
+ *
+ * ensure font resolution is the same as GD's hard-coded 96
+ */
+ if (version_compare(phpversion("imagick"), "3.0.2", ">=")) {
+ $text->setResolution(96, 96);
+ $text->setFontSize($font->getSize());
+ } else {
+ $text->setFontSize((int) ($font->getSize() * (96 / 72)));
+ }
+ $text->setFillColor($pixel);
+ $text->setTextAntialias(true);
+
+ $info = $this->imagick->queryFontMetrics($text, $string);
+ $rad = deg2rad($angle);
+ $cos = cos($rad);
+ $sin = sin($rad);
+
+ // round(0 * $cos - 0 * $sin)
+ $x1 = 0;
+ $x2 = round($info['characterWidth'] * $cos - $info['characterHeight'] * $sin);
+ // round(0 * $sin + 0 * $cos)
+ $y1 = 0;
+ $y2 = round($info['characterWidth'] * $sin + $info['characterHeight'] * $cos);
+
+ $xdiff = 0 - min($x1, $x2);
+ $ydiff = 0 - min($y1, $y2);
+
+ if ($width !== null) {
+ $string = $this->wrapText($string, $text, $angle, $width);
+ }
+
+ $this->imagick->annotateImage(
+ $text, $position->getX() + $x1 + $xdiff,
+ $position->getY() + $y2 + $ydiff, $angle, $string
+ );
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ $text->clear();
+ $text->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Draw text operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets specifically formatted color string from ColorInterface instance
+ *
+ * @param ColorInterface $color
+ *
+ * @return string
+ */
+ private function getColor(ColorInterface $color)
+ {
+ $pixel = new \ImagickPixel((string) $color);
+ $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100);
+
+ return $pixel;
+ }
+
+ /**
+ * Internal
+ *
+ * Fits a string into box with given width
+ */
+ private function wrapText($string, $text, $angle, $width)
+ {
+ $result = '';
+ $words = explode(' ', $string);
+ foreach ($words as $word) {
+ $teststring = $result . ' ' . $word;
+ $testbox = $this->imagick->queryFontMetrics($text, $teststring, true);
+ if ($testbox['textWidth'] > $width) {
+ $result .= ($result == '' ? '' : "\n") . $word;
+ } else {
+ $result .= ($result == '' ? '' : ' ') . $word;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Symfony/Component/Image/Imagick/Effects.php b/src/Symfony/Component/Image/Imagick/Effects.php
new file mode 100644
index 0000000000000..0c484b713a45f
--- /dev/null
+++ b/src/Symfony/Component/Image/Imagick/Effects.php
@@ -0,0 +1,119 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Imagick;
+
+use Symfony\Component\Image\Effects\EffectsInterface;
+use Symfony\Component\Image\Exception\NotSupportedException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\Color\RGB;
+
+/**
+ * Effects implementation using the Imagick PHP extension
+ */
+class Effects implements EffectsInterface
+{
+ private $imagick;
+
+ public function __construct(\Imagick $imagick)
+ {
+ $this->imagick = $imagick;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function gamma($correction)
+ {
+ try {
+ $this->imagick->gammaImage($correction, \Imagick::CHANNEL_ALL);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to apply gamma correction to the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function negative()
+ {
+ try {
+ $this->imagick->negateImage(false, \Imagick::CHANNEL_ALL);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to negate the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function grayscale()
+ {
+ try {
+ $this->imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function colorize(ColorInterface $color)
+ {
+ if (!$color instanceof RGB) {
+ throw new NotSupportedException('Colorize with non-rgb color is not supported');
+ }
+
+ try {
+ $this->imagick->colorizeImage((string) $color, new \ImagickPixel(sprintf('rgba(%d, %d, %d, 1)', $color->getRed(), $color->getGreen(), $color->getBlue())));
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to colorize the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sharpen()
+ {
+ try {
+ $this->imagick->sharpenImage(2, 1);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to sharpen the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blur($sigma = 1)
+ {
+ try {
+ $this->imagick->gaussianBlurImage(0, $sigma);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to blur the image', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+}
diff --git a/src/Symfony/Component/Image/Imagick/Font.php b/src/Symfony/Component/Image/Imagick/Font.php
new file mode 100644
index 0000000000000..d6093cbd9b5cf
--- /dev/null
+++ b/src/Symfony/Component/Image/Imagick/Font.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Imagick;
+
+use Symfony\Component\Image\Image\AbstractFont;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+/**
+ * Font implementation using the Imagick PHP extension
+ */
+final class Font extends AbstractFont
+{
+ /**
+ * @var \Imagick
+ */
+ private $imagick;
+
+ /**
+ * @param \Imagick $imagick
+ * @param string $file
+ * @param integer $size
+ * @param ColorInterface $color
+ */
+ public function __construct(\Imagick $imagick, $file, $size, ColorInterface $color)
+ {
+ $this->imagick = $imagick;
+
+ parent::__construct($file, $size, $color);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function box($string, $angle = 0)
+ {
+ $text = new \ImagickDraw();
+
+ $text->setFont($this->file);
+
+ /**
+ * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027
+ *
+ * ensure font resolution is the same as GD's hard-coded 96
+ */
+ if (version_compare(phpversion("imagick"), "3.0.2", ">=")) {
+ $text->setResolution(96, 96);
+ $text->setFontSize($this->size);
+ } else {
+ $text->setFontSize((int) ($this->size * (96 / 72)));
+ }
+
+ $info = $this->imagick->queryFontMetrics($text, $string);
+
+ $box = new Box($info['textWidth'], $info['textHeight']);
+
+ return $box;
+ }
+}
diff --git a/src/Symfony/Component/Image/Imagick/Image.php b/src/Symfony/Component/Image/Imagick/Image.php
new file mode 100644
index 0000000000000..afb17ca4ebdec
--- /dev/null
+++ b/src/Symfony/Component/Image/Imagick/Image.php
@@ -0,0 +1,900 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Imagick;
+
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\AbstractImage;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Fill\FillInterface;
+use Symfony\Component\Image\Image\Fill\Gradient\Horizontal;
+use Symfony\Component\Image\Image\Fill\Gradient\Linear;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Image\ProfileInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+
+/**
+ * Image implementation using the Imagick PHP extension
+ */
+final class Image extends AbstractImage
+{
+ /**
+ * @var \Imagick
+ */
+ private $imagick;
+ /**
+ * @var Layers
+ */
+ private $layers;
+ /**
+ * @var PaletteInterface
+ */
+ private $palette;
+
+ /**
+ * @var Boolean
+ */
+ private static $supportsColorspaceConversion;
+
+ private static $colorspaceMapping = array(
+ PaletteInterface::PALETTE_CMYK => \Imagick::COLORSPACE_CMYK,
+ PaletteInterface::PALETTE_RGB => \Imagick::COLORSPACE_RGB,
+ PaletteInterface::PALETTE_GRAYSCALE => \Imagick::COLORSPACE_GRAY,
+ );
+
+ /**
+ * Constructs a new Image instance
+ *
+ * @param \Imagick $imagick
+ * @param PaletteInterface $palette
+ * @param MetadataBag $metadata
+ */
+ public function __construct(\Imagick $imagick, PaletteInterface $palette, MetadataBag $metadata)
+ {
+ $this->metadata = $metadata;
+ $this->detectColorspaceConversionSupport();
+ $this->imagick = $imagick;
+ if (static::$supportsColorspaceConversion) {
+ $this->setColorspace($palette);
+ }
+ $this->palette = $palette;
+ $this->layers = new Layers($this, $this->palette, $this->imagick);
+ }
+
+ /**
+ * Destroys allocated imagick resources
+ */
+ public function __destruct()
+ {
+ if ($this->imagick instanceof \Imagick) {
+ $this->imagick->clear();
+ $this->imagick->destroy();
+ }
+ }
+
+ /**
+ * Returns the underlying \Imagick instance
+ *
+ * @return \Imagick
+ */
+ public function getImagick()
+ {
+ return $this->imagick;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function copy()
+ {
+ try {
+ if (version_compare(phpversion("imagick"), "3.1.0b1", ">=") || defined("HHVM_VERSION")) {
+ $clone = clone $this->imagick;
+ } else {
+ $clone = $this->imagick->clone();
+ }
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Copy operation failed', $e->getCode(), $e);
+ }
+
+ return new self($clone, $this->palette, clone $this->metadata);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function crop(PointInterface $start, BoxInterface $size)
+ {
+ if (!$start->in($this->getSize())) {
+ throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders');
+ }
+ try {
+ if ($this->layers()->count() > 1) {
+ // Crop each layer separately
+ $this->imagick = $this->imagick->coalesceImages();
+ foreach ($this->imagick as $frame) {
+ $frame->cropImage($size->getWidth(), $size->getHeight(), $start->getX(), $start->getY());
+ // Reset canvas for gif format
+ $frame->setImagePage(0, 0, 0, 0);
+ }
+ $this->imagick = $this->imagick->deconstructImages();
+ } else {
+ $this->imagick->cropImage($size->getWidth(), $size->getHeight(), $start->getX(), $start->getY());
+ // Reset canvas for gif format
+ $this->imagick->setImagePage(0, 0, 0, 0);
+ }
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Crop operation failed', $e->getCode(), $e);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function flipHorizontally()
+ {
+ try {
+ $this->imagick->flopImage();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Horizontal Flip operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function flipVertically()
+ {
+ try {
+ $this->imagick->flipImage();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Vertical flip operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function strip()
+ {
+ try {
+ try {
+ $this->profile($this->palette->profile());
+ } catch (\Exception $e) {
+ // here we discard setting the profile as the previous incorporated profile
+ // is corrupted, let's now strip the image
+ }
+ $this->imagick->stripImage();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Strip operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function paste(ImageInterface $image, PointInterface $start)
+ {
+ if (!$image instanceof self) {
+ throw new InvalidArgumentException(sprintf('Imagick\Image can only paste() Imagick\Image instances, %s given', get_class($image)));
+ }
+
+ if (!$this->getSize()->contains($image->getSize(), $start)) {
+ throw new OutOfBoundsException('Cannot paste image of the given size at the specified position, as it moves outside of the current image\'s box');
+ }
+
+ try {
+ $this->imagick->compositeImage($image->imagick, \Imagick::COMPOSITE_DEFAULT, $start->getX(), $start->getY());
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Paste operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ try {
+ if ($this->layers->count() > 1) {
+ $this->imagick = $this->imagick->coalesceImages();
+ foreach ($this->imagick as $frame) {
+ $frame->resizeImage($size->getWidth(), $size->getHeight(), $this->getFilter($filter), 1);
+ }
+ $this->imagick = $this->imagick->deconstructImages();
+ } else {
+ $this->imagick->resizeImage($size->getWidth(), $size->getHeight(), $this->getFilter($filter), 1);
+ }
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Resize operation failed', $e->getCode(), $e);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function rotate($angle, ColorInterface $background = null)
+ {
+ $color = $background ? $background : $this->palette->color('fff');
+
+ try {
+ $pixel = $this->getColor($color);
+
+ $this->imagick->rotateimage($pixel, $angle);
+
+ $pixel->clear();
+ $pixel->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Rotate operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function save($path = null, array $options = array())
+ {
+ $path = null === $path ? $this->imagick->getImageFilename() : $path;
+ if (null === $path) {
+ throw new RuntimeException('You can omit save path only if image has been open from a file');
+ }
+
+ try {
+ $this->prepareOutput($options, $path);
+ $this->imagick->writeImages($path, true);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Save operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function show($format, array $options = array())
+ {
+ header('Content-type: '.$this->getMimeType($format));
+ echo $this->get($format, $options);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($format, array $options = array())
+ {
+ try {
+ $options['format'] = $format;
+ $this->prepareOutput($options);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Get operation failed', $e->getCode(), $e);
+ }
+
+ return $this->imagick->getImagesBlob();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function interlace($scheme)
+ {
+ static $supportedInterlaceSchemes = array(
+ ImageInterface::INTERLACE_NONE => \Imagick::INTERLACE_NO,
+ ImageInterface::INTERLACE_LINE => \Imagick::INTERLACE_LINE,
+ ImageInterface::INTERLACE_PLANE => \Imagick::INTERLACE_PLANE,
+ ImageInterface::INTERLACE_PARTITION => \Imagick::INTERLACE_PARTITION,
+ );
+
+ if (!array_key_exists($scheme, $supportedInterlaceSchemes)) {
+ throw new InvalidArgumentException('Unsupported interlace type');
+ }
+
+ $this->imagick->setInterlaceScheme($supportedInterlaceSchemes[$scheme]);
+
+ return $this;
+ }
+
+ /**
+ * @param array $options
+ * @param string $path
+ */
+ private function prepareOutput(array $options, $path = null)
+ {
+ if (isset($options['format'])) {
+ $this->imagick->setImageFormat($options['format']);
+ }
+
+ if (isset($options['animated']) && true === $options['animated']) {
+ $format = isset($options['format']) ? $options['format'] : 'gif';
+ $delay = isset($options['animated.delay']) ? $options['animated.delay'] : null;
+ $loops = isset($options['animated.loops']) ? $options['animated.loops'] : 0;
+
+ $options['flatten'] = false;
+
+ $this->layers->animate($format, $delay, $loops);
+ } else {
+ $this->layers->merge();
+ }
+ $this->applyImageOptions($this->imagick, $options, $path);
+
+ // flatten only if image has multiple layers
+ if ((!isset($options['flatten']) || $options['flatten'] === true) && count($this->layers) > 1) {
+ $this->flatten();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return $this->get('png');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function draw()
+ {
+ return new Drawer($this->imagick);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function effects()
+ {
+ return new Effects($this->imagick);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSize()
+ {
+ try {
+ $i = $this->imagick->getIteratorIndex();
+ $this->imagick->rewind();
+ $width = $this->imagick->getImageWidth();
+ $height = $this->imagick->getImageHeight();
+ $this->imagick->setIteratorIndex($i);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Could not get size', $e->getCode(), $e);
+ }
+
+ return new Box($width, $height);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function applyMask(ImageInterface $mask)
+ {
+ if (!$mask instanceof self) {
+ throw new InvalidArgumentException('Can only apply instances of Symfony\Component\Image\Imagick\Image as masks');
+ }
+
+ $size = $this->getSize();
+ $maskSize = $mask->getSize();
+
+ if ($size != $maskSize) {
+ throw new InvalidArgumentException(sprintf('The given mask doesn\'t match current image\'s size, Current mask\'s dimensions are %s, while image\'s dimensions are %s', $maskSize, $size));
+ }
+
+ $mask = $mask->mask();
+ $mask->imagick->negateImage(true);
+
+ try {
+ // remove transparent areas of the original from the mask
+ $mask->imagick->compositeImage($this->imagick, \Imagick::COMPOSITE_DSTIN, 0, 0);
+ $this->imagick->compositeImage($mask->imagick, \Imagick::COMPOSITE_COPYOPACITY, 0, 0);
+
+ $mask->imagick->clear();
+ $mask->imagick->destroy();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Apply mask operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mask()
+ {
+ $mask = $this->copy();
+
+ try {
+ $mask->imagick->modulateImage(100, 0, 100);
+ $mask->imagick->setImageMatte(false);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Mask operation failed', $e->getCode(), $e);
+ }
+
+ return $mask;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return ImageInterface
+ */
+ public function fill(FillInterface $fill)
+ {
+ try {
+ if ($this->isLinearOpaque($fill)) {
+ $this->applyFastLinear($fill);
+ } else {
+ $iterator = $this->imagick->getPixelIterator();
+
+ foreach ($iterator as $y => $pixels) {
+ foreach ($pixels as $x => $pixel) {
+ $color = $fill->getColor(new Point($x, $y));
+
+ $pixel->setColor((string) $color);
+ $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100);
+ }
+
+ $iterator->syncIterator();
+ }
+ }
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Fill operation failed', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function histogram()
+ {
+ try {
+ $pixels = $this->imagick->getImageHistogram();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Error while fetching histogram', $e->getCode(), $e);
+ }
+
+ $image = $this;
+
+ return array_map(function (\ImagickPixel $pixel) use ($image) {
+ return $image->pixelToColor($pixel);
+ },$pixels);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getColorAt(PointInterface $point)
+ {
+ if (!$point->in($this->getSize())) {
+ throw new RuntimeException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight()));
+ }
+
+ try {
+ $pixel = $this->imagick->getImagePixelColor($point->getX(), $point->getY());
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Error while getting image pixel color', $e->getCode(), $e);
+ }
+
+ return $this->pixelToColor($pixel);
+ }
+
+ /**
+ * Returns a color given a pixel, depending the Palette context
+ *
+ * Note : this method is public for PHP 5.3 compatibility
+ *
+ * @param \ImagickPixel $pixel
+ *
+ * @return ColorInterface
+ *
+ * @throws InvalidArgumentException In case a unknown color is requested
+ */
+ public function pixelToColor(\ImagickPixel $pixel)
+ {
+ static $colorMapping = array(
+ ColorInterface::COLOR_RED => \Imagick::COLOR_RED,
+ ColorInterface::COLOR_GREEN => \Imagick::COLOR_GREEN,
+ ColorInterface::COLOR_BLUE => \Imagick::COLOR_BLUE,
+ ColorInterface::COLOR_CYAN => \Imagick::COLOR_CYAN,
+ ColorInterface::COLOR_MAGENTA => \Imagick::COLOR_MAGENTA,
+ ColorInterface::COLOR_YELLOW => \Imagick::COLOR_YELLOW,
+ ColorInterface::COLOR_KEYLINE => \Imagick::COLOR_BLACK,
+ // There is no gray component in \Imagick, let's use one of the RGB comp
+ ColorInterface::COLOR_GRAY => \Imagick::COLOR_RED,
+ );
+
+ $alpha = $this->palette->supportsAlpha() ? (int) round($pixel->getColorValue(\Imagick::COLOR_ALPHA) * 100) : null;
+ $palette = $this->palette();
+
+ return $this->palette->color(array_map(function ($color) use ($palette, $pixel, $colorMapping) {
+ if (!isset($colorMapping[$color])) {
+ throw new InvalidArgumentException(sprintf('Color %s is not mapped in Imagick', $color));
+ }
+ $multiplier = 255;
+ if ($palette->name() === PaletteInterface::PALETTE_CMYK) {
+ $multiplier = 100;
+ }
+
+ return $pixel->getColorValue($colorMapping[$color]) * $multiplier;
+ }, $this->palette->pixelDefinition()), $alpha);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function layers()
+ {
+ return $this->layers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function usePalette(PaletteInterface $palette)
+ {
+ if (!isset(static::$colorspaceMapping[$palette->name()])) {
+ throw new InvalidArgumentException(sprintf('The palette %s is not supported by Imagick driver', $palette->name()));
+ }
+
+ if ($this->palette->name() === $palette->name()) {
+ return $this;
+ }
+
+ if (!static::$supportsColorspaceConversion) {
+ throw new RuntimeException('Your version of Imagick does not support colorspace conversions.');
+ }
+
+ try {
+ try {
+ $hasICCProfile = (Boolean) $this->imagick->getImageProfile('icc');
+ } catch (\ImagickException $e) {
+ $hasICCProfile = false;
+ }
+
+ if (!$hasICCProfile) {
+ $this->profile($this->palette->profile());
+ }
+
+ $this->profile($palette->profile());
+ $this->setColorspace($palette);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to set colorspace', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function palette()
+ {
+ return $this->palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function profile(ProfileInterface $profile)
+ {
+ try {
+ $this->imagick->profileImage('icc', $profile->data());
+ } catch (\ImagickException $e) {
+ throw new RuntimeException(sprintf('Unable to add profile %s to image', $profile->name()), $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Internal
+ *
+ * Flatten the image.
+ */
+ private function flatten()
+ {
+ /**
+ * @see https://github.com/mkoppanen/imagick/issues/45
+ */
+ try {
+ if (method_exists($this->imagick, 'mergeImageLayers') && defined('Imagick::LAYERMETHOD_UNDEFINED')) {
+ $this->imagick = $this->imagick->mergeImageLayers(\Imagick::LAYERMETHOD_UNDEFINED);
+ } elseif (method_exists($this->imagick, 'flattenImages')) {
+ $this->imagick = $this->imagick->flattenImages();
+ }
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Flatten operation failed', $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Internal
+ *
+ * Applies options before save or output
+ *
+ * @param \Imagick $image
+ * @param array $options
+ * @param string $path
+ *
+ * @throws InvalidArgumentException
+ * @throws RuntimeException
+ */
+ private function applyImageOptions(\Imagick $image, array $options, $path)
+ {
+ if (isset($options['format'])) {
+ $format = $options['format'];
+ } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) {
+ $format = $extension;
+ } else {
+ $format = pathinfo($image->getImageFilename(), \PATHINFO_EXTENSION);
+ }
+
+ $format = strtolower($format);
+
+ $options = $this->updateSaveOptions($options);
+
+ if (isset($options['jpeg_quality']) && in_array($format, array('jpeg', 'jpg', 'pjpeg'))) {
+ $image->setImageCompressionQuality($options['jpeg_quality']);
+ }
+
+ if ((isset($options['png_compression_level']) || isset($options['png_compression_filter'])) && $format === 'png') {
+ // first digit: compression level (default: 7)
+ if (isset($options['png_compression_level'])) {
+ if ($options['png_compression_level'] < 0 || $options['png_compression_level'] > 9) {
+ throw new InvalidArgumentException('png_compression_level option should be an integer from 0 to 9');
+ }
+ $compression = $options['png_compression_level'] * 10;
+ } else {
+ $compression = 70;
+ }
+
+ // second digit: compression filter (default: 5)
+ if (isset($options['png_compression_filter'])) {
+ if ($options['png_compression_filter'] < 0 || $options['png_compression_filter'] > 9) {
+ throw new InvalidArgumentException('png_compression_filter option should be an integer from 0 to 9');
+ }
+ $compression += $options['png_compression_filter'];
+ } else {
+ $compression += 5;
+ }
+
+ $image->setImageCompressionQuality($compression);
+ }
+
+ if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) {
+ if ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERCENTIMETER) {
+ $image->setImageUnits(\Imagick::RESOLUTION_PIXELSPERCENTIMETER);
+ } elseif ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERINCH) {
+ $image->setImageUnits(\Imagick::RESOLUTION_PIXELSPERINCH);
+ } else {
+ throw new RuntimeException('Unsupported image unit format');
+ }
+
+ $filter = ImageInterface::FILTER_UNDEFINED;
+ if (!empty($options['resampling-filter'])) {
+ $filter = $options['resampling-filter'];
+ }
+
+ $image->setImageResolution($options['resolution-x'], $options['resolution-y']);
+ $image->resampleImage($options['resolution-x'], $options['resolution-y'], $this->getFilter($filter), 0);
+ }
+ }
+
+ /**
+ * Gets specifically formatted color string from Color instance
+ *
+ * @param ColorInterface $color
+ *
+ * @return \ImagickPixel
+ */
+ private function getColor(ColorInterface $color)
+ {
+ $pixel = new \ImagickPixel((string) $color);
+ $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100);
+
+ return $pixel;
+ }
+
+ /**
+ * Checks whether given $fill is linear and opaque
+ *
+ * @param FillInterface $fill
+ *
+ * @return Boolean
+ */
+ private function isLinearOpaque(FillInterface $fill)
+ {
+ return $fill instanceof Linear && $fill->getStart()->isOpaque() && $fill->getEnd()->isOpaque();
+ }
+
+ /**
+ * Performs optimized gradient fill for non-opaque linear gradients
+ *
+ * @param Linear $fill
+ */
+ private function applyFastLinear(Linear $fill)
+ {
+ $gradient = new \Imagick();
+ $size = $this->getSize();
+ $color = sprintf('gradient:%s-%s', (string) $fill->getStart(), (string) $fill->getEnd());
+
+ if ($fill instanceof Horizontal) {
+ $gradient->newPseudoImage($size->getHeight(), $size->getWidth(), $color);
+ $gradient->rotateImage(new \ImagickPixel(), 90);
+ } else {
+ $gradient->newPseudoImage($size->getWidth(), $size->getHeight(), $color);
+ }
+
+ $this->imagick->compositeImage($gradient, \Imagick::COMPOSITE_OVER, 0, 0);
+ $gradient->clear();
+ $gradient->destroy();
+ }
+
+ /**
+ * Internal
+ *
+ * Get the mime type based on format.
+ *
+ * @param string $format
+ *
+ * @return string mime-type
+ *
+ * @throws RuntimeException
+ */
+ private function getMimeType($format)
+ {
+ static $mimeTypes = array(
+ 'jpeg' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+ 'wbmp' => 'image/vnd.wap.wbmp',
+ 'xbm' => 'image/xbm',
+ );
+
+ if (!isset($mimeTypes[$format])) {
+ throw new RuntimeException(sprintf('Unsupported format given. Only %s are supported, %s given', implode(", ", array_keys($mimeTypes)), $format));
+ }
+
+ return $mimeTypes[$format];
+ }
+
+ /**
+ * Sets colorspace and image type, assigns the palette.
+ *
+ * @param PaletteInterface $palette
+ *
+ * @throws InvalidArgumentException
+ */
+ private function setColorspace(PaletteInterface $palette)
+ {
+ $typeMapping = array(
+ // We use Matte variants to preserve alpha
+ //
+ // (the constants \Imagick::IMGTYPE_TRUECOLORMATTE and \Imagick::IMGTYPE_GRAYSCALEMATTE do not exist anymore in Imagick 7,
+ // to fix this the former values are hard coded here, the documentation under http://php.net/manual/en/imagick.settype.php
+ // doesn't tell us which constants to use and the alternative constants listed under
+ // https://pecl.php.net/package/imagick/3.4.3RC1 do not exist either, so we found no other way to fix it as to hard code
+ // the values here)
+ PaletteInterface::PALETTE_CMYK => defined('\Imagick::IMGTYPE_TRUECOLORMATTE') ? \Imagick::IMGTYPE_TRUECOLORMATTE : 7,
+ PaletteInterface::PALETTE_RGB => defined('\Imagick::IMGTYPE_TRUECOLORMATTE') ? \Imagick::IMGTYPE_TRUECOLORMATTE : 7,
+ PaletteInterface::PALETTE_GRAYSCALE => defined('\Imagick::IMGTYPE_GRAYSCALEMATTE') ? \Imagick::IMGTYPE_GRAYSCALEMATTE : 3,
+ );
+
+ if (!isset(static::$colorspaceMapping[$palette->name()])) {
+ throw new InvalidArgumentException(sprintf('The palette %s is not supported by Imagick driver', $palette->name()));
+ }
+
+ $this->imagick->setType($typeMapping[$palette->name()]);
+ $this->imagick->setColorspace(static::$colorspaceMapping[$palette->name()]);
+ $this->palette = $palette;
+ }
+
+ /**
+ * Older imagemagick versions does not support colorspace conversions.
+ * Let's detect if it is supported.
+ *
+ * @return Boolean
+ */
+ private function detectColorspaceConversionSupport()
+ {
+ if (null !== static::$supportsColorspaceConversion) {
+ return static::$supportsColorspaceConversion;
+ }
+
+ return static::$supportsColorspaceConversion = method_exists('Imagick', 'setColorspace');
+ }
+
+ /**
+ * Returns the filter if it's supported.
+ *
+ * @param string $filter
+ *
+ * @return string
+ *
+ * @throws InvalidArgumentException If the filter is unsupported.
+ */
+ private function getFilter($filter = ImageInterface::FILTER_UNDEFINED)
+ {
+ static $supportedFilters = array(
+ ImageInterface::FILTER_UNDEFINED => \Imagick::FILTER_UNDEFINED,
+ ImageInterface::FILTER_BESSEL => \Imagick::FILTER_BESSEL,
+ ImageInterface::FILTER_BLACKMAN => \Imagick::FILTER_BLACKMAN,
+ ImageInterface::FILTER_BOX => \Imagick::FILTER_BOX,
+ ImageInterface::FILTER_CATROM => \Imagick::FILTER_CATROM,
+ ImageInterface::FILTER_CUBIC => \Imagick::FILTER_CUBIC,
+ ImageInterface::FILTER_GAUSSIAN => \Imagick::FILTER_GAUSSIAN,
+ ImageInterface::FILTER_HANNING => \Imagick::FILTER_HANNING,
+ ImageInterface::FILTER_HAMMING => \Imagick::FILTER_HAMMING,
+ ImageInterface::FILTER_HERMITE => \Imagick::FILTER_HERMITE,
+ ImageInterface::FILTER_LANCZOS => \Imagick::FILTER_LANCZOS,
+ ImageInterface::FILTER_MITCHELL => \Imagick::FILTER_MITCHELL,
+ ImageInterface::FILTER_POINT => \Imagick::FILTER_POINT,
+ ImageInterface::FILTER_QUADRATIC => \Imagick::FILTER_QUADRATIC,
+ ImageInterface::FILTER_SINC => \Imagick::FILTER_SINC,
+ ImageInterface::FILTER_TRIANGLE => \Imagick::FILTER_TRIANGLE
+ );
+
+ if (!array_key_exists($filter, $supportedFilters)) {
+ throw new InvalidArgumentException(sprintf(
+ 'The resampling filter "%s" is not supported by Imagick driver.',
+ $filter
+ ));
+ }
+
+ return $supportedFilters[$filter];
+ }
+}
diff --git a/src/Symfony/Component/Image/Imagick/Layers.php b/src/Symfony/Component/Image/Imagick/Layers.php
new file mode 100644
index 0000000000000..860ee8f6f5331
--- /dev/null
+++ b/src/Symfony/Component/Image/Imagick/Layers.php
@@ -0,0 +1,271 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Imagick;
+
+use Symfony\Component\Image\Image\AbstractLayers;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+
+class Layers extends AbstractLayers
+{
+ /**
+ * @var Image
+ */
+ private $image;
+ /**
+ * @var \Imagick
+ */
+ private $resource;
+ /**
+ * @var integer
+ */
+ private $offset = 0;
+ /**
+ * @var array
+ */
+ private $layers = array();
+
+ private $palette;
+
+ public function __construct(Image $image, PaletteInterface $palette, \Imagick $resource)
+ {
+ $this->image = $image;
+ $this->resource = $resource;
+ $this->palette = $palette;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function merge()
+ {
+ foreach ($this->layers as $offset => $image) {
+ try {
+ $this->resource->setIteratorIndex($offset);
+ $this->resource->setImage($image->getImagick());
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to substitute layer', $e->getCode(), $e);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function animate($format, $delay, $loops)
+ {
+ if ('gif' !== strtolower($format)) {
+ throw new InvalidArgumentException('Animated picture is currently only supported on gif');
+ }
+
+ if (!is_int($loops) || $loops < 0) {
+ throw new InvalidArgumentException('Loops must be a positive integer.');
+ }
+
+ if (null !== $delay && (!is_int($delay) || $delay < 0)) {
+ throw new InvalidArgumentException('Delay must be either null or a positive integer.');
+ }
+
+ try {
+ foreach ($this as $offset => $layer) {
+ $this->resource->setIteratorIndex($offset);
+ $this->resource->setFormat($format);
+
+ if (null !== $delay) {
+ $layer->getImagick()->setImageDelay($delay / 10);
+ $layer->getImagick()->setImageTicksPerSecond(100);
+ }
+ $layer->getImagick()->setImageIterations($loops);
+
+ $this->resource->setImage($layer->getImagick());
+ }
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to animate layers', $e->getCode(), $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function coalesce()
+ {
+ try {
+ $coalescedResource = $this->resource->coalesceImages();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to coalesce layers', $e->getCode(), $e);
+ }
+
+ $count = $coalescedResource->getNumberImages();
+ for ($offset = 0; $offset < $count; $offset++) {
+ try {
+ $coalescedResource->setIteratorIndex($offset);
+ $this->layers[$offset] = new Image($coalescedResource->getImage(), $this->palette, new MetadataBag());
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to retrieve layer', $e->getCode(), $e);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current()
+ {
+ return $this->extractAt($this->offset);
+ }
+
+ /**
+ * Tries to extract layer at given offset
+ *
+ * @param integer $offset
+ *
+ * @return Image
+ * @throws RuntimeException
+ */
+ private function extractAt($offset)
+ {
+ if (!isset($this->layers[$offset])) {
+ try {
+ $this->resource->setIteratorIndex($offset);
+ $this->layers[$offset] = new Image($this->resource->getImage(), $this->palette, new MetadataBag());
+ } catch (\ImagickException $e) {
+ throw new RuntimeException(sprintf('Failed to extract layer %d', $offset), $e->getCode(), $e);
+ }
+ }
+
+ return $this->layers[$offset];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return $this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ ++$this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ $this->offset = 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ return $this->offset < count($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ try {
+ return $this->resource->getNumberImages();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Failed to count the number of layers', $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetExists($offset)
+ {
+ return is_int($offset) && $offset >= 0 && $offset < count($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetGet($offset)
+ {
+ return $this->extractAt($offset);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetSet($offset, $image)
+ {
+ if (!$image instanceof Image) {
+ throw new InvalidArgumentException('Only an Imagick Image can be used as layer');
+ }
+
+ if (null === $offset) {
+ $offset = count($this) - 1;
+ } else {
+ if (!is_int($offset)) {
+ throw new InvalidArgumentException('Invalid offset for layer, it must be an integer');
+ }
+
+ if (count($this) < $offset || 0 > $offset) {
+ throw new OutOfBoundsException(sprintf('Invalid offset for layer, it must be a value between 0 and %d, %d given', count($this), $offset));
+ }
+
+ if (isset($this[$offset])) {
+ unset($this[$offset]);
+ $offset = $offset - 1;
+ }
+ }
+
+ $frame = $image->getImagick();
+
+ try {
+ if (count($this) > 0) {
+ $this->resource->setIteratorIndex($offset);
+ }
+ $this->resource->addImage($frame);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Unable to set the layer', $e->getCode(), $e);
+ }
+
+ $this->layers = array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetUnset($offset)
+ {
+ try {
+ $this->extractAt($offset);
+ } catch (RuntimeException $e) {
+ return;
+ }
+
+ try {
+ $this->resource->setIteratorIndex($offset);
+ $this->resource->removeImage();
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Unable to remove layer', $e->getCode(), $e);
+ }
+ }
+}
diff --git a/src/Symfony/Component/Image/Imagick/Loader.php b/src/Symfony/Component/Image/Imagick/Loader.php
new file mode 100644
index 0000000000000..46991c797473a
--- /dev/null
+++ b/src/Symfony/Component/Image/Imagick/Loader.php
@@ -0,0 +1,184 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Imagick;
+
+use Symfony\Component\Image\Exception\NotSupportedException;
+use Symfony\Component\Image\Image\AbstractLoader;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Palette\CMYK;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Palette\Grayscale;
+
+/**
+ * Loader implementation using the Imagick PHP extension
+ */
+final class Loader extends AbstractLoader
+{
+ /**
+ * @throws RuntimeException
+ */
+ public function __construct()
+ {
+ if (!class_exists('Imagick')) {
+ throw new RuntimeException('Imagick not installed');
+ }
+
+ $version = $this->getVersion(new \Imagick());
+
+ if (version_compare('6.2.9', $version) > 0) {
+ throw new RuntimeException(sprintf('ImageMagick version 6.2.9 or higher is required, %s provided', $version));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function open($path)
+ {
+ $path = $this->checkPath($path);
+
+ try {
+ $imagick = new \Imagick($path);
+ $image = new Image($imagick, $this->createPalette($imagick), $this->getMetadataReader()->readFile($path));
+ } catch (\Exception $e) {
+ throw new RuntimeException(sprintf('Unable to open image %s', $path), $e->getCode(), $e);
+ }
+
+ return $image;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(BoxInterface $size, ColorInterface $color = null)
+ {
+ $width = $size->getWidth();
+ $height = $size->getHeight();
+
+ $palette = null !== $color ? $color->getPalette() : new RGB();
+ $color = null !== $color ? $color : $palette->color('fff');
+
+ try {
+ $pixel = new \ImagickPixel((string) $color);
+ $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100);
+
+ $imagick = new \Imagick();
+ $imagick->newImage($width, $height, $pixel);
+ $imagick->setImageMatte(true);
+ $imagick->setImageBackgroundColor($pixel);
+
+ if (version_compare('6.3.1', $this->getVersion($imagick)) < 0) {
+ if (method_exists($imagick, 'setImageAlpha')) {
+ $imagick->setImageAlpha($pixel->getColorValue(\Imagick::COLOR_ALPHA));
+ } else {
+ $imagick->setImageOpacity($pixel->getColorValue(\Imagick::COLOR_ALPHA));
+ }
+ }
+
+ $pixel->clear();
+ $pixel->destroy();
+
+ return new Image($imagick, $palette, new MetadataBag());
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Could not create empty image', $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load($string)
+ {
+ try {
+ $imagick = new \Imagick();
+
+ $imagick->readImageBlob($string);
+ $imagick->setImageMatte(true);
+
+ return new Image($imagick, $this->createPalette($imagick), $this->getMetadataReader()->readData($string));
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Could not load image from string', $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read($resource)
+ {
+ if (!is_resource($resource)) {
+ throw new InvalidArgumentException('Variable does not contain a stream resource');
+ }
+
+ $content = stream_get_contents($resource);
+
+ try {
+ $imagick = new \Imagick();
+ $imagick->readImageBlob($content);
+ } catch (\ImagickException $e) {
+ throw new RuntimeException('Could not read image from resource', $e->getCode(), $e);
+ }
+
+ return new Image($imagick, $this->createPalette($imagick), $this->getMetadataReader()->readData($content, $resource));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function font($file, $size, ColorInterface $color)
+ {
+ return new Font(new \Imagick(), $file, $size, $color);
+ }
+
+ /**
+ * Returns the palette corresponding to an \Imagick resource colorspace
+ *
+ * @param \Imagick $imagick
+ *
+ * @return CMYK|Grayscale|RGB
+ *
+ * @throws NotSupportedException
+ */
+ private function createPalette(\Imagick $imagick)
+ {
+ switch ($imagick->getImageColorspace()) {
+ case \Imagick::COLORSPACE_RGB:
+ case \Imagick::COLORSPACE_SRGB:
+ return new RGB();
+ case \Imagick::COLORSPACE_CMYK:
+ return new CMYK();
+ case \Imagick::COLORSPACE_GRAY:
+ return new Grayscale();
+ default:
+ throw new NotSupportedException('Only RGB and CMYK colorspace are currently supported');
+ }
+ }
+
+ /**
+ * Returns ImageMagick version
+ *
+ * @param \Imagick $imagick
+ *
+ * @return string
+ */
+ private function getVersion(\Imagick $imagick)
+ {
+ $v = $imagick->getVersion();
+ list($version) = sscanf($v['versionString'], 'ImageMagick %s %04d-%02d-%02d %s %s');
+
+ return $version;
+ }
+}
diff --git a/src/Symfony/Component/Image/LICENSE b/src/Symfony/Component/Image/LICENSE
new file mode 100644
index 0000000000000..ce39894f6a9a2
--- /dev/null
+++ b/src/Symfony/Component/Image/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2016-2017 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/Symfony/Component/Image/README.md b/src/Symfony/Component/Image/README.md
new file mode 100644
index 0000000000000..a31e215cd9b1f
--- /dev/null
+++ b/src/Symfony/Component/Image/README.md
@@ -0,0 +1,3 @@
+Symfony Image Manipulation Component
+====================================
+
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedFOGRA27.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedFOGRA27.icc
new file mode 100644
index 0000000000000..086ac9d92d3e7
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedFOGRA27.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedFOGRA39.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedFOGRA39.icc
new file mode 100644
index 0000000000000..61cb86a595564
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedFOGRA39.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedGRACoL2006.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedGRACoL2006.icc
new file mode 100644
index 0000000000000..1f360b7ed702b
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/CoatedGRACoL2006.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2001Coated.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2001Coated.icc
new file mode 100644
index 0000000000000..5841b46fd6b8c
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2001Coated.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2001Uncoated.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2001Uncoated.icc
new file mode 100644
index 0000000000000..8ade70de0fb46
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2001Uncoated.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2002Newspaper.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2002Newspaper.icc
new file mode 100644
index 0000000000000..18307e270928a
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2002Newspaper.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2003WebCoated.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2003WebCoated.icc
new file mode 100644
index 0000000000000..733dc1dfd872c
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanColor2003WebCoated.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanWebCoated.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanWebCoated.icc
new file mode 100644
index 0000000000000..004b8b9d374b0
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/JapanWebCoated.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebCoatedSWOP.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebCoatedSWOP.icc
new file mode 100644
index 0000000000000..078a6443a712f
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebCoatedSWOP.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebUncoated.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebUncoated.icc
new file mode 100644
index 0000000000000..75efcb259a48c
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebUncoated.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/UncoatedFOGRA29.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/UncoatedFOGRA29.icc
new file mode 100644
index 0000000000000..70e13f5618793
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/UncoatedFOGRA29.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedFOGRA28.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedFOGRA28.icc
new file mode 100644
index 0000000000000..06bf0e63bf08b
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedFOGRA28.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedSWOP2006Grade3.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedSWOP2006Grade3.icc
new file mode 100644
index 0000000000000..ad24acb77680d
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedSWOP2006Grade3.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedSWOP2006Grade5.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedSWOP2006Grade5.icc
new file mode 100644
index 0000000000000..11a82c8fba85f
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/CMYK/WebCoatedSWOP2006Grade5.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/Color Profile Bundling License_10.15.08.pdf b/src/Symfony/Component/Image/Resources/Adobe/Color Profile Bundling License_10.15.08.pdf
new file mode 100644
index 0000000000000..3c508c1857b6c
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/Color Profile Bundling License_10.15.08.pdf differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/Profile Information.pdf b/src/Symfony/Component/Image/Resources/Adobe/Profile Information.pdf
new file mode 100644
index 0000000000000..14afaca1e4aaf
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/Profile Information.pdf differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/AdobeRGB1998.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/AdobeRGB1998.icc
new file mode 100644
index 0000000000000..a79f576b5972a
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/AdobeRGB1998.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/AppleRGB.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/AppleRGB.icc
new file mode 100644
index 0000000000000..ff59d23524b60
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/AppleRGB.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/ColorMatchRGB.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/ColorMatchRGB.icc
new file mode 100644
index 0000000000000..df927eac71f24
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/ColorMatchRGB.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/PAL_SECAM.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/PAL_SECAM.icc
new file mode 100644
index 0000000000000..88c1717d81377
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/PAL_SECAM.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/SMPTE-C.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/SMPTE-C.icc
new file mode 100644
index 0000000000000..8eeeb30532077
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/SMPTE-C.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoHD.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoHD.icc
new file mode 100644
index 0000000000000..57f121a5aa0ad
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoHD.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoNTSC.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoNTSC.icc
new file mode 100644
index 0000000000000..78d1ff371331e
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoNTSC.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoPAL.icc b/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoPAL.icc
new file mode 100644
index 0000000000000..0b7ed9bf0368b
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/RGB/VideoPAL.icc differ
diff --git a/src/Symfony/Component/Image/Resources/Adobe/Trademark Information.pdf b/src/Symfony/Component/Image/Resources/Adobe/Trademark Information.pdf
new file mode 100644
index 0000000000000..03a293ac25f96
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/Adobe/Trademark Information.pdf differ
diff --git a/src/Symfony/Component/Image/Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc b/src/Symfony/Component/Image/Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc
new file mode 100644
index 0000000000000..71e33830223c4
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc differ
diff --git a/src/Symfony/Component/Image/Resources/color.org/sRGB_IEC61966-2-1_no_black_scaling.icc b/src/Symfony/Component/Image/Resources/color.org/sRGB_IEC61966-2-1_no_black_scaling.icc
new file mode 100644
index 0000000000000..d0ef5738a80ac
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/color.org/sRGB_IEC61966-2-1_no_black_scaling.icc differ
diff --git a/src/Symfony/Component/Image/Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC b/src/Symfony/Component/Image/Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC
new file mode 100644
index 0000000000000..24adafeb16d5f
Binary files /dev/null and b/src/Symfony/Component/Image/Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC differ
diff --git a/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php b/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php
new file mode 100644
index 0000000000000..75fbd1f2809f9
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php
@@ -0,0 +1,162 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Histogram\Bucket;
+use Symfony\Component\Image\Image\Histogram\Range;
+
+if (class_exists(\PHPUnit_Framework_Constraint::class)) {
+ abstract class PHPUnitConstraint extends \PHPUnit_Framework_Constraint{}
+} else {
+ abstract class PHPUnitConstraint extends Constraint{}
+}
+
+class IsImageEqual extends PHPUnitConstraint
+{
+ /**
+ * @var \Symfony\Component\Image\Image\ImageInterface
+ */
+ private $value;
+
+ /**
+ * @var float
+ */
+ private $delta;
+
+ /**
+ * @var integer
+ */
+ private $buckets;
+
+ /**
+ * @param \Symfony\Component\Image\Image\ImageInterface $value
+ * @param float $delta
+ * @param integer $buckets
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct($value, $delta = 0.1, $buckets = 4)
+ {
+ if (!$value instanceof ImageInterface) {
+ throw \PHPUnit_Util_InvalidArgumentHelper::factory(1, ImageInterface::class);
+ }
+
+ if (!is_numeric($delta)) {
+ throw \PHPUnit_Util_InvalidArgumentHelper::factory(2, 'numeric');
+ }
+
+ if (!is_integer($buckets) || $buckets <= 0) {
+ throw \PHPUnit_Util_InvalidArgumentHelper::factory(3, 'integer');
+ }
+
+ $this->value = $value;
+ $this->delta = $delta;
+ $this->buckets = $buckets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function evaluate($other, $description = '', $returnResult = false)
+ {
+ if (!$other instanceof ImageInterface) {
+ throw \PHPUnit_Util_InvalidArgumentHelper::factory(1, ImageInterface::class);
+ }
+
+ list($currentRed, $currentGreen, $currentBlue, $currentAlpha) = $this->normalize($this->value);
+ list($otherRed, $otherGreen, $otherBlue, $otherAlpha) = $this->normalize($other);
+
+ $total = 0;
+
+ foreach ($currentRed as $bucket => $count) {
+ $total += abs($count - $otherRed[$bucket]);
+ }
+
+ foreach ($currentGreen as $bucket => $count) {
+ $total += abs($count - $otherGreen[$bucket]);
+ }
+
+ foreach ($currentBlue as $bucket => $count) {
+ $total += abs($count - $otherBlue[$bucket]);
+ }
+
+ foreach ($currentAlpha as $bucket => $count) {
+ $total += abs($count - $otherAlpha[$bucket]);
+ }
+
+ return $total <= $this->delta;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toString()
+ {
+ return sprintf('contains color histogram identical to expected %s', \PHPUnit_Util_Type::toString($this->value));
+ }
+
+ /**
+ * @param \Symfony\Component\Image\Image\ImageInterface $image
+ *
+ * @return array
+ */
+ private function normalize(ImageInterface $image)
+ {
+ $step = (int) round(255 / $this->buckets);
+
+ $red =
+ $green =
+ $blue =
+ $alpha = array();
+
+ for ($i = 1; $i <= $this->buckets; $i++) {
+ $range = new Range(($i - 1) * $step, $i * $step);
+ $red[] = new Bucket($range);
+ $green[] = new Bucket($range);
+ $blue[] = new Bucket($range);
+ $alpha[] = new Bucket($range);
+ }
+
+ foreach ($image->histogram() as $color) {
+ foreach ($red as $bucket) {
+ $bucket->add($color->getRed());
+ }
+
+ foreach ($green as $bucket) {
+ $bucket->add($color->getGreen());
+ }
+
+ foreach ($blue as $bucket) {
+ $bucket->add($color->getBlue());
+ }
+
+ foreach ($alpha as $bucket) {
+ $bucket->add($color->getAlpha());
+ }
+ }
+
+ $total = $image->getSize()->square();
+
+ $callback = function (Bucket $bucket) use ($total) {
+ return count($bucket) / $total;
+ };
+
+ return array(
+ array_map($callback, $red),
+ array_map($callback, $green),
+ array_map($callback, $blue),
+ array_map($callback, $alpha),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php b/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php
new file mode 100644
index 0000000000000..f3717cb33733f
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php
@@ -0,0 +1,252 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Draw;
+
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\Font;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\Point\Center;
+use Symfony\Component\Image\Image\LoaderInterface;
+use Symfony\Component\Image\Tests\TestCase;
+
+abstract class AbstractDrawerTest extends TestCase
+{
+ public function testDrawASmileyFace()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+
+ $canvas->draw()
+ ->chord(new Point(200, 200), new Box(200, 150), 0, 180, $this->getColor('fff'), false)
+ ->ellipse(new Point(125, 100), new Box(50, 50), $this->getColor('fff'))
+ ->ellipse(new Point(275, 100), new Box(50, 50), $this->getColor('fff'), true);
+
+ $canvas->save(__DIR__.'/../Fixtures/smiley.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/smiley.png'));
+
+ unlink(__DIR__.'/../Fixtures/smiley.png');
+ }
+
+ public function testDrawAnEllipse()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+
+ $canvas->draw()
+ ->ellipse(new Center($canvas->getSize()), new Box(300, 200), $this->getColor('fff'), true);
+
+ $canvas->save(__DIR__.'/../Fixtures/ellipse.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/ellipse.png'));
+
+ unlink(__DIR__.'/../Fixtures/ellipse.png');
+ }
+
+ public function testDrawAPieSlice()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+
+ $canvas->draw()
+ ->pieSlice(new Point(200, 150), new Box(100, 200), 45, 135, $this->getColor('fff'), true);
+
+ $canvas->save(__DIR__.'/../Fixtures/pie.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/pie.png'));
+
+ unlink(__DIR__.'/../Fixtures/pie.png');
+ }
+
+ public function testDrawAChord()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+
+ $canvas->draw()
+ ->chord(new Point(200, 150), new Box(100, 200), 45, 135, $this->getColor('fff'), true);
+
+ $canvas->save(__DIR__.'/../Fixtures/chord.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/chord.png'));
+
+ unlink(__DIR__.'/../Fixtures/chord.png');
+ }
+
+ public function testDrawALine()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+
+ $canvas->draw()
+ ->line(new Point(50, 50), new Point(350, 250), $this->getColor('fff'))
+ ->line(new Point(50, 250), new Point(350, 50), $this->getColor('fff'));
+
+ $canvas->save(__DIR__.'/../Fixtures/lines.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/lines.png'));
+
+ unlink(__DIR__.'/../Fixtures/lines.png');
+ }
+
+ public function testDrawAPolygon()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+
+ $canvas->draw()
+ ->polygon(array(
+ new Point(50, 20),
+ new Point(350, 20),
+ new Point(350, 280),
+ new Point(50, 280),
+ ), $this->getColor('fff'), true);
+
+ $canvas->save(__DIR__.'/../Fixtures/polygon.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/polygon.png'));
+
+ unlink(__DIR__.'/../Fixtures/polygon.png');
+ }
+
+ public function testDrawADot()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+
+ $canvas->draw()
+ ->dot(new Point(200, 150), $this->getColor('fff'))
+ ->dot(new Point(200, 151), $this->getColor('fff'))
+ ->dot(new Point(200, 152), $this->getColor('fff'))
+ ->dot(new Point(200, 153), $this->getColor('fff'));
+
+ $canvas->save(__DIR__.'/../Fixtures/dot.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/dot.png'));
+
+ unlink(__DIR__.'/../Fixtures/dot.png');
+ }
+
+ public function testDrawAnArc()
+ {
+ $imagine = $this->getLoader();
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('000'));
+ $size = $canvas->getSize();
+
+ $canvas->draw()
+ ->arc(new Center($size), $size->scale(0.5), 0, 180, $this->getColor('fff'));
+
+ $canvas->save(__DIR__.'/../Fixtures/arc.png');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/arc.png'));
+
+ unlink(__DIR__.'/../Fixtures/arc.png');
+ }
+
+ public function testDrawText()
+ {
+ if (!$this->isFontTestSupported()) {
+ $this->markTestSkipped('This install does not support font tests');
+ }
+
+ $path = __DIR__.'/../Fixtures/font/Arial.ttf';
+ $black = $this->getColor('000');
+ $file36 = __DIR__.'/../Fixtures/bulat36.png';
+ $file24 = __DIR__.'/../Fixtures/bulat24.png';
+ $file18 = __DIR__.'/../Fixtures/bulat18.png';
+ $file12 = __DIR__.'/../Fixtures/bulat12.png';
+
+ $imagine = $this->getLoader();
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('fff'));
+ $font = $imagine->font($path, 36, $black);
+
+ $canvas->draw()
+ ->text('Bulat', $font, new Point(0, 0), 135);
+
+ $canvas->save($file36);
+
+ unset($canvas);
+
+ $this->assertTrue(file_exists($file36));
+
+ unlink($file36);
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('fff'));
+ $font = $imagine->font($path, 24, $black);
+
+ $canvas->draw()
+ ->text('Bulat', $font, new Point(24, 24));
+
+ $canvas->save($file24);
+
+ unset($canvas);
+
+ $this->assertTrue(file_exists($file24));
+
+ unlink($file24);
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('fff'));
+ $font = $imagine->font($path, 18, $black);
+
+ $canvas->draw()
+ ->text('Bulat', $font, new Point(18, 18));
+
+ $canvas->save($file18);
+
+ unset($canvas);
+
+ $this->assertTrue(file_exists($file18));
+
+ unlink($file18);
+
+ $canvas = $imagine->create(new Box(400, 300), $this->getColor('fff'));
+ $font = $imagine->font($path, 12, $black);
+
+ $canvas->draw()
+ ->text('Bulat', $font, new Point(12, 12));
+
+ $canvas->save($file12);
+
+ unset($canvas);
+
+ $this->assertTrue(file_exists($file12));
+
+ unlink($file12);
+ }
+
+ private function getColor($color)
+ {
+ static $palette;
+
+ if (!$palette) {
+ $palette = new RGB();
+ }
+
+ return $palette->color($color);
+ }
+
+ /**
+ * @return LoaderInterface
+ */
+ abstract protected function getLoader();
+
+ abstract protected function isFontTestSupported();
+}
diff --git a/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php b/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php
new file mode 100644
index 0000000000000..1f9428c2381b2
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php
@@ -0,0 +1,141 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Effects;
+
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\LoaderInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Tests\TestCase;
+
+abstract class AbstractEffectsTest extends TestCase
+{
+
+ public function testNegate()
+ {
+ $palette = new RGB();
+ $imagine = $this->getLoader();
+
+ $image = $imagine->create(new Box(20, 20), $palette->color('ff0'));
+ $image->effects()
+ ->negative();
+
+ $this->assertEquals('#0000ff', (string) $image->getColorAt(new Point(10, 10)));
+
+ $image->effects()
+ ->negative();
+
+ $this->assertEquals('#ffff00', (string) $image->getColorAt(new Point(10, 10)));
+ }
+
+ public function testGamma()
+ {
+ $palette = new RGB();
+ $imagine = $this->getLoader();
+
+ $r = 20;
+ $g = 90;
+ $b = 240;
+
+ $image = $imagine->create(new Box(20, 20), $palette->color(array($r, $g, $b)));
+ $image->effects()
+ ->gamma(1.2);
+
+ $pixel = $image->getColorAt(new Point(10, 10));
+
+ $this->assertNotEquals($r, $pixel->getRed());
+ $this->assertNotEquals($g, $pixel->getGreen());
+ $this->assertNotEquals($b, $pixel->getBlue());
+ }
+
+ public function testGrayscale()
+ {
+ $palette = new RGB();
+ $imagine = $this->getLoader();
+
+ $r = 20;
+ $g = 90;
+ $b = 240;
+
+ $image = $imagine->create(new Box(20, 20), $palette->color(array($r, $g, $b)));
+ $image->effects()
+ ->grayscale();
+
+ $pixel = $image->getColorAt(new Point(10, 10));
+
+ $this->assertEquals($this->getGrayValue(), (string) $pixel);
+
+ $greyR = (int) $pixel->getRed();
+ $greyG = (int) $pixel->getGreen();
+ $greyB = (int) $pixel->getBlue();
+
+ $this->assertEquals($greyR, $this->getComponentGrayValue());
+ $this->assertEquals($greyR, $greyG);
+ $this->assertEquals($greyR, $greyB);
+ $this->assertEquals($greyG, $greyB);
+ }
+
+ protected function getGrayValue()
+ {
+ return '#565656';
+ }
+
+ protected function getComponentGrayValue()
+ {
+ return 86;
+ }
+
+ public function testColorize()
+ {
+ $palette = new RGB();
+ $imagine = $this->getLoader();
+
+ $blue = $palette->color('#0000FF');
+
+ $image = $imagine->create(new Box(15, 15), $palette->color('000'));
+ $image->effects()
+ ->colorize($blue);
+
+ $pixel = $image->getColorAt(new Point(10, 10));
+
+ $this->assertEquals((string) $blue, (string) $pixel);
+
+ $this->assertEquals($blue->getRed(), $pixel->getRed());
+ $this->assertEquals($blue->getGreen(), $pixel->getGreen());
+ $this->assertEquals($blue->getBlue(), $pixel->getBlue());
+ }
+
+ public function testBlur()
+ {
+ $palette = new RGB();
+ $imagine = $this->getLoader();
+
+ $image = $imagine->create(new Box(20, 20), $palette->color('#fff'));
+
+ $image->draw()
+ ->line(new Point(10, 0), new Point(10, 20), $palette->color('#000'), 1);
+
+ $image->effects()
+ ->blur();
+
+ $pixel = $image->getColorAt(new Point(9, 10));
+
+ $this->assertNotEquals(255, $pixel->getRed());
+ $this->assertNotEquals(255, $pixel->getGreen());
+ $this->assertNotEquals(255, $pixel->getBlue());
+ }
+
+ /**
+ * @return LoaderInterface
+ */
+ abstract protected function getLoader();
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php b/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php
new file mode 100644
index 0000000000000..18b82364f9053
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Advanced;
+
+use Symfony\Component\Image\Filter\Advanced\Border;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class BorderTest extends FilterTestCase
+{
+ public function testBorderImage()
+ {
+ $color = $this->getMockBuilder(ColorInterface::class)->getMock();
+ $width = 2;
+ $height = 4;
+ $image = $this->getImage();
+
+ $size = $this->getMockBuilder(BoxInterface::class)->getMock();
+ $size->expects($this->once())
+ ->method('getWidth')
+ ->will($this->returnValue($width));
+
+ $size->expects($this->once())
+ ->method('getHeight')
+ ->will($this->returnValue($height));
+
+ $draw = $this->getDrawer();
+ $draw->expects($this->exactly(4))
+ ->method('line')
+ ->will($this->returnValue($draw));
+
+ $image->expects($this->once())
+ ->method('getSize')
+ ->will($this->returnValue($size));
+
+ $image->expects($this->once())
+ ->method('draw')
+ ->will($this->returnValue($draw));
+
+ $filter = new Border($color, $width, $height);
+
+ $this->assertSame($image, $filter->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php b/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php
new file mode 100644
index 0000000000000..b85566cc66c92
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Advanced;
+
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Filter\Advanced\Canvas;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class CanvasTest extends FilterTestCase
+{
+ /**
+ * @covers \Symfony\Component\Image\Filter\Advanced\Canvas::apply
+ *
+ * @dataProvider getDataSet
+ *
+ * @param BoxInterface $size
+ * @param PointInterface $placement
+ * @param ColorInterface $background
+ */
+ public function testShouldCanvasImageAndReturnResult(BoxInterface $size, PointInterface $placement = null, ColorInterface $background = null)
+ {
+ $placement = $placement ?: new Point(0, 0);
+ $image = $this->getImage();
+
+ $canvas = $this->getImage();
+ $canvas->expects($this->once())->method('paste')->with($image, $placement);
+
+ $imagine = $this->getLoader();
+ $imagine->expects($this->once())->method('create')->with($size, $background)->will($this->returnValue($canvas));
+
+ $command = new Canvas($imagine, $size, $placement, $background);
+
+ $this->assertSame($canvas, $command->apply($image));
+ }
+
+ /**
+ * Data provider for testShouldCanvasImageAndReturnResult
+ *
+ * @return array
+ */
+ public function getDataSet()
+ {
+ return array(
+ array(new Box(50, 15), new Point(10, 10), $this->getColor()),
+ array(new Box(300, 25), new Point(15, 15)),
+ array(new Box(123, 23)),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php b/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php
new file mode 100644
index 0000000000000..0d3f457dafd62
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php
@@ -0,0 +1,85 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Advanced;
+
+use Symfony\Component\Image\Filter\Advanced\Grayscale;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+class GrayscaleTest extends FilterTestCase
+{
+ /**
+ * @covers \Symfony\Component\Image\Filter\Advanced\Grayscale::apply
+ *
+ * @dataProvider getDataSet
+ *
+ * @param BoxInterface $size
+ * @param ColorInterface $color
+ * @param ColorInterface $filteredColor
+ */
+ public function testGrayscaling(BoxInterface $size, ColorInterface $color, ColorInterface $filteredColor)
+ {
+ $image = $this->getImage();
+ $imageWidth = $size->getWidth();
+ $imageHeight = $size->getHeight();
+
+ $size = $this->getMockBuilder(BoxInterface::class)->getMock();
+ $size->expects($this->once())
+ ->method('getWidth')
+ ->will($this->returnValue($imageWidth));
+
+ $size->expects($this->once())
+ ->method('getHeight')
+ ->will($this->returnValue($imageHeight));
+
+ $image->expects($this->any())
+ ->method('getSize')
+ ->will($this->returnValue($size));
+
+ $image->expects($this->exactly($imageWidth*$imageHeight))
+ ->method('getColorAt')
+ ->will($this->returnValue($color));
+
+ $color->expects($this->exactly($imageWidth*$imageHeight))
+ ->method('grayscale')
+ ->will($this->returnValue($filteredColor));
+
+ $draw = $this->getDrawer();
+ $draw->expects($this->exactly($imageWidth*$imageHeight))
+ ->method('dot')
+ ->with($this->isInstanceOf(Point::class), $this->equalTo($filteredColor));
+
+ $image->expects($this->exactly($imageWidth*$imageHeight))
+ ->method('draw')
+ ->will($this->returnValue($draw));
+
+ $filter = new Grayscale();
+ $this->assertSame($image, $filter->apply($image));
+ }
+
+ /**
+ * Data provider for testShouldCanvasImageAndReturnResult
+ *
+ * @return array
+ */
+ public function getDataSet()
+ {
+ return array(
+ array(new Box(20, 10), $this->getColor(), $this->getColor()),
+ array(new Box(10, 15), $this->getColor(), $this->getColor()),
+ array(new Box(12, 23), $this->getColor(), $this->getColor()),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php
new file mode 100644
index 0000000000000..b13fd4793fcfb
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+USE Symfony\Component\Image\Filter\Basic\Autorotate;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class AutorotateTest extends FilterTestCase
+{
+ /**
+ * @dataProvider provideMetadataAndRotations
+ */
+ public function testApply($expectedRotation, $hFlipExpected, MetadataBag $metadata)
+ {
+ $image = $this->getImage();
+ $image->expects($this->any())
+ ->method('metadata')
+ ->will($this->returnValue($metadata));
+
+ if (null === $expectedRotation) {
+ $image->expects($this->never())
+ ->method('rotate');
+ } else {
+ $image->expects($this->once())
+ ->method('rotate')
+ ->with($expectedRotation);
+ }
+
+ $image->expects($hFlipExpected ? $this->once() : $this->never())
+ ->method('flipHorizontally');
+
+ $filter = new Autorotate($this->getColor());
+ $filter->apply($image);
+ }
+
+ public function provideMetadataAndRotations()
+ {
+ return array(
+ array(null, false, new MetadataBag(array())),
+ array(null, false, new MetadataBag(array('ifd0.Orientation' => null))),
+ array(null, false, new MetadataBag(array('ifd0.Orientation' => 0))),
+ array(null, false, new MetadataBag(array('ifd0.Orientation' => 1))),
+ array(null, true, new MetadataBag(array('ifd0.Orientation' => 2))),
+ array(180, false, new MetadataBag(array('ifd0.Orientation' => 3))),
+ array(180, true, new MetadataBag(array('ifd0.Orientation' => 4))),
+ array(-90, true, new MetadataBag(array('ifd0.Orientation' => 5))),
+ array(90, false, new MetadataBag(array('ifd0.Orientation' => 6))),
+ array(90, true, new MetadataBag(array('ifd0.Orientation' => 7))),
+ array(-90, false, new MetadataBag(array('ifd0.Orientation' => 8))),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php
new file mode 100644
index 0000000000000..76fb138006761
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+use Symfony\Component\Image\Filter\Basic\Copy;
+
+class CopyTest extends FilterTestCase
+{
+ public function testShouldCopyAndReturnResultingImage()
+ {
+ $command = new Copy();
+ $image = $this->getImage();
+ $clone = $this->getImage();
+
+ $image->expects($this->once())
+ ->method('copy')
+ ->will($this->returnValue($clone));
+
+ $this->assertSame($clone, $command->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php
new file mode 100644
index 0000000000000..4ce0acc61f426
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Filter\Basic\Crop;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class CropTest extends FilterTestCase
+{
+ /**
+ * @covers \Symfony\Component\Image\Filter\Basic\Crop::apply
+ *
+ * @dataProvider getDataSet
+ *
+ * @param PointInterface $start
+ * @param BoxInterface $size
+ */
+ public function testShouldApplyCropAndReturnResult(PointInterface $start, BoxInterface $size)
+ {
+ $image = $this->getImage();
+
+ $command = new Crop($start, $size);
+
+ $image->expects($this->once())
+ ->method('crop')
+ ->with($start, $size)
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $command->apply($image));
+ }
+
+ /**
+ * Provides coordinates and sizes for testShouldApplyCropAndReturnResult
+ *
+ * @return array
+ */
+ public function getDataSet()
+ {
+ return array(
+ array(new Point(0, 0), new Box(40, 50)),
+ array(new Point(0, 15), new Box(50, 32))
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php
new file mode 100644
index 0000000000000..6f8236f9863c2
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\FlipHorizontally;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class FlipHorizontallyTest extends FilterTestCase
+{
+ public function testShouldFlipImage()
+ {
+ $image = $this->getImage();
+ $filter = new FlipHorizontally();
+
+ $image->expects($this->once())
+ ->method('flipHorizontally')
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $filter->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php
new file mode 100644
index 0000000000000..c1014c27f6cd3
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\FlipVertically;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class FlipVerticallyTest extends FilterTestCase
+{
+ public function testShouldFlipImage()
+ {
+ $image = $this->getImage();
+ $filter = new FlipVertically();
+
+ $image->expects($this->once())
+ ->method('flipVertically')
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $filter->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php
new file mode 100644
index 0000000000000..096d9147b4da7
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Filter\Basic\Paste;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class PasteTest extends FilterTestCase
+{
+ public function testShouldFlipImage()
+ {
+ $start = new Point(0, 0);
+ $image = $this->getImage();
+ $toPaste = $this->getImage();
+ $filter = new Paste($toPaste, $start);
+
+ $image->expects($this->once())
+ ->method('paste')
+ ->with($toPaste, $start)
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $filter->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php
new file mode 100644
index 0000000000000..2c0569298331c
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\Resize;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class ResizeTest extends FilterTestCase
+{
+ /**
+ * @covers \Symfony\Component\Image\Filter\Basic\Resize::apply
+ *
+ * @dataProvider getDataSet
+ *
+ * @param BoxInterface $size
+ */
+ public function testShouldResizeImageAndReturnResult(BoxInterface $size)
+ {
+ $image = $this->getImage();
+
+ $image->expects($this->once())
+ ->method('resize')
+ ->with($size)
+ ->will($this->returnValue($image));
+
+ $command = new Resize($size);
+
+ $this->assertSame($image, $command->apply($image));
+ }
+
+ /**
+ * Data provider for testShouldResizeImageAndReturnResult
+ *
+ * @return array
+ */
+ public function getDataSet()
+ {
+ return array(
+ array(new Box(50, 15)),
+ array(new Box(300, 25)),
+ array(new Box(123, 23)),
+ array(new Box(45, 23))
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php
new file mode 100644
index 0000000000000..e9ee8e0572ceb
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\Rotate;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class RotateTest extends FilterTestCase
+{
+ public function testShouldRotateImageAndReturnResult()
+ {
+ $image = $this->getImage();
+ $angle = 90;
+ $command = new Rotate($angle);
+
+ $image->expects($this->once())
+ ->method('rotate')
+ ->with($angle)
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $command->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php
new file mode 100644
index 0000000000000..b80987a5791f3
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\Save;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class SaveTest extends FilterTestCase
+{
+ public function testShouldSaveImageAndReturnResult()
+ {
+ $image = $this->getImage();
+ $path = '/path/to/image.jpg';
+ $command = new Save($path);
+
+ $image->expects($this->once())
+ ->method('save')
+ ->with($path)
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $command->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php
new file mode 100644
index 0000000000000..38e5aebf541ed
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\Show;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class ShowTest extends FilterTestCase
+{
+ public function testShouldShowImageAndReturnResult()
+ {
+ $image = $this->getImage();
+ $format = 'jpg';
+ $command = new Show($format);
+
+ $image->expects($this->once())
+ ->method('show')
+ ->with($format)
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $command->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php
new file mode 100644
index 0000000000000..208ee36b2ddc6
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\Strip;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class StripTest extends FilterTestCase
+{
+ public function testShouldStripImage()
+ {
+ $image = $this->getImage();
+ $filter = new Strip();
+
+ $image->expects($this->once())
+ ->method('strip')
+ ->will($this->returnValue($image));
+
+ $this->assertSame($image, $filter->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php
new file mode 100644
index 0000000000000..b24242c2e6b2c
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\Thumbnail;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\ManipulatorInterface;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class ThumbnailTest extends FilterTestCase
+{
+ public function testShouldMakeAThumbnail()
+ {
+ $image = $this->getImage();
+ $thumbnail = $this->getImage();
+ $size = new Box(50, 50);
+ $filter = new Thumbnail($size);
+
+ $image->expects($this->once())
+ ->method('thumbnail')
+ ->with($size, ManipulatorInterface::THUMBNAIL_INSET)
+ ->will($this->returnValue($thumbnail));
+
+ $this->assertSame($thumbnail, $filter->apply($image));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php
new file mode 100644
index 0000000000000..6be7368926ccc
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php
@@ -0,0 +1,115 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter\Basic;
+
+use Symfony\Component\Image\Filter\Basic\WebOptimization;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Tests\Filter\FilterTestCase;
+
+class WebOptimizationTest extends FilterTestCase
+{
+ public function testShouldNotSave()
+ {
+ $image = $this->getImage();
+ $filter = new WebOptimization();
+
+ $image->expects($this->once())
+ ->method('usePalette')
+ ->with($this->isInstanceOf(RGB::class))
+ ->will($this->returnValue($image));
+
+ $image->expects($this->once())
+ ->method('strip')
+ ->will($this->returnValue($image));
+
+ $image->expects($this->never())
+ ->method('save');
+
+ $this->assertSame($image, $filter->apply($image));
+ }
+
+ public function testShouldSaveWithCallbackAndCustomOption()
+ {
+ $image = $this->getImage();
+ $result = '/path/to/ploum';
+ $path = function (ImageInterface $image) use ($result) { return $result; };
+ $filter = new WebOptimization($path, array(
+ 'custom-option' => 'custom-value',
+ 'resolution-y' => 100,
+ ));
+ $capturedOptions = null;
+
+ $image->expects($this->once())
+ ->method('usePalette')
+ ->with($this->isInstanceOf(RGB::class))
+ ->will($this->returnValue($image));
+
+ $image->expects($this->once())
+ ->method('strip')
+ ->will($this->returnValue($image));
+
+ $image->expects($this->once())
+ ->method('save')
+ ->with($this->equalTo($result), $this->isType('array'))
+ ->will($this->returnCallback(function ($path, $options) use (&$capturedOptions, $image) {
+ $capturedOptions = $options;
+
+ return $image;
+ }));
+
+ $this->assertSame($image, $filter->apply($image));
+
+ $this->assertCount(4, $capturedOptions);
+ $this->assertEquals('custom-value', $capturedOptions['custom-option']);
+ $this->assertEquals(ImageInterface::RESOLUTION_PIXELSPERINCH, $capturedOptions['resolution-units']);
+ $this->assertEquals(72, $capturedOptions['resolution-x']);
+ $this->assertEquals(100, $capturedOptions['resolution-y']);
+ }
+
+ public function testShouldSaveWithPathAndCustomOption()
+ {
+ $image = $this->getImage();
+ $path = '/path/to/dest';
+ $filter = new WebOptimization($path, array(
+ 'custom-option' => 'custom-value',
+ 'resolution-y' => 100,
+ ));
+ $capturedOptions = null;
+
+ $image->expects($this->once())
+ ->method('usePalette')
+ ->with($this->isInstanceOf(RGB::class))
+ ->will($this->returnValue($image));
+
+ $image->expects($this->once())
+ ->method('strip')
+ ->will($this->returnValue($image));
+
+ $image->expects($this->once())
+ ->method('save')
+ ->with($this->equalTo($path), $this->isType('array'))
+ ->will($this->returnCallback(function ($path, $options) use (&$capturedOptions, $image) {
+ $capturedOptions = $options;
+
+ return $image;
+ }));
+
+ $this->assertSame($image, $filter->apply($image));
+
+ $this->assertCount(4, $capturedOptions);
+ $this->assertEquals('custom-value', $capturedOptions['custom-option']);
+ $this->assertEquals(ImageInterface::RESOLUTION_PIXELSPERINCH, $capturedOptions['resolution-units']);
+ $this->assertEquals(72, $capturedOptions['resolution-x']);
+ $this->assertEquals(100, $capturedOptions['resolution-y']);
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php b/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php
new file mode 100644
index 0000000000000..aadc75159864f
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php
@@ -0,0 +1,24 @@
+getLoader()->create(new Box(200, 200));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/FilterTestCase.php b/src/Symfony/Component/Image/Tests/Filter/FilterTestCase.php
new file mode 100644
index 0000000000000..cdaeb3475696e
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/FilterTestCase.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter;
+
+use Symfony\Component\Image\Draw\DrawerInterface;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\LoaderInterface;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Tests\TestCase;
+
+abstract class FilterTestCase extends TestCase
+{
+ protected function getImage()
+ {
+ return $this->getMockBuilder(ImageInterface::class)->getMock();
+ }
+
+ protected function getLoader()
+ {
+ return $this->getMockBuilder(LoaderInterface::class)->getMock();
+ }
+
+ protected function getDrawer()
+ {
+ return $this->getMockBuilder(DrawerInterface::class)->getMock();
+ }
+
+ protected function getPalette()
+ {
+ return $this->getMockBuilder(PaletteInterface::class)->getMock();
+ }
+
+ protected function getColor()
+ {
+ return $this->getMockBuilder(ColorInterface::class)->getMock();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/LoaderAwareTest.php b/src/Symfony/Component/Image/Tests/Filter/LoaderAwareTest.php
new file mode 100644
index 0000000000000..6176a1475c2e8
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/LoaderAwareTest.php
@@ -0,0 +1,86 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter;
+
+use Symfony\Component\Image\Filter\Transformation;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\LoaderInterface;
+
+/**
+ * LoaderAwareTest.
+ */
+class LoaderAwareTest extends FilterTestCase
+{
+ /**
+ * Test if filter works when passing Loader instance directly.
+ */
+ public function testFilterWorksWhenPassedLoaderAndCalledDirectly()
+ {
+ $loaderMock = $this->getLoaderMock();
+
+ $filter = new DummyLoaderAwareFilter();
+ $filter->setLoader($loaderMock);
+ $image = $filter->apply($this->getImage());
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ }
+
+ /**
+ * Test if filter works when passing Loader instance via
+ * Transformation.
+ */
+ public function testFilterWorksWhenPassedLoaderViaTransformation()
+ {
+ $loaderMock = $this->getLoaderMock();
+
+ $filters = new Transformation($loaderMock);
+ $filters->add(new DummyLoaderAwareFilter());
+ $image = $filters->apply($this->getImage());
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ }
+
+ /**
+ * Test if filter throws exception when called directly without
+ * passing Loader instance.
+ *
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ */
+ public function testFilterThrowsExceptionWhenCalledDirectly()
+ {
+ $filter = new DummyLoaderAwareFilter();
+ $filter->apply($this->getImage());
+ }
+
+ /**
+ * Test if filter throws exception via Transformation without
+ * passing Loader instance.
+ *
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ */
+ public function testFilterThrowsExceptionViaTransformation()
+ {
+ $filters = new Transformation();
+ $filters->add(new DummyLoaderAwareFilter());
+ $filters->apply($this->getImage());
+ }
+
+ protected function getLoaderMock()
+ {
+ $imagineMock = $this->getMockBuilder(LoaderInterface::class)->getMock();
+ $imagineMock->expects($this->once())
+ ->method('create')
+ ->will($this->returnValue($this->getImage()));
+
+ return $imagineMock;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php b/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php
new file mode 100644
index 0000000000000..956ce09b93623
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php
@@ -0,0 +1,182 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Filter;
+
+use Symfony\Component\Image\Filter\FilterInterface;
+use Symfony\Component\Image\Filter\Transformation;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\ManipulatorInterface;
+
+class TransformationTest extends FilterTestCase
+{
+ public function testSimpleStack()
+ {
+ $image = $this->getImage();
+ $size = new Box(50, 50);
+ $path = sys_get_temp_dir();
+
+ $image->expects($this->once())
+ ->method('resize')
+ ->with($size)
+ ->will($this->returnValue($image));
+
+ $image->expects($this->once())
+ ->method('save')
+ ->with($path)
+ ->will($this->returnValue($image));
+
+ $transformation = new Transformation();
+ $this->assertSame($image, $transformation->resize($size)
+ ->save($path)
+ ->apply($image)
+ );
+ }
+
+ public function testComplexFlow()
+ {
+ $image = $this->getImage();
+ $clone = $this->getImage();
+ $thumbnail = $this->getImage();
+ $path = sys_get_temp_dir();
+ $size = new Box(50, 50);
+ $resize = new Box(200, 200);
+ $angle = 90;
+ $background = $this->getPalette()->color('fff');
+
+ $image->expects($this->once())
+ ->method('resize')
+ ->with($resize)
+ ->will($this->returnValue($image));
+
+ $image->expects($this->once())
+ ->method('copy')
+ ->will($this->returnValue($clone));
+
+ $clone->expects($this->once())
+ ->method('rotate')
+ ->with($angle, $background)
+ ->will($this->returnValue($clone));
+
+ $clone->expects($this->once())
+ ->method('thumbnail')
+ ->with($size, ManipulatorInterface::THUMBNAIL_INSET)
+ ->will($this->returnValue($thumbnail));
+
+ $thumbnail->expects($this->once())
+ ->method('save')
+ ->with($path)
+ ->will($this->returnValue($thumbnail));
+
+ $transformation = new Transformation();
+
+ $transformation->resize($resize)
+ ->copy()
+ ->rotate($angle, $background)
+ ->thumbnail($size, ManipulatorInterface::THUMBNAIL_INSET)
+ ->save($path);
+
+ $this->assertSame($thumbnail, $transformation->apply($image));
+ }
+
+ public function testCropFlipPasteShow()
+ {
+ $img1 = $this->getImage();
+ $img2 = $this->getImage();
+ $start = new Point(0, 0);
+ $size = new Box(50, 50);
+
+ $img1->expects($this->once())
+ ->method('paste')
+ ->with($img2, $start)
+ ->will($this->returnValue($img1));
+
+ $img1->expects($this->once())
+ ->method('show')
+ ->with('png')
+ ->will($this->returnValue($img1));
+
+ $img2->expects($this->once())
+ ->method('flipHorizontally')
+ ->will($this->returnValue($img2));
+
+ $img2->expects($this->once())
+ ->method('flipVertically')
+ ->will($this->returnValue($img2));
+
+ $img2->expects($this->once())
+ ->method('crop')
+ ->with($start, $size)
+ ->will($this->returnValue($img2));
+
+ $transformation2 = new Transformation();
+ $transformation2->flipHorizontally()
+ ->flipVertically()
+ ->crop($start, $size);
+
+ $transformation1 = new Transformation();
+ $transformation1->paste($transformation2->apply($img2), $start)
+ ->show('png')
+ ->apply($img1);
+ }
+
+ public function testFilterSorting()
+ {
+ $filter1 = new TestFilter();
+ $filter2 = new TestFilter();
+ $filter3 = new TestFilter();
+
+ $transformation1 = new Transformation();
+ $transformation1
+ ->add($filter1, 5)
+ ->add($filter2, -3)
+ ->add($filter3);
+
+ $expected1 = array(
+ $filter2,
+ $filter3,
+ $filter1,
+ );
+
+ $transformation2 = new Transformation();
+ $transformation2
+ ->add($filter1)
+ ->add($filter2)
+ ->add($filter3);
+
+ $expected2 = array(
+ $filter1,
+ $filter2,
+ $filter3,
+ );
+
+ $this->assertSame($expected1, $transformation1->getFilters());
+ $this->assertSame($expected2, $transformation2->getFilters());
+ }
+
+ public function testGetEmptyFilters()
+ {
+ $transformation = new Transformation();
+ $this->assertSame(array(), $transformation->getFilters());
+ }
+}
+
+class TestFilter implements FilterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/100-percent-black.png b/src/Symfony/Component/Image/Tests/Fixtures/100-percent-black.png
new file mode 100644
index 0000000000000..168f5f726c4ce
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/100-percent-black.png differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/65-percent-black.png b/src/Symfony/Component/Image/Tests/Fixtures/65-percent-black.png
new file mode 100644
index 0000000000000..61b0eb6630636
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/65-percent-black.png differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/anima.gif b/src/Symfony/Component/Image/Tests/Fixtures/anima.gif
new file mode 100644
index 0000000000000..5bca0266ffbbf
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/anima.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/anima2.gif b/src/Symfony/Component/Image/Tests/Fixtures/anima2.gif
new file mode 100644
index 0000000000000..6af13d882cc6d
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/anima2.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/anima3.gif b/src/Symfony/Component/Image/Tests/Fixtures/anima3.gif
new file mode 100644
index 0000000000000..e70888089bf67
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/anima3.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/blue.gif b/src/Symfony/Component/Image/Tests/Fixtures/blue.gif
new file mode 100644
index 0000000000000..5af85c92f4976
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/blue.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/cat.gif b/src/Symfony/Component/Image/Tests/Fixtures/cat.gif
new file mode 100644
index 0000000000000..961fe9686ca4a
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/cat.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/crop/anima3-topleft.gif b/src/Symfony/Component/Image/Tests/Fixtures/crop/anima3-topleft.gif
new file mode 100644
index 0000000000000..08abb5a869b62
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/crop/anima3-topleft.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/example.svg b/src/Symfony/Component/Image/Tests/Fixtures/example.svg
new file mode 100644
index 0000000000000..438734c1707c6
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Fixtures/example.svg
@@ -0,0 +1,69 @@
+
+
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/exifOrientation/90.jpg b/src/Symfony/Component/Image/Tests/Fixtures/exifOrientation/90.jpg
new file mode 100644
index 0000000000000..335a69731a775
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/exifOrientation/90.jpg differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/font/Arial.ttf b/src/Symfony/Component/Image/Tests/Fixtures/font/Arial.ttf
new file mode 100644
index 0000000000000..ab68fb197d447
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/font/Arial.ttf differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/google.png b/src/Symfony/Component/Image/Tests/Fixtures/google.png
new file mode 100644
index 0000000000000..763f5627995f6
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/google.png differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/invalid-icc-profile.jpg b/src/Symfony/Component/Image/Tests/Fixtures/invalid-icc-profile.jpg
new file mode 100644
index 0000000000000..924ff27cd38e3
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/invalid-icc-profile.jpg differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/invalid-image.jpg b/src/Symfony/Component/Image/Tests/Fixtures/invalid-image.jpg
new file mode 100644
index 0000000000000..c12adcc160aab
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Fixtures/invalid-image.jpg
@@ -0,0 +1 @@
+This file is not a valid image.
\ No newline at end of file
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/large.jpg b/src/Symfony/Component/Image/Tests/Fixtures/large.jpg
new file mode 100644
index 0000000000000..81c47e565bf28
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/large.jpg differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/pink.gif b/src/Symfony/Component/Image/Tests/Fixtures/pink.gif
new file mode 100644
index 0000000000000..3711f47a6894f
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/pink.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/pixel-CMYK.jpg b/src/Symfony/Component/Image/Tests/Fixtures/pixel-CMYK.jpg
new file mode 100644
index 0000000000000..4841c149b040b
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/pixel-CMYK.jpg differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/pixel-GBR.jpg b/src/Symfony/Component/Image/Tests/Fixtures/pixel-GBR.jpg
new file mode 100644
index 0000000000000..4cc22656a8dae
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/pixel-GBR.jpg differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/pixel-grayscale.jpg b/src/Symfony/Component/Image/Tests/Fixtures/pixel-grayscale.jpg
new file mode 100644
index 0000000000000..89aebea6cec53
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/pixel-grayscale.jpg differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/resize/210-design-19933.jpg b/src/Symfony/Component/Image/Tests/Fixtures/resize/210-design-19933.jpg
new file mode 100755
index 0000000000000..bcb9dad990c4b
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/resize/210-design-19933.jpg differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/resize/anima3-150x100.gif b/src/Symfony/Component/Image/Tests/Fixtures/resize/anima3-150x100.gif
new file mode 100644
index 0000000000000..e5c52ebb07443
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/resize/anima3-150x100.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/results/in_out/.placeholder b/src/Symfony/Component/Image/Tests/Fixtures/results/in_out/.placeholder
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/sample.gif b/src/Symfony/Component/Image/Tests/Fixtures/sample.gif
new file mode 100644
index 0000000000000..91c686d9f356c
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/sample.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/trans.gif b/src/Symfony/Component/Image/Tests/Fixtures/trans.gif
new file mode 100644
index 0000000000000..b58aec91fb3e9
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/trans.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/trans.png b/src/Symfony/Component/Image/Tests/Fixtures/trans.png
new file mode 100644
index 0000000000000..89d9bf1724f29
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/trans.png differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/xparent.gif b/src/Symfony/Component/Image/Tests/Fixtures/xparent.gif
new file mode 100644
index 0000000000000..bed5790521060
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/xparent.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Fixtures/yellow.gif b/src/Symfony/Component/Image/Tests/Fixtures/yellow.gif
new file mode 100644
index 0000000000000..56d595b50597b
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Fixtures/yellow.gif differ
diff --git a/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php
new file mode 100644
index 0000000000000..e1faf863ca029
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php
@@ -0,0 +1,55 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Functional;
+
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Gd\Loader;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Tests\TestCase;
+
+class GdTransparentGifHandlingTest extends TestCase
+{
+ private function getLoader()
+ {
+ try {
+ $imagine = new Loader();
+ } catch (RuntimeException $e) {
+ $this->markTestSkipped($e->getMessage());
+ }
+
+ return $imagine;
+ }
+
+ public function testShouldResize()
+ {
+ $imagine = $this->getLoader();
+ $new = sys_get_temp_dir()."/sample.jpeg";
+
+ $image = $imagine->open(__DIR__.'/../Fixtures/xparent.gif');
+ $size = $image->getSize()->scale(0.5);
+
+ $image
+ ->resize($size)
+ ;
+
+ $image = $imagine
+ ->create($size)
+ ->paste($image, new Point(0, 0))
+ ->save($new)
+ ;
+
+ $this->assertSame(272, $image->getSize()->getWidth());
+ $this->assertSame(171, $image->getSize()->getHeight());
+
+ unlink($new);
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gd/DrawerTest.php b/src/Symfony/Component/Image/Tests/Gd/DrawerTest.php
new file mode 100644
index 0000000000000..63feeaa14c051
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gd/DrawerTest.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gd;
+
+use Symfony\Component\Image\Gd\Loader;
+use Symfony\Component\Image\Tests\Draw\AbstractDrawerTest;
+
+class DrawerTest extends AbstractDrawerTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!function_exists('gd_info')) {
+ $this->markTestSkipped('Gd not installed');
+ }
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function isFontTestSupported()
+ {
+ $infos = gd_info();
+
+ return isset($infos['FreeType Support']) ? $infos['FreeType Support'] : false;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gd/EffectsTest.php b/src/Symfony/Component/Image/Tests/Gd/EffectsTest.php
new file mode 100644
index 0000000000000..ef00329915a43
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gd/EffectsTest.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gd;
+
+use Symfony\Component\Image\Gd\Loader;
+use Symfony\Component\Image\Tests\Effects\AbstractEffectsTest;
+
+class EffectsTest extends AbstractEffectsTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!function_exists('gd_info')) {
+ $this->markTestSkipped('Gd not installed');
+ }
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gd/ImageTest.php b/src/Symfony/Component/Image/Tests/Gd/ImageTest.php
new file mode 100644
index 0000000000000..e00387f88ac34
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gd/ImageTest.php
@@ -0,0 +1,132 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gd;
+
+use Symfony\Component\Image\Gd\Loader;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Tests\Image\AbstractImageTest;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Exception\RuntimeException;
+
+class ImageTest extends AbstractImageTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!function_exists('gd_info')) {
+ $this->markTestSkipped('Gd not installed');
+ }
+ }
+
+ public function testImageResolutionChange()
+ {
+ $this->markTestSkipped('GD driver does not support resolution options');
+ }
+
+ public function provideFilters()
+ {
+ return array(
+ array(ImageInterface::FILTER_UNDEFINED),
+ );
+ }
+
+ public function providePalettes()
+ {
+ return array(
+ array(RGB::class, array(255, 0, 0)),
+ );
+ }
+
+ public function provideFromAndToPalettes()
+ {
+ return array(
+ array(
+ RGB::class,
+ RGB::class,
+ array(10, 10, 10),
+ ),
+ );
+ }
+
+ public function testProfile()
+ {
+ try {
+ parent::testProfile();
+ $this->fail('A RuntimeException should have been raised');
+ } catch (RuntimeException $e) {
+ $this->assertSame('GD driver does not support color profiles', $e->getMessage());
+ }
+ }
+
+ public function testPaletteIsGrayIfGrayImage()
+ {
+ $this->markTestSkipped('Gd does not support Gray colorspace');
+ }
+
+ public function testPaletteIsCMYKIfCMYKImage()
+ {
+ $this->markTestSkipped('GD driver does not recognize CMYK images properly');
+ }
+
+ public function testGetColorAtCMYK()
+ {
+ $this->markTestSkipped('GD driver does not recognize CMYK images properly');
+ }
+
+ public function testChangeColorSpaceAndStripImage()
+ {
+ $this->markTestSkipped('GD driver does not support ICC profiles');
+ }
+
+ public function testStripImageWithInvalidProfile()
+ {
+ $this->markTestSkipped('GD driver does not support ICC profiles');
+ }
+
+ public function testStripGBRImageHasGoodColors()
+ {
+ $this->markTestSkipped('GD driver does not support ICC profiles');
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function supportMultipleLayers()
+ {
+ return false;
+ }
+
+ public function testRotateWithNoBackgroundColor()
+ {
+ if (version_compare(PHP_VERSION, '5.5', '>=')) {
+ // see https://bugs.php.net/bug.php?id=65148
+ $this->markTestSkipped('Disabling test while bug #65148 is open');
+ }
+
+ parent::testRotateWithNoBackgroundColor();
+ }
+
+ /**
+ * @dataProvider provideVariousSources
+ */
+ public function testResolutionOnSave($source)
+ {
+ $this->markTestSkipped('Gd only supports 72 dpi resolution');
+ }
+
+ protected function getImageResolution(ImageInterface $image)
+ {
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gd/LayersTest.php b/src/Symfony/Component/Image/Tests/Gd/LayersTest.php
new file mode 100644
index 0000000000000..4752d2f0176ce
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gd/LayersTest.php
@@ -0,0 +1,127 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gd;
+
+use Symfony\Component\Image\Gd\Layers;
+use Symfony\Component\Image\Gd\Image;
+use Symfony\Component\Image\Gd\Loader;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Tests\Image\AbstractLayersTest;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+
+class LayersTest extends AbstractLayersTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!function_exists('gd_info')) {
+ $this->markTestSkipped('Gd not installed');
+ }
+ }
+
+ public function testCount()
+ {
+ $resource = imagecreate(20, 20);
+ $palette = $this->getMockBuilder(PaletteInterface::class)->getMock();
+ $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource);
+
+ $this->assertCount(1, $layers);
+ }
+
+ public function testGetLayer()
+ {
+ $resource = imagecreate(20, 20);
+ $palette = $this->getMockBuilder(PaletteInterface::class)->getMock();
+ $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource);
+
+ foreach ($layers as $layer) {
+ $this->assertInstanceOf(ImageInterface::class, $layer);
+ }
+ }
+
+ public function testLayerArrayAccess()
+ {
+ $image = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $layers = $image->layers();
+
+ $this->assertLayersEquals($image, $layers[0]);
+ $this->assertTrue(isset($layers[0]));
+ }
+
+ public function testLayerAddGetSetRemove()
+ {
+ $image = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $layers = $image->layers();
+
+ $this->assertLayersEquals($image, $layers->get(0));
+ $this->assertTrue($layers->has(0));
+ }
+
+ public function testLayerArrayAccessInvalidArgumentExceptions($offset = null)
+ {
+ $this->markTestSkipped('Gd does not fully support layers array access');
+ }
+
+ public function testLayerArrayAccessOutOfBoundsExceptions($offset = null)
+ {
+ $this->markTestSkipped('Gd does not fully support layers array access');
+ }
+
+ public function testAnimateEmpty()
+ {
+ $this->markTestSkipped('Gd does not support animated gifs');
+ }
+
+ public function testAnimateLoaded()
+ {
+ $this->markTestSkipped('Gd does not support animated gifs');
+ }
+
+ /**
+ * @dataProvider provideAnimationParameters
+ */
+ public function testAnimateWithParameters($delay, $loops)
+ {
+ $this->markTestSkipped('Gd does not support animated gifs');
+ }
+
+ /**
+ * @dataProvider provideAnimationParameters
+ */
+ public function testAnimateWithWrongParameters($delay, $loops)
+ {
+ $this->markTestSkipped('Gd does not support animated gifs');
+ }
+
+ public function getImage($path = null)
+ {
+ return new Image(imagecreatetruecolor(10, 10), new RGB(), new MetadataBag());
+ }
+
+ public function getLayers(ImageInterface $image, $resource)
+ {
+ return new Layers($image, new RGB(), $resource);
+ }
+
+ public function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function assertLayersEquals($expected, $actual)
+ {
+ $this->assertEquals($expected->getGdResource(), $actual->getGdResource());
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gd/LoaderTest.php b/src/Symfony/Component/Image/Tests/Gd/LoaderTest.php
new file mode 100644
index 0000000000000..aab0bb3e5f24a
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gd/LoaderTest.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gd;
+
+use Symfony\Component\Image\Gd\Loader;
+use Symfony\Component\Image\Tests\Image\AbstractLoaderTest;
+use Symfony\Component\Image\Image\Box;
+
+class LoaderTest extends AbstractLoaderTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!function_exists('gd_info')) {
+ $this->markTestSkipped('Gd not installed');
+ }
+ }
+
+ protected function getEstimatedFontBox()
+ {
+ if (defined('HHVM_VERSION_ID')) {
+ return new Box(114, 46);
+ }
+
+ if (PHP_VERSION_ID >= 70000) {
+ return new Box(112, 45);
+ }
+
+ return new Box(112, 46);
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function isFontTestSupported()
+ {
+ $infos = gd_info();
+
+ return isset($infos['FreeType Support']) ? $infos['FreeType Support'] : false;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gmagick/DrawerTest.php b/src/Symfony/Component/Image/Tests/Gmagick/DrawerTest.php
new file mode 100644
index 0000000000000..329ac5a2d2a1d
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gmagick/DrawerTest.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gmagick;
+
+use Symfony\Component\Image\Gmagick\Loader;
+use Symfony\Component\Image\Tests\Draw\AbstractDrawerTest;
+
+class DrawerTest extends AbstractDrawerTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Gmagick')) {
+ $this->markTestSkipped('Gmagick is not installed');
+ }
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function isFontTestSupported()
+ {
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gmagick/EffectsTest.php b/src/Symfony/Component/Image/Tests/Gmagick/EffectsTest.php
new file mode 100644
index 0000000000000..302acc894a899
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gmagick/EffectsTest.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gmagick;
+
+use Symfony\Component\Image\Gmagick\Loader;
+use Symfony\Component\Image\Tests\Effects\AbstractEffectsTest;
+
+class EffectsTest extends AbstractEffectsTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Gmagick')) {
+ $this->markTestSkipped('Gmagick is not installed');
+ }
+ }
+
+ public function testColorize()
+ {
+ $this->setExpectedException(\RuntimeException::class);
+ parent::testColorize();
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php b/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php
new file mode 100644
index 0000000000000..9eb4a0924c3c9
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gmagick;
+
+use Symfony\Component\Image\Gmagick\Loader;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\CMYK;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Tests\Image\AbstractImageTest;
+
+class ImageTest extends AbstractImageTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // disable GC while https://bugs.php.net/bug.php?id=63677 is still open
+ // If GC enabled, Gmagick unit tests fail
+ gc_disable();
+
+ if (!class_exists('Gmagick')) {
+ $this->markTestSkipped('Gmagick is not installed');
+ }
+ }
+
+ // We redeclare this test because Gmagick does not support alpha
+ public function testGetColorAt()
+ {
+ $color = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/65-percent-black.png')
+ ->getColorAt(new Point(0, 0));
+
+ $this->assertEquals('#000000', (string) $color);
+ // Gmagick does not supports alpha
+ $this->assertTrue($color->isOpaque());
+ }
+
+ public function provideFromAndToPalettes()
+ {
+ return array(
+ array(
+ RGB::class,
+ CMYK::class,
+ array(10, 10, 10),
+ ),
+ array(
+ CMYK::class,
+ RGB::class,
+ array(10, 10, 10, 0),
+ ),
+ );
+ }
+
+ public function providePalettes()
+ {
+ return array(
+ array(RGB::class, array(255, 0, 0)),
+ array(CMYK::class, array(10, 0, 0, 0)),
+ );
+ }
+
+ public function testPaletteIsGrayIfGrayImage()
+ {
+ $this->markTestSkipped('Gmagick does not support Gray colorspace, because of the lack omg image type support');
+ }
+
+ public function testGetColorAtCMYK()
+ {
+ $this->markTestSkipped('Gmagick fails to read CMYK colors properly, see https://bugs.php.net/bug.php?id=67435');
+ }
+
+ public function testImageCreatedAlpha()
+ {
+ $this->markTestSkipped('Alpha transparency is not supported by Gmagick');
+ }
+
+ public function testFillAlphaPrecision()
+ {
+ $this->markTestSkipped('Alpha transparency is not supported by Gmagick');
+ }
+
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function supportMultipleLayers()
+ {
+ return true;
+ }
+
+ protected function getImageResolution(ImageInterface $image)
+ {
+ return $image->getGmagick()->getimageresolution();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gmagick/LayersTest.php b/src/Symfony/Component/Image/Tests/Gmagick/LayersTest.php
new file mode 100644
index 0000000000000..36f23e4dd3efb
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gmagick/LayersTest.php
@@ -0,0 +1,97 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gmagick;
+
+use Symfony\Component\Image\Gmagick\Layers;
+use Symfony\Component\Image\Gmagick\Image;
+use Symfony\Component\Image\Gmagick\Loader;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Tests\Image\AbstractLayersTest;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+
+class LayersTest extends AbstractLayersTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Gmagick')) {
+ $this->markTestSkipped('Gmagick is not installed');
+ }
+ }
+
+ public function testCount()
+ {
+ $palette = new RGB();
+ $resource = $this->getMockBuilder('\Gmagick')->getMock();
+
+ $resource->expects($this->once())
+ ->method('getnumberimages')
+ ->will($this->returnValue(42));
+
+ $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource);
+
+ $this->assertCount(42, $layers);
+ }
+
+ public function testGetLayer()
+ {
+ $palette = new RGB();
+ $resource = $this->getMockBuilder('\Gmagick')->getMock();
+
+ $resource->expects($this->any())
+ ->method('getnumberimages')
+ ->will($this->returnValue(2));
+
+ $layer = $this->getMockBuilder('\Gmagick')->getMock();
+
+ $resource->expects($this->any())
+ ->method('getimage')
+ ->will($this->returnValue($layer));
+
+ $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource);
+
+ foreach ($layers as $layer) {
+ $this->assertInstanceOf(ImageInterface::class, $layer);
+ }
+ }
+
+ public function testAnimateEmpty()
+ {
+ $this->markTestSkipped('Animate empty is skipped due to https://bugs.php.net/bug.php?id=62309');
+ }
+
+ public function getImage($path = null)
+ {
+ if ($path) {
+ return new Image(new \Gmagick($path), new RGB(), new MetadataBag());
+ } else {
+ return new Image(new \Gmagick(), new RGB(), new MetadataBag());
+ }
+ }
+
+ public function getLoader()
+ {
+ return new Loader();
+ }
+
+ public function getLayers(ImageInterface $image, $resource)
+ {
+ return new Layers($image, $resource, new MetadataBag());
+ }
+
+ protected function assertLayersEquals($expected, $actual)
+ {
+ $this->assertEquals($expected->getGmagick(), $actual->getGmagick());
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Gmagick/LoaderTest.php b/src/Symfony/Component/Image/Tests/Gmagick/LoaderTest.php
new file mode 100644
index 0000000000000..c21264af08a89
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Gmagick/LoaderTest.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Gmagick;
+
+use Symfony\Component\Image\Gmagick\Loader;
+use Symfony\Component\Image\Tests\Image\AbstractLoaderTest;
+use Symfony\Component\Image\Image\Box;
+
+class LoaderTest extends AbstractLoaderTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Gmagick')) {
+ $this->markTestSkipped('Gmagick is not installed');
+ }
+ }
+
+ public function testCreateAlphaPrecision()
+ {
+ $this->markTestSkipped('Alpha transparency is not supported by Gmagick');
+ }
+
+ protected function getEstimatedFontBox()
+ {
+ return new Box(117, 55);
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function isFontTestSupported()
+ {
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php b/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php
new file mode 100644
index 0000000000000..e2b915563ad3c
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php
@@ -0,0 +1,817 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\LayersInterface;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Palette\CMYK;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\Grayscale;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\Fill\Gradient\Horizontal;
+use Symfony\Component\Image\Image\Point\Center;
+use Symfony\Component\Image\Tests\TestCase;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Profile;
+use Symfony\Component\Image\Imagick\Image as ImagickImage;
+use Symfony\Component\Image\Gmagick\Image as GmagickImage;
+use Symfony\Component\Image\Imagick\Loader as ImagickLoader;
+use Symfony\Component\Image\Gmagick\Loader as GmagickLoader;
+
+abstract class AbstractImageTest extends TestCase
+{
+ public function testPaletteIsRGBIfRGBImage()
+ {
+ $image = $this->getLoader()->open(__DIR__ . '/../Fixtures/google.png');
+ $this->assertInstanceOf(RGB::class, $image->palette());
+ }
+
+ public function testPaletteIsCMYKIfCMYKImage()
+ {
+ $image = $this->getLoader()->open(__DIR__ . '/../Fixtures/pixel-CMYK.jpg');
+ $this->assertInstanceOf(CMYK::class, $image->palette());
+ }
+
+ public function testPaletteIsGrayIfGrayImage()
+ {
+ $image = $this->getLoader()->open(__DIR__ . '/../Fixtures/pixel-grayscale.jpg');
+ $this->assertInstanceOf(Grayscale::class, $image->palette());
+ }
+
+ public function testDefaultPaletteCreationIsRGB()
+ {
+ $image = $this->getLoader()->create(new Box(10, 10));
+ $this->assertInstanceOf(RGB::class, $image->palette());
+ }
+
+ /**
+ * @dataProvider providePalettes
+ */
+ public function testPaletteAssociatedIsRelatedToGivenColor($paletteClass, $input)
+ {
+ $palette = new $paletteClass();
+
+ $image = $this
+ ->getLoader()
+ ->create(new Box(10, 10), $palette->color($input));
+
+ $this->assertEquals($palette, $image->palette());
+ }
+
+ public function providePalettes()
+ {
+ return array(
+ array(RGB::class, array(255, 0, 0)),
+ array(CMYK::class, array(10, 0, 0, 0)),
+ array(Grayscale::class, array(25)),
+ );
+ }
+
+ /**
+ * @dataProvider provideFromAndToPalettes
+ */
+ public function testUsePalette($from, $to, $color)
+ {
+ $palette = new $from();
+
+ $image = $this
+ ->getLoader()
+ ->create(new Box(10, 10), $palette->color($color));
+
+ $targetPalette = new $to();
+
+ $image->usePalette($targetPalette);
+
+ $this->assertEquals($targetPalette, $image->palette());
+ $image->save(__DIR__ . '/tmp.jpg');
+
+ $image = $this->getLoader()->open(__DIR__ . '/tmp.jpg');
+
+ $this->assertInstanceOf($to, $image->palette());
+ unlink(__DIR__ . '/tmp.jpg');
+ }
+
+ public function testSaveWithoutFormatShouldSaveInOriginalFormat()
+ {
+ if (!extension_loaded('exif')) {
+ $this->markTestSkipped('The EXIF extension is required for this test');
+ }
+
+ $tmpFile = __DIR__ . '/tmpfile';
+
+ $this
+ ->getLoader()
+ ->open(__DIR__ . '/../Fixtures/large.jpg')
+ ->save($tmpFile);
+
+ $data = exif_read_data($tmpFile);
+ $this->assertEquals('image/jpeg', $data['MimeType']);
+ unlink($tmpFile);
+ }
+
+ public function testSaveWithoutPathFileFromImageLoadShouldBeOkay()
+ {
+ $source = __DIR__ . '/../Fixtures/google.png';
+ $tmpFile = __DIR__ . '/../Fixtures/google.tmp.png';
+
+ if (file_exists($tmpFile)) {
+ unlink($tmpFile);
+ }
+
+ copy($source, $tmpFile);
+
+ $this->assertEquals(md5_file($source), md5_file($tmpFile));
+
+ $this
+ ->getLoader()
+ ->open($tmpFile)
+ ->resize(new Box(20, 20))
+ ->save();
+
+ $this->assertNotEquals(md5_file($source), md5_file($tmpFile));
+ unlink($tmpFile);
+ }
+
+ public function testSaveWithoutPathFileFromImageCreationShouldFail()
+ {
+ $image = $this->getLoader()->create(new Box(20, 20));
+ $this->setExpectedException(RuntimeException::class);
+ $image->save();
+ }
+
+ public function provideFromAndToPalettes()
+ {
+ return array(
+ array(
+ RGB::class,
+ CMYK::class,
+ array(10, 10, 10),
+ ),
+ array(
+ RGB::class,
+ Grayscale::class,
+ array(10, 10, 10),
+ ),
+ array(
+ CMYK::class,
+ RGB::class,
+ array(10, 10, 10, 0),
+ ),
+ array(
+ CMYK::class,
+ Grayscale::class,
+ array(10, 10, 10, 0),
+ ),
+ array(
+ Grayscale::class,
+ RGB::class,
+ array(10),
+ ),
+ array(
+ Grayscale::class,
+ CMYK::class,
+ array(10),
+ ),
+ );
+ }
+
+ public function testProfile()
+ {
+ $image = $this
+ ->getLoader()
+ ->create(new Box(10, 10))
+ ->profile(Profile::fromPath(__DIR__ . '/../../Resources/Adobe/RGB/VideoHD.icc'));
+
+ $color = $image->getColorAt(new Point(0, 0));
+
+ $this->assertInstanceOf(RGB::class, $color->getPalette());
+ $this->assertSame(255, $color->getValue(ColorInterface::COLOR_RED));
+ $this->assertSame(255, $color->getValue(ColorInterface::COLOR_GREEN));
+ $this->assertSame(255, $color->getValue(ColorInterface::COLOR_BLUE));
+ $this->assertSame(100, $color->getAlpha());
+ }
+
+ public function testRotateWithNoBackgroundColor()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+ $image->rotate(90);
+
+ $size = $image->getSize();
+
+ $this->assertSame(126, $size->getWidth());
+ $this->assertSame(364, $size->getHeight());
+ }
+
+ public function testCopyResizedImageToImage()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+ $size = $image->getSize();
+
+ $image = $image->paste(
+ $image->copy()
+ ->resize($size->scale(0.5))
+ ->flipVertically(),
+ new Center($size)
+ );
+
+ $this->assertSame(364, $image->getSize()->getWidth());
+ $this->assertSame(126, $image->getSize()->getHeight());
+ }
+
+ /**
+ * @dataProvider provideFilters
+ */
+ public function testResizeWithVariousFilters($filter)
+ {
+ $factory = $this->getLoader();
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+
+ $image = $image->resize(new Box(30, 30), $filter);
+
+ $this->assertSame(30, $image->getSize()->getWidth());
+ $this->assertSame(30, $image->getSize()->getHeight());
+ }
+
+ public function testResizeWithInvalidFilter()
+ {
+ $factory = $this->getLoader();
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+
+ $this->setExpectedException(InvalidArgumentException::class);
+ $image->resize(new Box(30, 30), 'no filter');
+ }
+
+ public function provideFilters()
+ {
+ return array(
+ array(ImageInterface::FILTER_UNDEFINED),
+ array(ImageInterface::FILTER_POINT),
+ array(ImageInterface::FILTER_BOX),
+ array(ImageInterface::FILTER_TRIANGLE),
+ array(ImageInterface::FILTER_HERMITE),
+ array(ImageInterface::FILTER_HANNING),
+ array(ImageInterface::FILTER_HAMMING),
+ array(ImageInterface::FILTER_BLACKMAN),
+ array(ImageInterface::FILTER_GAUSSIAN),
+ array(ImageInterface::FILTER_QUADRATIC),
+ array(ImageInterface::FILTER_CUBIC),
+ array(ImageInterface::FILTER_CATROM),
+ array(ImageInterface::FILTER_MITCHELL),
+ array(ImageInterface::FILTER_LANCZOS),
+ array(ImageInterface::FILTER_BESSEL),
+ array(ImageInterface::FILTER_SINC),
+ );
+ }
+
+ public function testThumbnailShouldReturnACopy()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+ $thumbnail = $image->thumbnail(new Box(20, 20));
+
+ $this->assertNotSame($image, $thumbnail);
+ }
+
+ public function testThumbnailWithInvalidModeShouldThrowAnException()
+ {
+ $factory = $this->getLoader();
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+ $this->setExpectedException(InvalidArgumentException::class, 'Invalid mode specified');
+ $image->thumbnail(new Box(20, 20), "boumboum");
+ }
+
+ public function testResizeShouldReturnTheImage()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+ $resized = $image->resize(new Box(20, 20));
+
+ $this->assertSame($image, $resized);
+ }
+
+ /**
+ * @dataProvider provideDimensionsAndModesForThumbnailGeneration
+ */
+ public function testThumbnailGeneration($sourceW, $sourceH, $thumbW, $thumbH, $mode, $expectedW, $expectedH)
+ {
+ $factory = $this->getLoader();
+ $image = $factory->create(new Box($sourceW, $sourceH));
+ $inset = $image->thumbnail(new Box($thumbW, $thumbH), $mode);
+
+ $size = $inset->getSize();
+
+ $this->assertEquals($expectedW, $size->getWidth());
+ $this->assertEquals($expectedH, $size->getHeight());
+ }
+
+ public function provideDimensionsAndModesForThumbnailGeneration()
+ {
+ return array(
+ // landscape with smaller portrait
+ array(320, 240, 32, 48, ImageInterface::THUMBNAIL_INSET, 32, round(32 * 240 / 320)),
+ array(320, 240, 32, 48, ImageInterface::THUMBNAIL_OUTBOUND, 32, 48),
+ // landscape with smaller landscape
+ array(320, 240, 32, 16, ImageInterface::THUMBNAIL_INSET, round(16 * 320 / 240), 16),
+ array(320, 240, 32, 16, ImageInterface::THUMBNAIL_OUTBOUND, 32, 16),
+
+ // portait with smaller portrait
+ array(240, 320, 24, 48, ImageInterface::THUMBNAIL_INSET, 24, round(24 * 320 / 240)),
+ array(240, 320, 24, 48, ImageInterface::THUMBNAIL_OUTBOUND, 24, 48),
+ // portait with smaller landscape
+ array(240, 320, 24, 16, ImageInterface::THUMBNAIL_INSET, round(16 * 240 / 320), 16),
+ array(240, 320, 24, 16, ImageInterface::THUMBNAIL_OUTBOUND, 24, 16),
+
+ // landscape with larger portrait
+ array(32, 24, 320, 300, ImageInterface::THUMBNAIL_INSET, 32, 24),
+ array(32, 24, 320, 300, ImageInterface::THUMBNAIL_OUTBOUND, 32, 24),
+ // landscape with larger landscape
+ array(32, 24, 320, 200, ImageInterface::THUMBNAIL_INSET, 32, 24),
+ array(32, 24, 320, 200, ImageInterface::THUMBNAIL_OUTBOUND, 32, 24),
+
+ // portait with larger portrait
+ array(24, 32, 240, 300, ImageInterface::THUMBNAIL_INSET, 24, 32),
+ array(24, 32, 240, 300, ImageInterface::THUMBNAIL_OUTBOUND, 24, 32),
+ // portait with larger landscape
+ array(24, 32, 240, 400, ImageInterface::THUMBNAIL_INSET, 24, 32),
+ array(24, 32, 240, 400, ImageInterface::THUMBNAIL_OUTBOUND, 24, 32),
+
+ // landscape with intersect portrait
+ array(320, 240, 340, 220, ImageInterface::THUMBNAIL_INSET, round(220 * 320 / 240), 220),
+ array(320, 240, 340, 220, ImageInterface::THUMBNAIL_OUTBOUND, 320, 220),
+ // landscape with intersect portrait
+ array(320, 240, 300, 360, ImageInterface::THUMBNAIL_INSET, 300, round(300 / 320 * 240)),
+ array(320, 240, 300, 360, ImageInterface::THUMBNAIL_OUTBOUND, 300, 240),
+ );
+ }
+
+ public function testThumbnailGenerationToDimensionsLergestThanSource()
+ {
+ $test_image = __DIR__.'/../Fixtures/google.png';
+ $test_image_width = 364;
+ $test_image_height = 126;
+ $width = $test_image_width + 1;
+ $height = $test_image_height + 1;
+
+ $factory = $this->getLoader();
+ $image = $factory->open($test_image);
+ $size = $image->getSize();
+
+ $this->assertEquals($test_image_width, $size->getWidth());
+ $this->assertEquals($test_image_height, $size->getHeight());
+
+ $inset = $image->thumbnail(new Box($width, $height), ImageInterface::THUMBNAIL_INSET);
+ $size = $inset->getSize();
+ unset($inset);
+
+ $this->assertEquals($test_image_width, $size->getWidth());
+ $this->assertEquals($test_image_height, $size->getHeight());
+
+ $outbound = $image->thumbnail(new Box($width, $height), ImageInterface::THUMBNAIL_OUTBOUND);
+ $size = $outbound->getSize();
+ unset($outbound);
+ unset($image);
+
+ $this->assertEquals($test_image_width, $size->getWidth());
+ $this->assertEquals($test_image_height, $size->getHeight());
+ }
+
+ public function testCropResizeFlip()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png')
+ ->crop(new Point(0, 0), new Box(126, 126))
+ ->resize(new Box(200, 200))
+ ->flipHorizontally();
+
+ $size = $image->getSize();
+
+ unset($image);
+
+ $this->assertEquals(200, $size->getWidth());
+ $this->assertEquals(200, $size->getHeight());
+ }
+
+ public function testCreateAndSaveEmptyImage()
+ {
+ $factory = $this->getLoader();
+
+ $palette = new RGB();
+
+ $image = $factory->create(new Box(400, 300), $palette->color('000'));
+
+ $size = $image->getSize();
+
+ unset($image);
+
+ $this->assertEquals(400, $size->getWidth());
+ $this->assertEquals(300, $size->getHeight());
+ }
+
+ public function testCreateTransparentGradient()
+ {
+ $factory = $this->getLoader();
+
+ $palette = new RGB();
+
+ $size = new Box(100, 50);
+ $image = $factory->create($size, $palette->color('f00'));
+
+ $image->paste(
+ $factory->create($size, $palette->color('ff0'))
+ ->applyMask(
+ $factory->create($size)
+ ->fill(
+ new Horizontal(
+ $image->getSize()->getWidth(),
+ $palette->color('fff'),
+ $palette->color('000')
+ )
+ )
+ ),
+ new Point(0, 0)
+ );
+
+ $size = $image->getSize();
+
+ unset($image);
+
+ $this->assertEquals(100, $size->getWidth());
+ $this->assertEquals(50, $size->getHeight());
+ }
+
+ public function testMask()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+
+ $image->applyMask($image->mask())
+ ->save(__DIR__.'/../Fixtures/mask.png');
+
+ $size = $factory->open(__DIR__.'/../Fixtures/mask.png')
+ ->getSize();
+
+ $this->assertEquals(364, $size->getWidth());
+ $this->assertEquals(126, $size->getHeight());
+
+ unlink(__DIR__.'/../Fixtures/mask.png');
+ }
+
+ public function testColorHistogram()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+
+ $this->assertEquals(6438, count($image->histogram()));
+ }
+
+ public function testImageResolutionChange()
+ {
+ $imagine = $this->getLoader();
+ $image = $imagine->open(__DIR__.'/../Fixtures/resize/210-design-19933.jpg');
+ $outfile = __DIR__.'/../Fixtures/resize/reduced.jpg';
+ $image->save($outfile, array(
+ 'resolution-units' => ImageInterface::RESOLUTION_PIXELSPERINCH,
+ 'resolution-x' => 144,
+ 'resolution-y' => 144
+ ));
+
+ if ($imagine instanceof ImagickLoader) {
+ $i = new \Imagick($outfile);
+ $info = $i->identifyimage();
+ $this->assertEquals(144, $info['resolution']['x']);
+ $this->assertEquals(144, $info['resolution']['y']);
+ }
+ if ($imagine instanceof GmagickLoader) {
+ $i = new \Gmagick($outfile);
+ $info = $i->getimageresolution();
+ $this->assertEquals(144, $info['x']);
+ $this->assertEquals(144, $info['y']);
+ }
+
+ unlink($outfile);
+ }
+
+ public function testInOutResult()
+ {
+ $this->processInOut("trans", "png","png");
+ $this->processInOut("trans", "png","gif");
+ $this->processInOut("trans", "png","jpg");
+ $this->processInOut("anima", "gif","png");
+ $this->processInOut("anima", "gif","gif");
+ $this->processInOut("anima", "gif","jpg");
+ $this->processInOut("trans", "gif","png");
+ $this->processInOut("trans", "gif","gif");
+ $this->processInOut("trans", "gif","jpg");
+ $this->processInOut("large", "jpg","png");
+ $this->processInOut("large", "jpg","gif");
+ $this->processInOut("large", "jpg","jpg");
+ }
+
+ public function testLayerReturnsALayerInterface()
+ {
+ $factory = $this->getLoader();
+
+ $image = $factory->open(__DIR__.'/../Fixtures/google.png');
+
+ $this->assertInstanceOf(LayersInterface::class, $image->layers());
+ }
+
+ public function testCountAMonoLayeredImage()
+ {
+ $this->assertEquals(1, count($this->getMonoLayeredImage()->layers()));
+ }
+
+ public function testCountAMultiLayeredImage()
+ {
+ if (!$this->supportMultipleLayers()) {
+ $this->markTestSkipped('This driver does not support multiple layers');
+ }
+
+ $this->assertGreaterThan(1, count($this->getMultiLayeredImage()->layers()));
+ }
+
+ public function testLayerOnMonoLayeredImage()
+ {
+ foreach ($this->getMonoLayeredImage()->layers() as $layer) {
+ $this->assertInstanceOf(ImageInterface::class, $layer);
+ $this->assertCount(1, $layer->layers());
+ }
+ }
+
+ public function testLayerOnMultiLayeredImage()
+ {
+ foreach ($this->getMultiLayeredImage()->layers() as $layer) {
+ $this->assertInstanceOf(ImageInterface::class, $layer);
+ $this->assertCount(1, $layer->layers());
+ }
+ }
+
+ public function testChangeColorSpaceAndStripImage()
+ {
+ $color = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/pixel-CMYK.jpg')
+ ->usePalette(new RGB())
+ ->strip()
+ ->getColorAt(new Point(0, 0));
+
+ $this->assertEquals('#0082a2', (string) $color);
+ }
+
+ public function testStripImageWithInvalidProfile()
+ {
+ $image = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/invalid-icc-profile.jpg');
+
+ $color = $image->getColorAt(new Point(0, 0));
+ $image->strip();
+ $afterColor = $image->getColorAt(new Point(0, 0));
+
+ $this->assertEquals((string) $color, (string) $afterColor);
+ }
+
+ public function testGetColorAt()
+ {
+ $color = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/65-percent-black.png')
+ ->getColorAt(new Point(0, 0));
+
+ $this->assertEquals('#000000', (string) $color);
+ $this->assertFalse($color->isOpaque());
+ $this->assertEquals('65', $color->getAlpha());
+ }
+
+ public function testGetColorAtGrayScale()
+ {
+ $color = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/pixel-grayscale.jpg')
+ ->getColorAt(new Point(0, 0));
+
+ $this->assertEquals('#4d4d4d', (string) $color);
+ $this->assertTrue($color->isOpaque());
+ }
+
+ public function testGetColorAtCMYK()
+ {
+ $color = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/pixel-CMYK.jpg')
+ ->getColorAt(new Point(0, 0));
+
+ $this->assertEquals('cmyk(98%, 0%, 30%, 23%)', (string) $color);
+ $this->assertTrue($color->isOpaque());
+ }
+
+ public function testGetColorAtOpaque()
+ {
+ $color = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/100-percent-black.png')
+ ->getColorAt(new Point(0, 0));
+
+ $this->assertEquals('#000000', (string) $color);
+ $this->assertTrue($color->isOpaque());
+
+ $this->assertSame(0, $color->getRed());
+ $this->assertSame(0, $color->getGreen());
+ $this->assertSame(0, $color->getBlue());
+ }
+
+ public function testStripGBRImageHasGoodColors()
+ {
+ $color = $this
+ ->getLoader()
+ ->open(__DIR__.'/../Fixtures/pixel-GBR.jpg')
+ ->strip()
+ ->getColorAt(new Point(0, 0));
+
+ $this->assertEquals('#d07560', (string) $color);
+ }
+
+ // Test whether a simple action such as resizing a GIF works
+ // Using the original animated GIF and a slightly more complex one as reference
+ // anima2.gif courtesy of Cyndi Norrie (http://cyndipop.tumblr.com/) via 15 Folds (http://15folds.com)
+ public function testResizeAnimatedGifResizeResult()
+ {
+ if (!$this->supportMultipleLayers()) {
+ $this->markTestSkipped('This driver does not support multiple layers');
+ }
+
+ $imagine = $this->getLoader();
+
+ $image = $imagine->open(__DIR__.'/../Fixtures/anima.gif');
+
+ // Imagick requires the images to be coalesced first!
+ if ($image instanceof ImagickImage) {
+ $image->layers()->coalesce();
+ }
+
+ foreach ($image->layers() as $frame) {
+ $frame->resize(new Box(121, 124));
+ }
+
+ $image->save(__DIR__.'/../Fixtures/results/anima-half-size.gif', array('animated' => true));
+ @unlink(__DIR__.'/../Fixtures/results/anima-half-size.gif');
+
+ $image = $imagine->open(__DIR__.'/../Fixtures/anima2.gif');
+
+ // Imagick requires the images to be coalesced first!
+ if ($image instanceof ImagickImage) {
+ $image->layers()->coalesce();
+ }
+
+ foreach ($image->layers() as $frame) {
+ $frame->resize(new Box(200, 144));
+ }
+
+ $target = __DIR__.'/../Fixtures/results/anima2-half-size.gif';
+ $image->save($target, array('animated' => true));
+
+ $this->assertFileExists($target);
+
+ @unlink($target);
+ }
+
+ public function testMetadataReturnsMetadataInstance()
+ {
+ $this->assertInstanceOf(MetadataBag::class, $this->getMonoLayeredImage()->metadata());
+ }
+
+ public function testCloningImageResultsInNewMetadataInstance()
+ {
+ $image = $this->getMonoLayeredImage();
+ $originalMetadata = $image->metadata();
+ $clone = clone $image;
+ $this->assertNotSame($originalMetadata, $clone->metadata(), 'The image\'s metadata is the same after cloning the image, but must be a new instance.');
+ }
+
+ public function testImageSizeOnAnimatedGif()
+ {
+ $imagine = $this->getLoader();
+
+ $image = $imagine->open(__DIR__.'/../Fixtures/anima3.gif');
+
+ $size = $image->getSize();
+
+ $this->assertEquals(300, $size->getWidth());
+ $this->assertEquals(200, $size->getHeight());
+ }
+
+ /**
+ * @dataProvider provideVariousSources
+ */
+ public function testResolutionOnSave($source)
+ {
+ $file = __DIR__ . '/test-resolution.jpg';
+
+ $image = $this->getLoader()->open($source);
+ $image->save($file, array(
+ 'resolution-units' => ImageInterface::RESOLUTION_PIXELSPERINCH,
+ 'resolution-x' => 150,
+ 'resolution-y' => 120,
+ 'resampling-filter' => ImageInterface::FILTER_LANCZOS,
+ ));
+
+ $saved = $this->getLoader()->open($file);
+ $this->assertEquals(array('x' => 150, 'y' => 120), $this->getImageResolution($saved));
+ unlink($file);
+ }
+
+ public function provideVariousSources()
+ {
+ return array(
+ array(__DIR__.'/../Fixtures/example.svg'),
+ array(__DIR__.'/../Fixtures/100-percent-black.png'),
+ );
+ }
+
+ public function testFillAlphaPrecision()
+ {
+ $imagine = $this->getLoader();
+ $palette = new RGB();
+ $image = $imagine->create(new Box(1, 1), $palette->color("#f00"));
+ $fill = new Horizontal(100, $palette->color("#f00", 17), $palette->color("#f00", 73));
+ $image->fill($fill);
+
+ $actualColor = $image->getColorAt(new Point(0, 0));
+ $this->assertEquals(17, $actualColor->getAlpha());
+ }
+
+ public function testImageCreatedAlpha()
+ {
+ $palette = new RGB();
+ $image = $this->getLoader()->create(new Box(1, 1), $palette->color("#7f7f7f", 10));
+ $actualColor = $image->getColorAt(new Point(0, 0));
+
+ $this->assertEquals("#7f7f7f", (string) $actualColor);
+ $this->assertEquals(10, $actualColor->getAlpha());
+ }
+
+ abstract protected function getImageResolution(ImageInterface $image);
+
+ private function getMonoLayeredImage()
+ {
+ return $this->getLoader()->open(__DIR__.'/../Fixtures/google.png');
+ }
+
+ private function getMultiLayeredImage()
+ {
+ return $this->getLoader()->open(__DIR__.'/../Fixtures/cat.gif');
+ }
+
+ private function getInconsistentMultiLayeredImage()
+ {
+ return $this->getLoader()->open(__DIR__.'/../Fixtures/anima.gif');
+ }
+
+ protected function processInOut($file, $in, $out)
+ {
+ $factory = $this->getLoader();
+ $class = preg_replace('/\\\\/', "_", get_called_class());
+ $image = $factory->open(__DIR__.'/../Fixtures/'.$file.'.'.$in);
+ $thumb = $image->thumbnail(new Box(50, 50), ImageInterface::THUMBNAIL_OUTBOUND);
+ if (!is_dir(__DIR__.'/../Fixtures/results/in_out')) {
+ mkdir(__DIR__.'/../Fixtures/results/in_out', 0777, true);
+ }
+ $target = __DIR__."/../Fixtures/results/in_out/{$class}_{$file}_from_{$in}_to.{$out}";
+ $thumb->save($target);
+
+ $this->assertFileExists($target);
+ unlink($target);
+ }
+
+ /**
+ * @return \Symfony\Component\Image\Image\LoaderInterface
+ */
+ abstract protected function getLoader();
+
+ /**
+ * @return boolean
+ */
+ abstract protected function supportMultipleLayers();
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php b/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php
new file mode 100644
index 0000000000000..2a4f49b561a6d
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php
@@ -0,0 +1,282 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image;
+
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\OutOfBoundsException;
+use Symfony\Component\Image\Image\LoaderInterface;
+use Symfony\Component\Image\Tests\TestCase;
+
+abstract class AbstractLayersTest extends TestCase
+{
+ public function testMerge()
+ {
+ $palette = new RGB();
+ $image = $this->getLoader()->create(new Box(20, 20), $palette->color('#FFFFFF'));
+ foreach ($image->layers() as $layer) {
+ $layer
+ ->draw()
+ ->polygon(array(new Point(0, 0),new Point(0, 20),new Point(20, 20),new Point(20, 0)), $palette->color('#FF0000'), true);
+ }
+ $image->layers()->merge();
+
+ $this->assertEquals('#ff0000', (string) $image->getColorAt(new Point(5,5)));
+ }
+
+ public function testLayerArrayAccess()
+ {
+ if (defined('HHVM_VERSION_ID')) {
+ $this->markTestSkipped(sprintf('%s is not supported on HHVM', __METHOD__));
+ }
+
+ $firstImage = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $secondImage = $this->getImage(__DIR__ . "/../Fixtures/yellow.gif");
+ $thirdImage = $this->getImage(__DIR__ . "/../Fixtures/blue.gif");
+
+ $layers = $firstImage->layers();
+
+ $this->assertCount(1, $layers);
+
+ $layers[] = $secondImage;
+
+ $this->assertCount(2, $layers);
+ $this->assertLayersEquals($firstImage, $layers[0]);
+ $this->assertLayersEquals($secondImage, $layers[1]);
+
+ $layers[1] = $thirdImage;
+
+ $this->assertCount(2, $layers);
+ $this->assertLayersEquals($firstImage, $layers[0]);
+ $this->assertLayersEquals($thirdImage, $layers[1]);
+
+ $layers[] = $secondImage;
+
+ $this->assertCount(3, $layers);
+ $this->assertLayersEquals($firstImage, $layers[0]);
+ $this->assertLayersEquals($thirdImage, $layers[1]);
+ $this->assertLayersEquals($secondImage, $layers[2]);
+
+ $this->assertTrue(isset($layers[2]));
+ $this->assertTrue(isset($layers[1]));
+ $this->assertTrue(isset($layers[0]));
+
+ unset($layers[1]);
+
+ $this->assertCount(2, $layers);
+ $this->assertLayersEquals($firstImage, $layers[0]);
+ $this->assertLayersEquals($secondImage, $layers[1]);
+
+ $this->assertFalse(isset($layers[2]));
+ $this->assertTrue(isset($layers[1]));
+ $this->assertTrue(isset($layers[0]));
+ }
+
+ public function testLayerAddGetSetRemove()
+ {
+ if (defined('HHVM_VERSION_ID')) {
+ $this->markTestSkipped(sprintf('%s is not supported on HHVM', __METHOD__));
+ }
+
+ $firstImage = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $secondImage = $this->getImage(__DIR__ . "/../Fixtures/yellow.gif");
+ $thirdImage = $this->getImage(__DIR__ . "/../Fixtures/blue.gif");
+
+ $layers = $firstImage->layers();
+
+ $this->assertCount(1, $layers);
+
+ $layers->add($secondImage);
+
+ $this->assertCount(2, $layers);
+ $this->assertLayersEquals($firstImage, $layers->get(0));
+ $this->assertLayersEquals($secondImage, $layers->get(1));
+
+ $layers->set(1, $thirdImage);
+
+ $this->assertCount(2, $layers);
+ $this->assertLayersEquals($firstImage, $layers->get(0));
+ $this->assertLayersEquals($thirdImage, $layers->get(1));
+
+ $layers->add($secondImage);
+
+ $this->assertCount(3, $layers);
+ $this->assertLayersEquals($firstImage, $layers->get(0));
+ $this->assertLayersEquals($thirdImage, $layers->get(1));
+ $this->assertLayersEquals($secondImage, $layers->get(2));
+
+ $this->assertTrue($layers->has(2));
+ $this->assertTrue($layers->has(1));
+ $this->assertTrue($layers->has(0));
+
+ $layers->remove(1);
+
+ $this->assertCount(2, $layers);
+ $this->assertLayersEquals($firstImage, $layers->get(0));
+ $this->assertLayersEquals($secondImage, $layers->get(1));
+
+ $this->assertFalse($layers->has(2));
+ $this->assertTrue($layers->has(1));
+ $this->assertTrue($layers->has(0));
+ }
+
+ /**
+ * @dataProvider provideInvalidArguments
+ */
+ public function testLayerArrayAccessInvalidArgumentExceptions($offset)
+ {
+ $firstImage = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $secondImage = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+
+ $layers = $firstImage->layers();
+
+ try {
+ $layers[$offset] = $secondImage;
+ $this->fail('An exception should have been raised');
+ } catch (InvalidArgumentException $e) {
+ $this->assertSame('Invalid offset for layer, it must be an integer', $e->getMessage());
+ }
+ }
+
+ /**
+ * @dataProvider provideOutOfBoundsArguments
+ */
+ public function testLayerArrayAccessOutOfBoundsExceptions($offset)
+ {
+ $firstImage = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $secondImage = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+
+ $layers = $firstImage->layers();
+
+ try {
+ $layers[$offset] = $secondImage;
+ $this->fail('An exception should have been raised');
+ } catch (OutOfBoundsException $e) {
+ $this->assertSame(sprintf('Invalid offset for layer, it must be a value between 0 and 1, %s given', $offset), $e->getMessage());
+ }
+ }
+
+ public function testAnimateEmpty()
+ {
+ $image = $this->getImage();
+ $layers = $image->layers();
+
+ $layers[] = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $layers[] = $this->getImage(__DIR__ . "/../Fixtures/yellow.gif");
+ $layers[] = $this->getImage(__DIR__ . "/../Fixtures/blue.gif");
+
+ $target = __DIR__ . '/../Fixtures/temporary-gif.gif';
+
+ $image->save($target, array(
+ 'animated' => true,
+ ));
+
+ $this->assertFileExists($target);
+
+ @unlink($target);
+ }
+
+ /**
+ * @dataProvider provideAnimationParameters
+ */
+ public function testAnimateWithParameters($delay, $loops)
+ {
+ $image = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $layers = $image->layers();
+
+ $layers[] = $this->getImage(__DIR__ . "/../Fixtures/yellow.gif");
+ $layers[] = $this->getImage(__DIR__ . "/../Fixtures/blue.gif");
+
+ $target = __DIR__ . '/../Fixtures/temporary-gif.gif';
+
+ $image->save($target, array(
+ 'animated' => true,
+ 'animated.delay' => $delay,
+ 'animated.loops' => $loops,
+ ));
+
+ $this->assertFileExists($target);
+
+ @unlink($target);
+ }
+
+ public function provideAnimationParameters()
+ {
+ return array(
+ array(0, 0),
+ array(500, 0),
+ array(0, 10),
+ array(5000, 10),
+ );
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ * @dataProvider provideWrongAnimationParameters
+ */
+ public function testAnimateWithWrongParameters($delay, $loops)
+ {
+ $image = $this->getImage(__DIR__ . "/../Fixtures/pink.gif");
+ $layers = $image->layers();
+
+ $layers[] = $this->getImage(__DIR__ . "/../Fixtures/yellow.gif");
+ $layers[] = $this->getImage(__DIR__ . "/../Fixtures/blue.gif");
+
+ $target = __DIR__ . '/../Fixtures/temporary-gif.gif';
+
+ $image->save($target, array(
+ 'animated' => true,
+ 'animated.delay' => $delay,
+ 'animated.loops' => $loops,
+ ));
+
+ @unlink($target);
+ }
+
+ public function provideWrongAnimationParameters()
+ {
+ return array(
+ array(-1, 0),
+ array(500, -1),
+ array(-1, 10),
+ array(0, -1),
+ );
+ }
+
+ public function provideInvalidArguments()
+ {
+ return array(
+ array('lambda'),
+ array('0'),
+ array('1'),
+ array(1.0),
+ );
+ }
+
+ public function provideOutOfBoundsArguments()
+ {
+ return array(
+ array(-1),
+ array(2),
+ );
+ }
+
+ abstract protected function getImage($path = null);
+
+ /**
+ * @return LoaderInterface
+ */
+ abstract protected function getLoader();
+ abstract protected function assertLayersEquals($expected, $actual);
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php b/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php
new file mode 100644
index 0000000000000..a3795b5c60070
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php
@@ -0,0 +1,206 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image;
+
+use Symfony\Component\Image\Exception\InvalidArgumentException;
+use Symfony\Component\Image\Exception\RuntimeException;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\Color;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Tests\TestCase;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\LoaderInterface;
+
+abstract class AbstractLoaderTest extends TestCase
+{
+ public function testShouldCreateEmptyImage()
+ {
+ $factory = $this->getLoader();
+ $image = $factory->create(new Box(50, 50));
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(50, $size->getWidth());
+ $this->assertEquals(50, $size->getHeight());
+ }
+
+ public function testShouldOpenAnImage()
+ {
+ $source = __DIR__.'/../Fixtures/google.png';
+ $factory = $this->getLoader();
+ $image = $factory->open($source);
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(364, $size->getWidth());
+ $this->assertEquals(126, $size->getHeight());
+
+ $metadata = $image->metadata();
+
+ $this->assertEquals($source, $metadata['uri']);
+ $this->assertEquals(realpath($source), $metadata['filepath']);
+ }
+
+ public function testShouldOpenAnSplFileResource()
+ {
+ $source = __DIR__.'/../Fixtures/google.png';
+ $resource = new \SplFileInfo($source);
+ $factory = $this->getLoader();
+ $image = $factory->open($resource);
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(364, $size->getWidth());
+ $this->assertEquals(126, $size->getHeight());
+
+ $metadata = $image->metadata();
+
+ $this->assertEquals($source, $metadata['uri']);
+ $this->assertEquals(realpath($source), $metadata['filepath']);
+ }
+
+ public function testShouldFailOnUnknownImage()
+ {
+ $invalidResource = __DIR__.'/path/that/does/not/exist';
+
+ $this->setExpectedException(InvalidArgumentException::class, sprintf('File %s does not exist.', $invalidResource));
+ $this->getLoader()->open($invalidResource);
+ }
+
+ public function testShouldFailOnInvalidImage()
+ {
+ $source = __DIR__.'/../Fixtures/invalid-image.jpg';
+
+ $this->setExpectedException(RuntimeException::class, sprintf('Unable to open image %s', $source));
+ $this->getLoader()->open($source);
+ }
+
+ public function testShouldOpenAnHttpImage()
+ {
+ $factory = $this->getLoader();
+ $image = $factory->open(self::HTTP_IMAGE);
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(280, $size->getWidth());
+ $this->assertEquals(140, $size->getHeight());
+
+ $metadata = $image->metadata();
+
+ $this->assertEquals(self::HTTP_IMAGE, $metadata['uri']);
+ $this->assertArrayNotHasKey('filepath', $metadata);
+ }
+
+ public function testShouldCreateImageFromString()
+ {
+ $factory = $this->getLoader();
+ $image = $factory->load(file_get_contents(__DIR__.'/../Fixtures/google.png'));
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(364, $size->getWidth());
+ $this->assertEquals(126, $size->getHeight());
+
+ $metadata = $image->metadata();
+
+ $this->assertArrayNotHasKey('uri', $metadata);
+ $this->assertArrayNotHasKey('filepath', $metadata);
+ }
+
+ public function testShouldCreateImageFromStreamWithMetadata()
+ {
+ $source = 'http://imagine.readthedocs.org/en/latest/_static/exit-90-test.jpg';
+ $resource = fopen($source, 'r');
+
+ $factory = $this->getLoader();
+ $image = $factory->read($resource);
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(50, $size->getWidth());
+ $this->assertEquals(50, $size->getHeight());
+
+ $metadata = $image->metadata();
+
+ $this->assertEquals($source, $metadata['uri']);
+ $this->assertEquals(realpath($source), $metadata['filepath']);
+ $this->assertEquals(6, $metadata['ifd0.Orientation']);
+ }
+
+ public function testShouldCreateImageFromResource()
+ {
+ $source = __DIR__.'/../Fixtures/google.png';
+ $factory = $this->getLoader();
+ $resource = fopen($source, 'r');
+ $image = $factory->read($resource);
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(364, $size->getWidth());
+ $this->assertEquals(126, $size->getHeight());
+
+ $metadata = $image->metadata();
+
+ $this->assertEquals($source, $metadata['uri']);
+ $this->assertEquals(realpath($source), $metadata['filepath']);
+ }
+
+ public function testShouldCreateImageFromHttpResource()
+ {
+ $factory = $this->getLoader();
+ $resource = fopen(self::HTTP_IMAGE, 'r');
+ $image = $factory->read($resource);
+ $size = $image->getSize();
+
+ $this->assertInstanceOf(ImageInterface::class, $image);
+ $this->assertEquals(280, $size->getWidth());
+ $this->assertEquals(140, $size->getHeight());
+
+ $metadata = $image->metadata();
+
+ $this->assertEquals(self::HTTP_IMAGE, $metadata['uri']);
+ $this->assertArrayNotHasKey('filepath', $metadata);
+ }
+
+ public function testShouldDetermineFontSize()
+ {
+ if (!$this->isFontTestSupported()) {
+ $this->markTestSkipped('This install does not support font tests');
+ }
+
+ $palette = new RGB();
+ $path = __DIR__.'/../Fixtures/font/Arial.ttf';
+ $black = $palette->color('000');
+ $factory = $this->getLoader();
+
+ $this->assertEquals($this->getEstimatedFontBox(), $factory->font($path, 36, $black)->box('string'));
+ }
+
+ public function testCreateAlphaPrecision()
+ {
+ $imagine = $this->getLoader();
+ $palette = new RGB();
+ $image = $imagine->create(new Box(1, 1), $palette->color("#f00", 17));
+ $actualColor = $image->getColorAt(new Point(0, 0));
+ $this->assertEquals(17, $actualColor->getAlpha());
+ }
+
+ abstract protected function getEstimatedFontBox();
+
+ /**
+ * @return LoaderInterface
+ */
+ abstract protected function getLoader();
+
+ abstract protected function isFontTestSupported();
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/BoxTest.php b/src/Symfony/Component/Image/Tests/Image/BoxTest.php
new file mode 100644
index 0000000000000..29c1c1406f2ec
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/BoxTest.php
@@ -0,0 +1,187 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image;
+
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Tests\TestCase;
+
+class BoxTest extends TestCase
+{
+ /**
+ * @covers \Symfony\Component\Image\Image\Box::getWidth
+ * @covers \Symfony\Component\Image\Image\Box::getHeight
+ *
+ * @dataProvider getSizes
+ *
+ * @param integer $width
+ * @param integer $height
+ */
+ public function testShouldAssignWidthAndHeight($width, $height)
+ {
+ $size = new Box($width, $height);
+
+ $this->assertEquals($width, $size->getWidth());
+ $this->assertEquals($height, $size->getHeight());
+ }
+
+ /**
+ * Data provider for testShouldAssignWidthAndHeight
+ *
+ * @return array
+ */
+ public function getSizes()
+ {
+ return array(
+ array(1, 1),
+ array(10, 10),
+ array(15, 36)
+ );
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Box::__construct
+ *
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ *
+ * @dataProvider getInvalidSizes
+ *
+ * @param integer $width
+ * @param integer $height
+ */
+ public function testShouldThrowExceptionOnInvalidSize($width, $height)
+ {
+ new Box($width, $height);
+ }
+
+ /**
+ * Data provider for testShouldThrowExceptionOnInvalidSize
+ *
+ * @return array
+ */
+ public function getInvalidSizes()
+ {
+ return array(
+ array(0, 0),
+ array(15, 0),
+ array(0, 25),
+ array(-1, 4)
+ );
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Box::contains
+ *
+ * @dataProvider getSizeBoxStartAndExpected
+ *
+ * @param BoxInterface $size
+ * @param BoxInterface $box
+ * @param PointInterface $start
+ * @param Boolean $expected
+ */
+ public function testShouldDetermineIfASizeContainsABoxAtAStartPosition(
+ BoxInterface $size,
+ BoxInterface $box,
+ PointInterface $start,
+ $expected
+ ) {
+ $this->assertEquals($expected, $size->contains($box, $start));
+ }
+
+ /**
+ * Data provider for testShouldDetermineIfASizeContainsABoxAtAStartPosition
+ *
+ * @return array
+ */
+ public function getSizeBoxStartAndExpected()
+ {
+ return array(
+ array(new Box(50, 50), new Box(30, 30), new Point(0, 0), true),
+ array(new Box(50, 50), new Box(30, 30), new Point(20, 20), true),
+ array(new Box(50, 50), new Box(30, 30), new Point(21, 21), false),
+ array(new Box(50, 50), new Box(30, 30), new Point(21, 20), false),
+ array(new Box(50, 50), new Box(30, 30), new Point(20, 22), false),
+ );
+ }
+
+ /**
+ * @cover Symfony\Component\Image\Image\Box::__toString
+ */
+ public function testToString()
+ {
+ $this->assertEquals('100x100 px', (string) new Box(100, 100));
+ }
+
+ public function testShouldScaleBox()
+ {
+ $box = new Box(10, 20);
+
+ $this->assertEquals(new Box(100, 200), $box->scale(10));
+ }
+
+ public function testShouldIncreaseBox()
+ {
+ $box = new Box(10, 20);
+
+ $this->assertEquals(new Box(15, 25), $box->increase(5));
+ }
+
+ /**
+ * @dataProvider getSizesAndSquares
+ *
+ * @param integer $width
+ * @param integer $height
+ * @param integer $square
+ */
+ public function testShouldCalculateSquare($width, $height, $square)
+ {
+ $box = new Box($width, $height);
+
+ $this->assertEquals($square, $box->square());
+ }
+
+ public function getSizesAndSquares()
+ {
+ return array(
+ array(10, 15, 150),
+ array(2, 2, 4),
+ array(9, 8, 72),
+ );
+ }
+
+ /**
+ * @dataProvider getDimensionsAndTargets
+ *
+ * @param integer $width
+ * @param integer $height
+ * @param integer $targetWidth
+ * @param integer $targetHeight
+ */
+ public function testShouldResizeToTargetWidthAndHeight($width, $height, $targetWidth, $targetHeight)
+ {
+ $box = new Box($width, $height);
+ $expected = new Box($targetWidth, $targetHeight);
+
+ $this->assertEquals($expected, $box->widen($targetWidth));
+ $this->assertEquals($expected, $box->heighten($targetHeight));
+ }
+
+ public function getDimensionsAndTargets()
+ {
+ return array(
+ array(10, 50, 50, 250),
+ array(25, 40, 50, 80),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php
new file mode 100644
index 0000000000000..65669f9faa624
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace Symfony\Component\Image\Tests\Image\Fill\Gradient;
+
+use Symfony\Component\Image\Image\Fill\Gradient\Horizontal;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Point;
+
+class HorizontalTest extends LinearTest
+{
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getEnd()
+ */
+ protected function getEnd()
+ {
+ return $this->getColor('fff');
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getStart()
+ */
+ protected function getStart()
+ {
+ return $this->getColor('000');
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getMask()
+ */
+ protected function getFill(ColorInterface $start, ColorInterface $end)
+ {
+ return new Horizontal(100, $start, $end);
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getPointsAndShades()
+ */
+ public function getPointsAndColors()
+ {
+ return array(
+ array($this->getColor('fff'), new Point(100, 5)),
+ array($this->getColor('000'), new Point(0, 15)),
+ array($this->getColor(array(128, 128, 128)), new Point(50, 25))
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php
new file mode 100644
index 0000000000000..cf85e74ac1490
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Fill\Gradient;
+
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Tests\TestCase;
+
+abstract class LinearTest extends TestCase
+{
+ /**
+ * @var \Symfony\Component\Image\Image\Fill\FillInterface
+ */
+ private $fill;
+
+ /**
+ * @var ColorInterface
+ */
+ private $start;
+
+ /**
+ * @var ColorInterface
+ */
+ private $end;
+ protected $palette;
+
+ protected function setUp()
+ {
+ $this->start = $this->getStart();
+ $this->end = $this->getEnd();
+ $this->fill = $this->getFill($this->start, $this->end);
+ }
+
+ /**
+ * @dataProvider getPointsAndColors
+ *
+ * @param integer $shade
+ * @param \Symfony\Component\Image\Image\PointInterface $position
+ */
+ public function testShouldProvideCorrectColorsValues(ColorInterface $color, PointInterface $position)
+ {
+ $this->assertEquals($color, $this->fill->getColor($position));
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Fill\Gradient\Linear::getStart
+ * @covers \Symfony\Component\Image\Image\Fill\Gradient\Linear::getEnd
+ */
+ public function testShouldReturnCorrectStartAndEnd()
+ {
+ $this->assertSame($this->start, $this->fill->getStart());
+ $this->assertSame($this->end, $this->fill->getEnd());
+ }
+
+ protected function getColor($color)
+ {
+ static $palette;
+
+ if (!$palette) {
+ $palette = new RGB();
+ }
+
+ return $palette->color($color);
+ }
+
+ /**
+ * @param ColorInterface $start
+ * @param ColorInterface $end
+ *
+ * @return Symfony\Component\Image\Image\Fill\FillInterface
+ */
+ abstract protected function getFill(ColorInterface $start, ColorInterface $end);
+
+ /**
+ * @return ColorInterface
+ */
+ abstract protected function getStart();
+
+ /**
+ * @return ColorInterface
+ */
+ abstract protected function getEnd();
+
+ /**
+ * @return array
+ */
+ abstract public function getPointsAndColors();
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php
new file mode 100644
index 0000000000000..fcb7fc1fb4f02
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace Symfony\Component\Image\Tests\Image\Fill\Gradient;
+
+use Symfony\Component\Image\Image\Fill\Gradient\Vertical;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Point;
+
+class VerticalTest extends LinearTest
+{
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getEnd()
+ */
+ protected function getEnd()
+ {
+ return $this->getColor('fff');
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getStart()
+ */
+ protected function getStart()
+ {
+ return $this->getColor('000');
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getMask()
+ */
+ protected function getFill(ColorInterface $start, ColorInterface $end)
+ {
+ return new Vertical(100, $start, $end);
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getPointsAndShades()
+ */
+ public function getPointsAndColors()
+ {
+ return array(
+ array($this->getColor('fff'), new Point(5, 100)),
+ array($this->getColor('000'), new Point(15, 0)),
+ array($this->getColor(array(128, 128, 128)), new Point(25, 50))
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php b/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php
new file mode 100644
index 0000000000000..4c2b73b44a19c
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Histogram;
+
+use Symfony\Component\Image\Image\Histogram\Bucket;
+use Symfony\Component\Image\Image\Histogram\Range;
+use Symfony\Component\Image\Tests\TestCase;
+
+class BucketTest extends TestCase
+{
+ private $bucket;
+
+ protected function setUp()
+ {
+ $this->bucket = new Bucket(new Range(0, 63));
+ $this->assertInstanceOf('Countable', $this->bucket);
+ }
+
+ /**
+ * @dataProvider getCountAndValues
+ *
+ * @param integer $count
+ * @param array $values
+ */
+ public function testShouldOnlyRegisterValuesInRange($count, array $values)
+ {
+ foreach ($values as $value) {
+ $this->bucket->add($value);
+ }
+
+ $this->assertEquals($count, $this->bucket->count());
+ }
+
+ public function getCountAndValues()
+ {
+ return array(
+ array(3, array(12, 123, 232, 142, 152, 172, 93, 35, 44)),
+ array(6, array(12, 123, 23, 14, 152, 17, 93, 35, 44)),
+ array(8, array(12, 12, 12, 23, 14, 152, 17, 93, 35, 44)),
+ array(0, array(121, 123, 234, 145, 152, 176, 93, 135, 144)),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php b/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php
new file mode 100644
index 0000000000000..552900b2b89aa
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Histogram;
+
+use Symfony\Component\Image\Image\Histogram\Range;
+use Symfony\Component\Image\Tests\TestCase;
+
+class RangeTest extends TestCase
+{
+ private $start = 0;
+ private $end = 63;
+
+ /**
+ * @dataProvider getExpectedResultsAndValues
+ *
+ * @param Boolean $contains
+ * @param integer $value
+ */
+ public function testShouldDetermineIfContainsValue($contains, $value)
+ {
+ $range = new Range($this->start, $this->end);
+
+ $this->assertEquals($contains, $range->contains($value));
+ }
+
+ public function getExpectedResultsAndValues()
+ {
+ return array(
+ array(true, 12),
+ array(true, 0),
+ array(false, 128),
+ array(false, 63),
+ );
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Image\Exception\OutOfBoundsException
+ */
+ public function testShouldThrowExceptionIfEndIsSmallerThanStart()
+ {
+ new Range($this->end, $this->start);
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Metadata/DefaultMetadataReaderTest.php b/src/Symfony/Component/Image/Tests/Image/Metadata/DefaultMetadataReaderTest.php
new file mode 100644
index 0000000000000..66a2e30fffc1a
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Metadata/DefaultMetadataReaderTest.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Metadata;
+
+use Symfony\Component\Image\Image\Metadata\DefaultMetadataReader;
+
+class DefaultMetadataReaderTest extends MetadataReaderTestCase
+{
+ protected function getReader()
+ {
+ return new DefaultMetadataReader();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Metadata/ExifMetadataReaderTest.php b/src/Symfony/Component/Image/Tests/Image/Metadata/ExifMetadataReaderTest.php
new file mode 100644
index 0000000000000..2efe02fac864e
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Metadata/ExifMetadataReaderTest.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Metadata;
+
+use Symfony\Component\Image\Image\Metadata\ExifMetadataReader;
+
+class ExifMetadataReaderTest extends MetadataReaderTestCase
+{
+ protected function getReader()
+ {
+ return new ExifMetadataReader();
+ }
+
+ public function testExifDataAreReadWithReadFile()
+ {
+ $metadata = $this->getReader()->readFile(__DIR__ . '/../../Fixtures/exifOrientation/90.jpg');
+ $this->assertTrue(isset($metadata['ifd0.Orientation']));
+ $this->assertEquals(6, $metadata['ifd0.Orientation']);
+ }
+
+ public function testExifDataAreReadWithReadHttpFile()
+ {
+ $source = self::HTTP_IMAGE;
+
+ $metadata = $this->getReader()->readFile($source);
+ $this->assertEquals(1, $metadata['ifd0.Orientation']);
+ }
+
+ public function testExifDataAreReadWithReadData()
+ {
+ $metadata = $this->getReader()->readData(file_get_contents(__DIR__ . '/../../Fixtures/exifOrientation/90.jpg'));
+ $this->assertTrue(isset($metadata['ifd0.Orientation']));
+ $this->assertEquals(6, $metadata['ifd0.Orientation']);
+ }
+
+ public function testExifDataAreReadWithReadStream()
+ {
+ $metadata = $this->getReader()->readStream(fopen(__DIR__ . '/../../Fixtures/exifOrientation/90.jpg', 'r'));
+ $this->assertTrue(isset($metadata['ifd0.Orientation']));
+ $this->assertEquals(6, $metadata['ifd0.Orientation']);
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataBagTest.php b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataBagTest.php
new file mode 100644
index 0000000000000..d94b96ff3d61b
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataBagTest.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Metadata;
+
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Tests\TestCase;
+
+class MetadataBagTest extends TestCase
+{
+ public function testArrayAccessImplementation()
+ {
+ $data = array('key1' => 'value1', 'key2' => 'value2');
+ $bag = new MetadataBag($data);
+
+ $this->assertFalse(isset($bag['key3']));
+ $this->assertTrue(isset($bag['key1']));
+ $bag['key3'] = 'value3';
+ $this->assertTrue(isset($bag['key3']));
+ unset($bag['key3']);
+ $this->assertFalse(isset($bag['key3']));
+ $bag['key1'] = 'valuetest';
+ $this->assertEquals('valuetest', $bag['key1']);
+ $this->assertEquals('value2', $bag['key2']);
+ }
+
+ public function testIteratorAggregateImplementation()
+ {
+ $data = array('key1' => 'value1', 'key2' => 'value2');
+ $bag = new MetadataBag($data);
+
+ $this->assertEquals(new \ArrayIterator($data), $bag->getIterator());
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php
new file mode 100644
index 0000000000000..94becbf98c05d
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Metadata;
+
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Metadata\MetadataReaderInterface;
+use Symfony\Component\Image\Tests\TestCase;
+
+/**
+ */
+abstract class MetadataReaderTestCase extends TestCase
+{
+ /**
+ * @return MetadataReaderInterface
+ */
+ abstract protected function getReader();
+
+ public function testReadFromFile()
+ {
+ $source = __DIR__ . '/../../Fixtures/pixel-CMYK.jpg';
+ $metadata = $this->getReader()->readFile($source);
+ $this->assertInstanceOf(MetadataBag::class, $metadata);
+ $this->assertEquals(realpath($source), $metadata['filepath']);
+ $this->assertEquals($source, $metadata['uri']);
+ }
+
+ public function testReadFromExifUncompatibleFile()
+ {
+ $source = __DIR__ . '/../../Fixtures/trans.png';
+ $metadata = $this->getReader()->readFile($source);
+ $this->assertInstanceOf(MetadataBag::class, $metadata);
+ $this->assertEquals(realpath($source), $metadata['filepath']);
+ $this->assertEquals($source, $metadata['uri']);
+ }
+
+ public function testReadFromHttpFile()
+ {
+ $source = self::HTTP_IMAGE;
+ $metadata = $this->getReader()->readFile($source);
+ $this->assertInstanceOf(MetadataBag::class, $metadata);
+ $this->assertFalse(isset($metadata['filepath']));
+ $this->assertEquals($source, $metadata['uri']);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ * @expectedExceptionMessage File /path/to/no/file does not exist.
+ */
+ public function testReadFromInvalidFileThrowsAnException()
+ {
+ $this->getReader()->readFile('/path/to/no/file');
+ }
+
+ public function testReadFromData()
+ {
+ $source = __DIR__ . '/../../Fixtures/pixel-CMYK.jpg';
+ $metadata = $this->getReader()->readData(file_get_contents($source));
+ $this->assertInstanceOf(MetadataBag::class, $metadata);
+ }
+
+ public function testReadFromInvalidDataDoesNotThrowException()
+ {
+ $metadata = $this->getReader()->readData('this is nonsense');
+ $this->assertInstanceOf(MetadataBag::class, $metadata);
+ }
+
+ public function testReadFromStream()
+ {
+ $source = __DIR__ . '/../../Fixtures/pixel-CMYK.jpg';
+ $resource = fopen($source, 'r');
+ $metadata = $this->getReader()->readStream($resource);
+ $this->assertInstanceOf(MetadataBag::class, $metadata);
+ $this->assertEquals(realpath($source), $metadata['filepath']);
+ $this->assertEquals($source, $metadata['uri']);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ * @expectedExceptionMessage Invalid resource provided.
+ */
+ public function testReadFromInvalidStreamThrowsAnException()
+ {
+ $metadata = $this->getReader()->readStream(false);
+ $this->assertInstanceOf(MetadataBag::class, $metadata);
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php
new file mode 100644
index 0000000000000..9b65b2821b556
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php
@@ -0,0 +1,112 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette;
+
+use Symfony\Component\Image\Image\ProfileInterface;
+use Symfony\Component\Image\Tests\TestCase;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+
+abstract class AbstractPaletteTest extends TestCase
+{
+ /**
+ * @dataProvider provideColorAndAlphaTuples
+ */
+ public function testColor($expected, $color, $alpha)
+ {
+ $result = $this->getPalette()->color($color, $alpha);
+ $this->assertInstanceOf(ColorInterface::class, $result);
+ $this->assertEquals((string) $expected, (string) $result);
+ }
+
+ /**
+ * @dataProvider provideColorAndAlpha
+ */
+ public function testColorIsCached($color, $alpha)
+ {
+ $this->assertSame($this->getPalette()->color($color, $alpha), $this->getPalette()->color($color, $alpha));
+ }
+
+ /**
+ * @dataProvider provideColorAndAlpha
+ */
+ public function testColorWithDifferentAlphasAreNotSame($color, $alpha)
+ {
+ $this->assertNotSame($this->getPalette()->color($color, 2), $this->getPalette()->color($color, 0));
+ }
+
+ /**
+ * @dataProvider provideColorsForBlending
+ */
+ public function testBlend($expected, $color1, $color2, $amount)
+ {
+ $result = $this->getPalette()->blend($color1, $color2, $amount);
+ $this->assertInstanceOf(ColorInterface::class, $result);
+ $this->assertEquals((string) $expected, (string) $result);
+ }
+
+ public function testUseProfile()
+ {
+ $this->getMockBuilder(ProfileInterface::class)->getMock();
+
+ $palette = $this->getPalette();
+
+ $new = $this->getMockBuilder(ProfileInterface::class)->getMock();
+ $palette->useProfile($new);
+
+ $this->assertEquals($new, $palette->profile());
+
+ }
+
+ public function testProfile()
+ {
+ $this->assertInstanceOf(ProfileInterface::class, $this->getPalette()->profile());
+ }
+
+ public function testName()
+ {
+ $this->assertInternalType('string', $this->getPalette()->name());
+ }
+
+ public function testPixelDefinition()
+ {
+ $this->assertInternalType('array', $this->getPalette()->pixelDefinition());
+
+ $available = array(
+ ColorInterface::COLOR_RED,
+ ColorInterface::COLOR_GREEN,
+ ColorInterface::COLOR_BLUE,
+ ColorInterface::COLOR_CYAN,
+ ColorInterface::COLOR_MAGENTA,
+ ColorInterface::COLOR_YELLOW,
+ ColorInterface::COLOR_KEYLINE,
+ ColorInterface::COLOR_GRAY,
+ );
+
+ foreach ($this->getPalette()->pixelDefinition() as $color) {
+ $this->assertTrue(in_array($color, $available));
+ }
+ }
+
+ public function testSupportsAlpha()
+ {
+ $this->assertInternalType('boolean', $this->getPalette()->supportsAlpha());
+ }
+
+ abstract public function provideColorAndAlphaTuples();
+
+ abstract public function provideColorsForBlending();
+
+ /**
+ * @return PaletteInterface
+ */
+ abstract protected function getPalette();
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php
new file mode 100644
index 0000000000000..91daf77009713
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette;
+
+use Symfony\Component\Image\Image\Palette\CMYK;
+use Symfony\Component\Image\Image\Palette\Color\CMYK as CMYKColor;
+
+class CMYKTest extends AbstractPaletteTest
+{
+ public function provideColorAndAlphaTuples()
+ {
+ $palette = $this->getPalette();
+
+ return array(
+ array(new CMYKColor($palette, array(1, 2, 3, 4)), array(1, 2, 3, 4), null),
+ array(new CMYKColor($palette, array(4, 3, 2, 1)), array(4, 3, 2, 1), null),
+ array(new CMYKColor($palette, array(0, 33, 67, 99)), array(3, 2, 1), null),
+ array(new CMYKColor($palette, array(0, 0, 0, 0)), array(255, 255, 255), null),
+ array(new CMYKColor($palette, array(0, 0, 0, 100)), array(0, 0, 0), null),
+ );
+ }
+
+ public function provideColorAndAlpha()
+ {
+ return array(
+ array(array(4, 3, 2, 1), null)
+ );
+ }
+
+ public function testColorWithDifferentAlphasAreNotSame($color = null, $alpha = null)
+ {
+ $this->markTestSkipped('CMYK does not support alpha');
+ }
+
+ public function provideColorsForBlending()
+ {
+ $palette = $this->getPalette();
+
+ return array(
+ array(
+ new CMYKColor($palette, array(56, 29, 38, 48)),
+ new CMYKColor($palette, array(1, 2, 3, 4)),
+ new CMYKColor($palette, array(50, 25, 32, 40)),
+ 1.1,
+ ),
+ array(
+ new CMYKColor($palette, array(21, 12, 15, 20)),
+ new CMYKColor($palette, array(1, 2, 3, 4)),
+ new CMYKColor($palette, array(50, 25, 32, 40)),
+ 0.4,
+ ),
+ );
+ }
+
+ protected function getPalette()
+ {
+ return new CMYK();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/AbstractColorTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/AbstractColorTest.php
new file mode 100644
index 0000000000000..967879cab9982
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/AbstractColorTest.php
@@ -0,0 +1,130 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\PaletteInterface;
+use Symfony\Component\Image\Tests\TestCase;
+
+abstract class AbstractColorTest extends TestCase
+{
+ /**
+ * @dataProvider provideColorAndAlphaTuples
+ */
+ public function testGetAlpha($expected, $color)
+ {
+ $this->assertEquals($expected, $color->getAlpha());
+ }
+
+ public function testGetPalette()
+ {
+ $this->assertInstanceOf(PaletteInterface::class, $this->getColor()->getPalette());
+ }
+
+ /**
+ * @dataProvider provideColorAndValueComponents
+ */
+ public function testGetvalue($expected, $color)
+ {
+ $data = array();
+
+ foreach ($color->getPalette()->pixelDefinition() as $component) {
+ $data[$component] = $color->getValue($component);
+ }
+
+ $this->assertEquals($expected, $data);
+ }
+
+ public function testDissolve()
+ {
+ $color = $this->getColor();
+ $alpha = $color->getAlpha();
+ $signature = (string) $color;
+
+ $color = $color->dissolve(2);
+
+ $this->assertEquals(2 + $alpha, $color->getAlpha());
+ $this->assertEquals($signature, (string) $color);
+ }
+
+ public function testLighten()
+ {
+ $color = $this->getColor();
+
+ $data = array();
+
+ foreach ($color->getPalette()->pixelDefinition() as $component) {
+ $data[$component] = $color->getValue($component);
+ }
+
+ $color->lighten(4);
+
+ foreach ($color->getPalette()->pixelDefinition() as $component) {
+ $this->assertLessThanOrEqual($data[$component], $color->getValue($component));
+ }
+ }
+
+ public function testDarken()
+ {
+ $color = $this->getColor();
+
+ $data = array();
+
+ foreach ($color->getPalette()->pixelDefinition() as $component) {
+ $data[$component] = $color->getValue($component);
+ }
+
+ $color->darken(4);
+
+ foreach ($color->getPalette()->pixelDefinition() as $component) {
+ $this->assertGreaterThanOrEqual($data[$component], $color->getValue($component));
+ }
+ }
+
+ /**
+ * @dataProvider provideGrayscaleData
+ */
+ public function testGrayscale($expected, $color)
+ {
+ $this->assertEquals($expected, (string) $color->grayscale());
+ }
+
+ /**
+ * @dataProvider provideOpaqueColors
+ */
+ public function testIsOpaque($color)
+ {
+ $this->assertTrue($color->isOpaque());
+ }
+
+ /**
+ * @dataProvider provideNotOpaqueColors
+ */
+ public function testIsNotOpaque($color)
+ {
+ $this->assertFalse($color->isOpaque());
+ }
+
+ abstract public function provideColorAndValueComponents();
+
+ abstract public function provideOpaqueColors();
+
+ abstract public function provideNotOpaqueColors();
+
+ abstract public function provideGrayscaleData();
+
+ abstract public function provideColorAndAlphaTuples();
+
+ /**
+ * @return ColorInterface
+ */
+ abstract protected function getColor();
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php
new file mode 100644
index 0000000000000..38d7f492479e8
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php
@@ -0,0 +1,75 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\Color\CMYK;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\CMYK as CMYKPalette;
+
+class CMYKTest extends AbstractColorTest
+{
+ /**
+ * @expectedException \Symfony\Component\Image\Exception\RuntimeException
+ */
+ public function testDissolve()
+ {
+ $this->getColor()->dissolve(1);
+ }
+
+ public function provideOpaqueColors()
+ {
+ return array(
+ array($this->getColor()),
+ );
+ }
+
+ public function testIsNotOpaque($color = null)
+ {
+ $this->markTestSkipped('CMYK color can not be not opaque');
+ }
+
+ public function provideNotOpaqueColors()
+ {
+ $this->markTestSkipped('CMYK color can not be not opaque');
+ }
+
+ public function provideGrayscaleData()
+ {
+ return array(
+ array('cmyk(42%, 42%, 42%, 25%)', $this->getColor()),
+ );
+ }
+
+ public function provideColorAndAlphaTuples()
+ {
+ return array(
+ array(null, $this->getColor())
+ );
+ }
+
+ protected function getColor()
+ {
+ return new CMYK(new CMYKPalette(), array(12, 23, 45, 25));
+ }
+
+ public function provideColorAndValueComponents()
+ {
+ return array(
+ array(array(
+ ColorInterface::COLOR_CYAN => 12,
+ ColorInterface::COLOR_MAGENTA => 23,
+ ColorInterface::COLOR_YELLOW => 45,
+ ColorInterface::COLOR_KEYLINE => 25,
+ ), $this->getColor()),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php
new file mode 100644
index 0000000000000..efb5d7739046f
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\Color\Gray;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\Grayscale;
+
+class GrayTest extends AbstractColorTest
+{
+ public function provideOpaqueColors()
+ {
+ return array(
+ array(new Gray(new Grayscale(), array(12), 100)),
+ array(new Gray(new Grayscale(), array(0), 100)),
+ array(new Gray(new Grayscale(), array(255), 100)),
+ );
+ }
+ public function provideNotOpaqueColors()
+ {
+ return array(
+ array($this->getColor()),
+ array(new Gray(new Grayscale(), array(12), 23)),
+ array(new Gray(new Grayscale(), array(0), 45)),
+ array(new Gray(new Grayscale(), array(255), 0)),
+ );
+ }
+
+ public function provideGrayscaleData()
+ {
+ return array(
+ array('#0c0c0c', $this->getColor()),
+ );
+ }
+
+ public function provideColorAndAlphaTuples()
+ {
+ return array(
+ array(14, $this->getColor())
+ );
+ }
+
+ protected function getColor()
+ {
+ return new Gray(new Grayscale(), array(12), 14);
+ }
+
+ public function provideColorAndValueComponents()
+ {
+ return array(
+ array(array(
+ ColorInterface::COLOR_GRAY => 12,
+ ), $this->getColor()),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php
new file mode 100644
index 0000000000000..de7fa13f28f96
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette\Color;
+
+use Symfony\Component\Image\Image\Palette\Color\RGB;
+use Symfony\Component\Image\Image\Palette\Color\ColorInterface;
+use Symfony\Component\Image\Image\Palette\RGB as RGBPalette;
+
+class RGBTest extends AbstractColorTest
+{
+ public function provideOpaqueColors()
+ {
+ return array(
+ array(new RGB(new RGBPalette(), array(12, 123, 245), 100)),
+ array(new RGB(new RGBPalette(), array(0, 0, 0), 100)),
+ array(new RGB(new RGBPalette(), array(255, 255, 255), 100)),
+ );
+ }
+ public function provideNotOpaqueColors()
+ {
+ return array(
+ array($this->getColor()),
+ array(new RGB(new RGBPalette(), array(12, 123, 245), 23)),
+ array(new RGB(new RGBPalette(), array(0, 0, 0), 45)),
+ array(new RGB(new RGBPalette(), array(255, 255, 255), 0)),
+ );
+ }
+
+ public function provideGrayscaleData()
+ {
+ return array(
+ array('#686868', $this->getColor()),
+ );
+ }
+
+ public function provideColorAndAlphaTuples()
+ {
+ return array(
+ array(14, $this->getColor())
+ );
+ }
+
+ protected function getColor()
+ {
+ return new RGB(new RGBPalette(), array(12, 123, 245), 14);
+ }
+
+ public function provideColorAndValueComponents()
+ {
+ return array(
+ array(array(
+ ColorInterface::COLOR_RED => 12,
+ ColorInterface::COLOR_GREEN => 123,
+ ColorInterface::COLOR_BLUE => 245,
+ ), $this->getColor()),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/ColorParserTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/ColorParserTest.php
new file mode 100644
index 0000000000000..0a49973c93445
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/ColorParserTest.php
@@ -0,0 +1,165 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette;
+
+use Symfony\Component\Image\Image\Palette\ColorParser;
+use Symfony\Component\Image\Tests\TestCase;
+
+class ColorParserTest extends TestCase
+{
+ /**
+ * @dataProvider provideRGBdataToParse
+ */
+ public function testParseToRGB($expected, $value)
+ {
+ $parser = new ColorParser();
+
+ $this->assertEquals($expected, $parser->parseToRGB($value));
+ }
+
+ /**
+ * @dataProvider provideRGBdataThatFail
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ */
+ public function testParseToRGBThatFails($value)
+ {
+ $parser = new ColorParser();
+ $parser->parseToRGB($value);
+ }
+
+ /**
+ * @dataProvider provideCMYKdataToParse
+ */
+ public function testParseToCMYK($expected, $value)
+ {
+ $parser = new ColorParser();
+
+ $this->assertEquals($expected, $parser->parseToCMYK($value));
+ }
+
+ /**
+ * @dataProvider provideCMYKdataThatFail
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ */
+ public function testParseToCMYKThatFails($value)
+ {
+ $parser = new ColorParser();
+ $parser->parseToCMYK($value);
+ }
+
+ public function provideRGBdataToParse()
+ {
+ return array(
+ array(array(255, 255, 0), 'ff0'),
+ array(array(255, 255, 0), '#ff0'),
+ array(array(205, 162, 52), 'CDA234'),
+ array(array(205, 162, 52), '#CDA234'),
+ array(array(205, 162, 52), 13476404),
+ array(array(124, 32, 125), array(124, 32, 125)),
+ );
+ }
+
+ public function provideCMYKdataToParse()
+ {
+ return array(
+ array(array(0, 0, 0, 0), 'FFFFFF'),
+ array(array(0, 0, 0, 100), '000000'),
+ array(array(0, 21, 75, 20), 'CDA234'),
+ array(array(0, 21, 75, 20), '#CDA234'),
+ array(array(0, 21, 75, 20), 'cmyk(0, 21, 75, 20)'),
+ array(array(0, 21, 75, 20), 'cmyk(0,21,75,20)'),
+ array(array(0, 21, 75, 20), 'cmyk(0%, 21%, 75%, 20%)'),
+ array(array(0, 21, 75, 20), 'cmyk(0%,21%,75%,20%)'),
+ array(array(0, 21, 75, 20), 13476404),
+ array(array(100, 0, 100, 0), '#00FF00'),
+ array(array(24, 32, 75, 12), array(24, 32, 75, 12)),
+ );
+ }
+
+ public function provideRGBdataThatFail()
+ {
+ $data = array(
+ array(array(0, 1)),
+ array(array(0, 1, 0, 1, 0)),
+ array('1234'),
+ array('#1234'),
+ );
+
+ if (function_exists('imagecreatetruecolor')) {
+ $data[] = array(imagecreatetruecolor(10, 10));
+ }
+
+ return $data;
+ }
+
+ public function provideCMYKdataThatFail()
+ {
+ $data = array(
+ array(array(0, 1)),
+ array(array(0, 1, 0, 1, 0)),
+ array('1234'),
+ array('#1234'),
+ );
+
+ if (function_exists('imagecreatetruecolor')) {
+ $data[] = array(imagecreatetruecolor(10, 10));
+ }
+
+ return $data;
+ }
+
+ /**
+ * @dataProvider provideGrayscaledataToParse
+ */
+ public function testParseToGrayscale($expected, $value)
+ {
+ $parser = new ColorParser();
+
+ $this->assertEquals($expected, $parser->parseToGrayscale($value));
+ }
+
+ /**
+ * @dataProvider provideGrayscaledataThatFail
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ */
+ public function testParseToGrayscaleThatFails($value)
+ {
+ $parser = new ColorParser();
+ $parser->parseToGrayscale($value);
+ }
+
+ public function provideGrayscaledataToParse()
+ {
+ return array(
+ array(array(23), array(23, 23, 23)),
+ array(array(0), array(0, 0, 0)),
+ array(array(255), array(255, 255, 255)),
+ array(array(23), array(23)),
+ array(array(0), array(0)),
+ array(array(255), array(255)),
+ array(array(136), '#888888'),
+ array(array(153), '999999'),
+ array(array(0), '#000000'),
+ array(array(255), 'FFFFFF'),
+ );
+ }
+
+ public function provideGrayscaledataThatFail()
+ {
+ return array(
+ array(array(23, 23, 24)),
+ array(array(0, 0, 1)),
+ array('#656666'),
+ array('777677'),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/GrayscaleTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/GrayscaleTest.php
new file mode 100644
index 0000000000000..d5d88fc7813a4
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/GrayscaleTest.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette;
+
+use Symfony\Component\Image\Image\Palette\Grayscale;
+use Symfony\Component\Image\Image\Palette\Color\Gray;
+
+class GrayscaleTest extends AbstractPaletteTest
+{
+ public function provideColorAndAlphaTuples()
+ {
+ $palette = $this->getPalette();
+
+ return array(
+ array(new Gray($palette, array(23), 0), array(23, 23, 23), null),
+ array(new Gray($palette, array(24), 3), array(24, 24, 24), 3),
+ array(new Gray($palette, array(23), 0), array(23), null),
+ array(new Gray($palette, array(24), 3), array(24), 3),
+ array(new Gray($palette, array(255), 0), array(255), null),
+ array(new Gray($palette, array(0), 0), array(0), null),
+ );
+ }
+
+ public function provideColorAndAlpha()
+ {
+ return array(
+ array(array(23, 23, 23), 0.5),
+ );
+ }
+
+ public function provideColorsForBlending()
+ {
+ $palette = $this->getPalette();
+
+ return array(
+ array(
+ new Gray($palette, array(55), 0),
+ new Gray($palette, array(1), 0),
+ new Gray($palette, array(50), 0),
+ 1.1,
+ ),
+ array(
+ new Gray($palette, array(21), 0),
+ new Gray($palette, array(1), 0),
+ new Gray($palette, array(50), 0),
+ 0.4,
+ ),
+ );
+ }
+
+ protected function getPalette()
+ {
+ return new Grayscale();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/RGBTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/RGBTest.php
new file mode 100644
index 0000000000000..95cb4412b67c8
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Palette/RGBTest.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Palette;
+
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor;
+
+class RGBTest extends AbstractPaletteTest
+{
+ public function provideColorAndAlphaTuples()
+ {
+ $palette = $this->getPalette();
+
+ return array(
+ array(new RGBColor($palette, array(23, 24, 0), 0), array(23, 24, 0), null),
+ array(new RGBColor($palette, array(23, 24, 0), 0), array(23, 24, 0), 0),
+ array(new RGBColor($palette, array(23, 24, 0), 3), array(23, 24, 0), 3),
+ array(new RGBColor($palette, array(129, 127, 168), 3), array(23, 24, 0, 34), 3),
+ array(new RGBColor($palette, array(255, 255, 255), 0), array(0, 0, 0, 0), null),
+ array(new RGBColor($palette, array(0, 0, 0), 0), array(0, 0, 0, 100), null),
+ );
+ }
+
+ public function provideColorAndAlpha()
+ {
+ return array(
+ array(array(23, 24, 0), 0.5),
+ );
+ }
+
+ public function provideColorsForBlending()
+ {
+ $palette = $this->getPalette();
+
+ return array(
+ array(
+ new RGBColor($palette, array(240, 0, 0), 0),
+ new RGBColor($palette, array(230, 0, 0), 0),
+ new RGBColor($palette, array(128, 0, 0), 0),
+ 1.1,
+ ),
+ array(
+ new RGBColor($palette, array(21, 11, 15), 0),
+ new RGBColor($palette, array(1, 2, 3), 0),
+ new RGBColor($palette, array(50, 25, 32), 0),
+ 0.4,
+ ),
+ );
+ }
+
+ protected function getPalette()
+ {
+ return new RGB();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php b/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php
new file mode 100644
index 0000000000000..5ca41d5f00b98
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php
@@ -0,0 +1,90 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image\Point;
+
+use Symfony\Component\Image\Image\Point\Center;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Image\PointInterface;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Tests\TestCase;
+
+class CenterTest extends TestCase
+{
+ /**
+ * @covers \Symfony\Component\Image\Image\Point\Center::getX
+ * @covers \Symfony\Component\Image\Image\Point\Center::getY
+ *
+ * @dataProvider getSizesAndCoordinates
+ *
+ * @param \Symfony\Component\Image\Image\BoxInterface $box
+ * @param \Symfony\Component\Image\Image\PointInterface $expected
+ */
+ public function testShouldGetCenterCoordinates(BoxInterface $box, PointInterface $expected)
+ {
+ $point = new Center($box);
+
+ $this->assertEquals($expected->getX(), $point->getX());
+ $this->assertEquals($expected->getY(), $point->getY());
+ }
+
+ /**
+ * Data provider for testShouldGetCenterCoordinates
+ *
+ * @return array
+ */
+ public function getSizesAndCoordinates()
+ {
+ return array(
+ array(new Box(10, 15), new Point(5, 8)),
+ array(new Box(40, 23), new Point(20, 12)),
+ array(new Box(14, 8), new Point(7, 4)),
+ );
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Point::getX
+ * @covers \Symfony\Component\Image\Image\Point::getY
+ * @covers \Symfony\Component\Image\Image\Point::move
+ *
+ * @dataProvider getMoves
+ *
+ * @param \Symfony\Component\Image\Image\BoxInterface $box
+ * @param integer $move
+ * @param integer $x1
+ * @param integer $y1
+ */
+ public function testShouldMoveByGivenAmount(BoxInterface $box, $move, $x1, $y1)
+ {
+ $point = new Center($box);
+ $shift = $point->move($move);
+
+ $this->assertEquals($x1, $shift->getX());
+ $this->assertEquals($y1, $shift->getY());
+ }
+
+ public function getMoves()
+ {
+ return array(
+ array(new Box(10, 20), 5, 10, 15),
+ array(new Box(5, 37), 2, 5, 21),
+ );
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Point\Center::__toString
+ */
+ public function testToString()
+ {
+ $this->assertEquals('(50, 50)', (string) new Center(new Box(100, 100)));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/PointTest.php b/src/Symfony/Component/Image/Tests/Image/PointTest.php
new file mode 100644
index 0000000000000..1afdc03503960
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/PointTest.php
@@ -0,0 +1,125 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image;
+
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Image\BoxInterface;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Tests\TestCase;
+
+class PointTest extends TestCase
+{
+ /**
+ * @covers \Symfony\Component\Image\Image\Point::getX
+ * @covers \Symfony\Component\Image\Image\Point::getY
+ * @covers \Symfony\Component\Image\Image\Point::in
+ *
+ * @dataProvider getCoordinates
+ *
+ * @param integer $x
+ * @param integer $y
+ * @param BoxInterface $box
+ * @param Boolean $expected
+ */
+ public function testShouldAssignXYCoordinates($x, $y, BoxInterface $box, $expected)
+ {
+ $coordinate = new Point($x, $y);
+
+ $this->assertEquals($x, $coordinate->getX());
+ $this->assertEquals($y, $coordinate->getY());
+
+ $this->assertEquals($expected, $coordinate->in($box));
+ }
+
+ /**
+ * Data provider for testShouldAssignXYCoordinates
+ *
+ * @return array
+ */
+ public function getCoordinates()
+ {
+ return array(
+ array(0, 0, new Box(5, 5), true),
+ array(5, 15, new Box(5, 5), false),
+ array(10, 23, new Box(10, 10), false),
+ array(42, 30, new Box(50, 50), true),
+ array(81, 16, new Box(50, 10), false),
+ );
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Point::__construct
+ *
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ *
+ * @dataProvider getInvalidCoordinates
+ *
+ * @param integer $x
+ * @param integer $y
+ */
+ public function testShouldThrowExceptionOnInvalidCoordinates($x, $y)
+ {
+ new Point($x, $y);
+ }
+
+ /**
+ * Data provider for testShouldThrowExceptionOnInvalidCoordinates
+ *
+ * @return array
+ */
+ public function getInvalidCoordinates()
+ {
+ return array(
+ array(-1, 0),
+ array(0, -1)
+ );
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Point::getX
+ * @covers \Symfony\Component\Image\Image\Point::getY
+ * @covers \Symfony\Component\Image\Image\Point::move
+ *
+ * @dataProvider getMoves
+ *
+ * @param integer $x
+ * @param integer $y
+ * @param integer $move
+ * @param integer $x1
+ * @param integer $y1
+ */
+ public function testShouldMoveByGivenAmount($x, $y, $move, $x1, $y1)
+ {
+ $point = new Point($x, $y);
+ $shift = $point->move($move);
+
+ $this->assertEquals($x1, $shift->getX());
+ $this->assertEquals($y1, $shift->getY());
+ }
+
+ public function getMoves()
+ {
+ return array(
+ array(0, 0, 5, 5, 5),
+ array(20, 30, 5, 25, 35),
+ array(0, 2, 7, 7, 9),
+ );
+ }
+
+ /**
+ * @covers \Symfony\Component\Image\Image\Point::__toString
+ */
+ public function testToString()
+ {
+ $this->assertEquals('(50, 50)', (string) new Point(50, 50));
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Image/ProfileTest.php b/src/Symfony/Component/Image/Tests/Image/ProfileTest.php
new file mode 100644
index 0000000000000..504bbc5e9b6d0
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Image/ProfileTest.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Image;
+
+use Symfony\Component\Image\Image\Profile;
+use Symfony\Component\Image\Tests\TestCase;
+
+class ProfileTest extends TestCase
+{
+ public function testName()
+ {
+ $profile = new Profile('romain', 'neutron');
+ $this->assertEquals('romain', $profile->name());
+ }
+
+ public function testData()
+ {
+ $profile = new Profile('romain', 'neutron');
+ $this->assertEquals('neutron', $profile->data());
+ }
+
+ public function testFromPath()
+ {
+ $file = __DIR__ . '/../../Resources/Adobe/CMYK/JapanColor2001Uncoated.icc';
+ $profile = Profile::fromPath($file);
+
+ $this->assertEquals(basename($file), $profile->name());
+ $this->assertEquals(file_get_contents($file), $profile->data());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException
+ */
+ public function testFromInvalidPath()
+ {
+ $file = __DIR__ . '/non-existent-profile.icc';
+ Profile::fromPath($file);
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Imagick/DrawerTest.php b/src/Symfony/Component/Image/Tests/Imagick/DrawerTest.php
new file mode 100644
index 0000000000000..ecef159ee360b
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Imagick/DrawerTest.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Imagick;
+
+use Symfony\Component\Image\Imagick\Loader;
+use Symfony\Component\Image\Tests\Draw\AbstractDrawerTest;
+
+class DrawerTest extends AbstractDrawerTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Imagick')) {
+ $this->markTestSkipped('Imagick is not installed');
+ }
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function isFontTestSupported()
+ {
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Imagick/EffectsTest.php b/src/Symfony/Component/Image/Tests/Imagick/EffectsTest.php
new file mode 100644
index 0000000000000..322657a07c072
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Imagick/EffectsTest.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Imagick;
+
+use Symfony\Component\Image\Imagick\Loader;
+use Symfony\Component\Image\Tests\Effects\AbstractEffectsTest;
+
+class EffectsTest extends AbstractEffectsTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Imagick')) {
+ $this->markTestSkipped('Imagick is not installed');
+ }
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function getGrayValue()
+ {
+ return '#555555';
+ }
+
+ protected function getComponentGrayValue()
+ {
+ return 85;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php b/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php
new file mode 100644
index 0000000000000..22951c46a6fed
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php
@@ -0,0 +1,154 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Imagick;
+
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Image\Point;
+use Symfony\Component\Image\Imagick\Loader;
+use Symfony\Component\Image\Imagick\Image;
+use Symfony\Component\Image\Image\Palette\CMYK;
+use Symfony\Component\Image\Image\Palette\RGB;
+use Symfony\Component\Image\Tests\Image\AbstractImageTest;
+use Symfony\Component\Image\Image\Box;
+use Symfony\Component\Image\Imagick\Image as ImagickImage;
+
+class ImageTest extends AbstractImageTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Imagick')) {
+ $this->markTestSkipped('Imagick is not installed');
+ }
+ }
+
+ protected function tearDown()
+ {
+ if (class_exists('Imagick')) {
+ $prop = new \ReflectionProperty(ImagickImage::class, 'supportsColorspaceConversion');
+ $prop->setAccessible(true);
+ $prop->setValue(null);
+ }
+
+ parent::tearDown();
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ public function testImageResizeUsesProperMethodBasedOnInputAndOutputSizes()
+ {
+ $imagine = $this->getLoader();
+
+ $image = $imagine->open(__DIR__.'/../Fixtures/resize/210-design-19933.jpg');
+
+ $image
+ ->resize(new Box(1500, 750))
+ ->save(__DIR__.'/../Fixtures/resize/large.png')
+ ;
+
+ $this->assertSame(1500, $image->getSize()->getWidth());
+ $this->assertSame(750, $image->getSize()->getHeight());
+
+ $image
+ ->resize(new Box(100, 50))
+ ->save(__DIR__.'/../Fixtures/resize/small.png')
+ ;
+
+ $this->assertSame(100, $image->getSize()->getWidth());
+ $this->assertSame(50, $image->getSize()->getHeight());
+
+ unlink(__DIR__.'/../Fixtures/resize/large.png');
+ unlink(__DIR__.'/../Fixtures/resize/small.png');
+ }
+
+ public function testAnimatedGifResize()
+ {
+ $imagine = $this->getLoader();
+ $image = $imagine->open(__DIR__.'/../Fixtures/anima3.gif');
+ $image
+ ->resize(new Box(150, 100))
+ ->save(__DIR__.'/../Fixtures/resize/anima3-150x100-actual.gif', array('animated' => true))
+ ;
+ $this->assertImageEquals(
+ $imagine->open(__DIR__.'/../Fixtures/resize/anima3-150x100.gif'),
+ $imagine->open(__DIR__.'/../Fixtures/resize/anima3-150x100-actual.gif')
+ );
+ unlink(__DIR__.'/../Fixtures/resize/anima3-150x100-actual.gif');
+ }
+
+ // Older imagemagick versions does not support colorspace conversion
+ public function testOlderImageMagickDoesNotAffectColorspaceUsageOnConstruct()
+ {
+ if (!$this->supportsMockingImagick()) {
+ $this->markTestSkipped('Imagick can not be mocked on this platform');
+ }
+
+ $palette = new CMYK();
+ $imagick = $this->getMockBuilder('\Imagick')->disableOriginalConstructor()->getMock();
+ $imagick->expects($this->any())
+ ->method('setColorspace')
+ ->will($this->throwException(new \RuntimeException('Method not supported')));
+
+ $prop = new \ReflectionProperty(ImagickImage::class, 'supportsColorspaceConversion');
+ $prop->setAccessible(true);
+ $prop->setValue(false);
+
+ // Avoid test marked as risky
+ $this->assertTrue(true);
+
+ return new Image($imagick, $palette, new MetadataBag());
+ }
+
+ /**
+ * @depends testOlderImageMagickDoesNotAffectColorspaceUsageOnConstruct
+ * @expectedException \Symfony\Component\Image\Exception\RuntimeException
+ * @expectedExceptionMessage Your version of Imagick does not support colorspace conversions.
+ */
+ public function testOlderImageMagickDoesNotAffectColorspaceUsageOnPaletteChange($image)
+ {
+ $image->usePalette(new RGB());
+ }
+
+ public function testAnimatedGifCrop()
+ {
+ $imagine = $this->getLoader();
+ $image = $imagine->open(__DIR__.'/../Fixtures/anima3.gif');
+ $image
+ ->crop(
+ new Point(0, 0),
+ new Box(150, 100)
+ )
+ ->save(__DIR__.'/../Fixtures/crop/anima3-topleft-actual.gif', array('animated' => true))
+ ;
+ $this->assertImageEquals(
+ $imagine->open(__DIR__.'/../Fixtures/crop/anima3-topleft.gif'),
+ $imagine->open(__DIR__.'/../Fixtures/crop/anima3-topleft-actual.gif')
+ );
+ unlink(__DIR__.'/../Fixtures/crop/anima3-topleft-actual.gif');
+ }
+
+
+ protected function supportMultipleLayers()
+ {
+ return true;
+ }
+
+ protected function getImageResolution(ImageInterface $image)
+ {
+ return $image->getImagick()->getImageResolution();
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php b/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php
new file mode 100644
index 0000000000000..afad59e7a7ec4
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php
@@ -0,0 +1,125 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Imagick;
+
+use Symfony\Component\Image\Image\Metadata\MetadataBag;
+use Symfony\Component\Image\Imagick\Image;
+use Symfony\Component\Image\Imagick\Layers;
+use Symfony\Component\Image\Imagick\Loader;
+use Symfony\Component\Image\Tests\Image\AbstractLayersTest;
+use Symfony\Component\Image\Image\ImageInterface;
+use Symfony\Component\Image\Image\Palette\RGB;
+
+class LayersTest extends AbstractLayersTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Imagick')) {
+ $this->markTestSkipped('Imagick is not installed');
+ }
+ }
+
+ public function testCount()
+ {
+ if (!$this->supportsMockingImagick()) {
+ $this->markTestSkipped('Imagick can not be mocked on this platform');
+ }
+
+ $palette = new RGB();
+ $resource = $this->getMockBuilder('\Imagick')->getMock();
+
+ $resource->expects($this->once())
+ ->method('getNumberImages')
+ ->will($this->returnValue(42));
+
+ $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource);
+
+ $this->assertCount(42, $layers);
+ }
+
+ public function testGetLayer()
+ {
+ if (!$this->supportsMockingImagick()) {
+ $this->markTestSkipped('Imagick can not be mocked on this platform');
+ }
+
+ $palette = new RGB();
+ $resource = $this->getMockBuilder('\Imagick')->getMock();
+
+ $resource->expects($this->any())
+ ->method('getNumberImages')
+ ->will($this->returnValue(2));
+
+ $layer = $this->getMockBuilder('\Imagick')->getMock();
+
+ $resource->expects($this->any())
+ ->method('getImage')
+ ->will($this->returnValue($layer));
+
+ $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource);
+
+ foreach ($layers as $layer) {
+ $this->assertInstanceOf(ImageInterface::class, $layer);
+ }
+ }
+
+ public function testCoalesce()
+ {
+ $width = null;
+ $height = null;
+
+ $resource = new \Imagick;
+ $palette = new RGB();
+ $resource->newImage(20, 10, new \ImagickPixel("black"));
+ $resource->newImage(10, 10, new \ImagickPixel("black"));
+
+ $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource);
+ $layers->coalesce();
+
+ foreach ($layers as $layer) {
+ $size = $layer->getSize();
+
+ if ($width === null) {
+ $width = $size->getWidth();
+ } else {
+ $this->assertEquals($width, $size->getWidth());
+ }
+
+ if ($height === null) {
+ $height = $size->getHeight();
+ } else {
+ $this->assertEquals($height, $size->getHeight());
+ }
+ }
+ }
+
+ public function getImage($path = null)
+ {
+ if ($path) {
+ return new Image(new \Imagick($path), new RGB(), new MetadataBag());
+ } else {
+ return new Image(new \Imagick(), new RGB(), new MetadataBag());
+ }
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function assertLayersEquals($expected, $actual)
+ {
+ $this->assertEquals($expected->getImagick(), $actual->getImagick());
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Imagick/LoaderTest.php b/src/Symfony/Component/Image/Tests/Imagick/LoaderTest.php
new file mode 100644
index 0000000000000..a37b8420b9359
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Imagick/LoaderTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests\Imagick;
+
+use Symfony\Component\Image\Imagick\Loader;
+use Symfony\Component\Image\Tests\Image\AbstractLoaderTest;
+use Symfony\Component\Image\Image\Box;
+
+class LoaderTest extends AbstractLoaderTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Imagick')) {
+ $this->markTestSkipped('Imagick is not installed');
+ }
+ }
+
+ public function testShouldOpenAnHttpImage()
+ {
+ if (defined('HHVM_VERSION_ID')) {
+ $this->markTestSkipped('Imagick on HHVM does not support opening URLs');
+ }
+
+ return parent::testShouldOpenAnHttpImage();
+ }
+
+ protected function getEstimatedFontBox()
+ {
+ return new Box(117, 55);
+ }
+
+ protected function getLoader()
+ {
+ return new Loader();
+ }
+
+ protected function isFontTestSupported()
+ {
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue131Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue131Test.php
new file mode 100644
index 0000000000000..bb332345fae7c
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Issues/Issue131Test.php
@@ -0,0 +1,106 @@
+isFile()) {
+ $filenames[] = $fileinfo->getPathname();
+ }
+ }
+
+ return $filenames;
+ }
+
+ private function getImagickLoader($file)
+ {
+ try {
+ $imagine = new ImagickLoader();
+ $image = $imagine->open($file);
+ } catch (RuntimeException $e) {
+ $this->markTestSkipped($e->getMessage());
+ }
+
+ return $image;
+ }
+
+ private function getGmagickLoader($file)
+ {
+ try {
+ $imagine = new GmagickLoader();
+ $image = $imagine->open($file);
+ } catch (RuntimeException $e) {
+ $this->markTestSkipped($e->getMessage());
+ }
+
+ return $image;
+ }
+
+ public function testShouldSaveOneFileWithImagick()
+ {
+ $dir = realpath($this->getTemporaryDir());
+ $targetFile = $dir . '/myfile.png';
+
+ $imagine = $this->getImagickLoader(__DIR__ . '/multi-layer.psd');
+
+ $imagine->save($targetFile);
+
+ if ( ! $this->probeOneFileAndCleanup($dir, $targetFile)) {
+ $this->fail('Imagick failed to generate one file');
+ }
+ }
+
+ public function testShouldSaveOneFileWithGmagick()
+ {
+ $dir = realpath($this->getTemporaryDir());
+ $targetFile = $dir . '/myfile.png';
+
+ $imagine = $this->getGmagickLoader(__DIR__ . '/multi-layer.psd');
+
+ $imagine->save($targetFile);
+
+ if ( ! $this->probeOneFileAndCleanup($dir, $targetFile)) {
+ $this->fail('Gmagick failed to generate one file');
+ }
+ }
+
+ private function probeOneFileAndCleanup($dir, $targetFile)
+ {
+ $this->assertFileExists($targetFile);
+
+ $retval = true;
+ $files = $this->getDirContent($dir);
+ $retval = $retval && count($files) === 1;
+ $file = current($files);
+ $retval = $retval && $targetFile === $file;
+
+ foreach ($files as $file) {
+ unlink($file);
+ }
+
+ rmdir($dir);
+
+ return $retval;
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue17Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue17Test.php
new file mode 100644
index 0000000000000..bbf41bc9c486d
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Issues/Issue17Test.php
@@ -0,0 +1,41 @@
+markTestSkipped($e->getMessage());
+ }
+
+ return $imagine;
+ }
+
+ public function testShouldResize()
+ {
+ $size = new Box(100, 10);
+ $imagine = $this->getLoader();
+
+ $imagine->open(__DIR__.'/../Fixtures/large.jpg')
+ ->thumbnail($size, ImageInterface::THUMBNAIL_OUTBOUND)
+ ->save(__DIR__.'/../Fixtures/resized.jpg');
+
+ $this->assertTrue(file_exists(__DIR__.'/../Fixtures/resized.jpg'));
+ $this->assertEquals(
+ $size,
+ $imagine->open(__DIR__.'/../Fixtures/resized.jpg')->getSize()
+ );
+
+ unlink(__DIR__.'/../Fixtures/resized.jpg');
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php
new file mode 100644
index 0000000000000..39d794ca3abaa
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php
@@ -0,0 +1,37 @@
+markTestSkipped($e->getMessage());
+ }
+
+ return $imagine;
+ }
+
+ public function testShouldSaveGifImageWithMoreThan256TransparentPixels()
+ {
+ $imagine = $this->getLoader();
+ $new = sys_get_temp_dir()."/sample.jpeg";
+
+ $image = $imagine
+ ->open(__DIR__.'/../Fixtures/sample.gif')
+ ->save($new)
+ ;
+
+ $this->assertSame(700, $image->getSize()->getWidth());
+ $this->assertSame(440, $image->getSize()->getHeight());
+
+ unlink($new);
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue67Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue67Test.php
new file mode 100644
index 0000000000000..148fb8363b180
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/Issues/Issue67Test.php
@@ -0,0 +1,34 @@
+markTestSkipped($e->getMessage());
+ }
+
+ return $imagine;
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Image\Exception\RuntimeException
+ */
+ public function testShouldThrowExceptionNotError()
+ {
+ $invalidPath = '/thispathdoesnotexist';
+
+ $imagine = $this->getLoader();
+
+ $imagine->open(__DIR__.'/../Fixtures/large.jpg')
+ ->save($invalidPath . '/myfile.jpg');
+ }
+}
diff --git a/src/Symfony/Component/Image/Tests/Issues/multi-layer.psd b/src/Symfony/Component/Image/Tests/Issues/multi-layer.psd
new file mode 100644
index 0000000000000..14e6e872349a8
Binary files /dev/null and b/src/Symfony/Component/Image/Tests/Issues/multi-layer.psd differ
diff --git a/src/Symfony/Component/Image/Tests/TestCase.php b/src/Symfony/Component/Image/Tests/TestCase.php
new file mode 100644
index 0000000000000..2da59f9815767
--- /dev/null
+++ b/src/Symfony/Component/Image/Tests/TestCase.php
@@ -0,0 +1,71 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Image\Tests;
+
+use PHPUnit\Framework\TestCase as PHPUnitTestCase;
+use Symfony\Component\Image\Tests\Constraint\IsImageEqual;
+
+class TestCase extends PHPUnitTestCase
+{
+ const HTTP_IMAGE = 'http://imagine.readthedocs.org/en/latest/_static/logo.jpg';
+
+ private static $supportMockingImagick;
+
+ /**
+ * Asserts that two images are equal using color histogram comparison method
+ *
+ * @param ImageInterface $expected
+ * @param ImageInterface $actual
+ * @param string $message
+ * @param float $delta
+ * @param integer $buckets
+ */
+ public static function assertImageEquals($expected, $actual, $message = '', $delta = 0.1, $buckets = 4)
+ {
+ $constraint = new IsImageEqual($expected, $delta, $buckets);
+
+ self::assertThat($actual, $constraint, $message);
+ }
+
+ public function setExpectedException($exception, $message = null, $code = null)
+ {
+ if (method_exists(parent::class, 'expectException')) {
+ parent::expectException($exception);
+ if (null !== $message) {
+ parent::expectExceptionMessage($message);
+ }
+ if (null !== $code) {
+ parent::expectExceptionCode($code);
+ }
+ } else {
+ return parent::setExpectedException($exception, $message, $code);
+ }
+ }
+
+ /**
+ * Actually it's not possible on some HHVM versions
+ */
+ protected function supportsMockingImagick()
+ {
+ if (null !== self::$supportMockingImagick) {
+ return self::$supportMockingImagick;
+ }
+
+ try {
+ @$this->getMockBuilder('\Imagick')->disableOriginalConstructor()->getMock();
+ } catch (\Exception $e) {
+ return self::$supportMockingImagick = false;
+ }
+
+ return self::$supportMockingImagick = true;
+ }
+}
diff --git a/src/Symfony/Component/Image/composer.json b/src/Symfony/Component/Image/composer.json
new file mode 100644
index 0000000000000..40eb1c0e4b829
--- /dev/null
+++ b/src/Symfony/Component/Image/composer.json
@@ -0,0 +1,48 @@
+{
+ "name": "symfony/image",
+ "type": "library",
+ "description": "Symfony Image Component",
+ "keywords": [
+ "image manipulation",
+ "image processing",
+ "drawing",
+ "graphics"
+ ],
+ "homepage": "https://symfony.com/",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ },
+ {
+ "name": "Bulat Shakirzyanov",
+ "email": "mallluhuct@gmail.com",
+ "homepage": "http://avalanche123.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.5.9"
+ },
+ "suggest": {
+ "ext-gd": "to use the Symfony Image GD implementation",
+ "ext-imagick": "to use the Symfony Image Imagick implementation",
+ "ext-gmagick": "to use the Symfony Image Gmagick implementation"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\Image\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3-dev"
+ }
+ }
+}
diff --git a/src/Symfony/Component/Image/phpunit.xml.dist b/src/Symfony/Component/Image/phpunit.xml.dist
new file mode 100644
index 0000000000000..16f33f76e4fdd
--- /dev/null
+++ b/src/Symfony/Component/Image/phpunit.xml.dist
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+ ./Tests/
+
+
+
+
+
+ ./
+
+ ./Tests
+ ./vendor
+
+
+
+